Подтвердить что ты не робот

Почему определенные типы подготовленных запросов с использованием PDO в PHP с MySQL медленны?

При использовании SELECT * FROM table WHERE Id IN ( .. ) запросов с более чем 10000 ключами с использованием PDO с помощью команды prepare()/execute() производительность ухудшает ~ 10X больше, чем выполнение того же запроса с использованием mysqli с подготовленными операторами или PDO без использования подготовленных операторов.

Более странные детали:

  • Более типичные операторы SELECT, не имеющие предложения WHERE Id IN( ..), выполняются отлично даже при 100K + строках. SELECT * FROM table WHERE Id, например, быстро.

  • Снижение производительности происходит после завершения подготовки()/execute() - полностью в PDOStatement::fetch() или PDOStatement::fetchAll(). Время выполнения запросов MySQL является крошечным во всех случаях - это не случай оптимизации MySQL.

  • Разделение запроса 10K на 10 запросов с помощью клавиш 1K является выполнимым.

  • Использование mysql, mysqli с подготовленными инструкциями или PDO без подготовленных операторов является выполнимым.

  • PDO w/подготовленный занимает примерно 6 секунд в примере ниже, в то время как другие принимают ~ 0,5 с.

  • В нелинейном режиме становится все хуже, чем больше у вас ключей. Попробуйте ключи 100K.

Пример кода:

// $imageIds is an array with 10K keys
$keyCount = count($imageIds);
$keys = implode(', ', array_fill(0, $keyCount, '?'));
$query = "SELECT * FROM images WHERE ImageID IN ({$keys})";
$stmt = $dbh->prepare($query);
$stmt->execute($imageIds);
// until now, it been fast.  fetch() is the slow part
while ($row = $stmt->fetch()) {
    $rows[] = $row;
}
4b9b3361

Ответ 1

Убедитесь, что вы указываете PDO, что это целое число, а не строка; если PDO ставит его как строку, тогда MySQL должен будет отображать значения для сравнения. В зависимости от того, как это происходит, это может привести к серьезным замедлениям, заставив MySQL избегать использования индекса.

Я не совсем уверен в поведении здесь, но у меня была эта проблема с Postgres несколько лет назад...

Ответ 2

В образце кода есть некоторые серьезные ошибки. Точнее.

// $imageIds is an array with 10K keys
$keyCount = count($imageIds);
$keys = implode(', ', array_fill(0, $keyCount, '?'));
$query = "SELECT * FROM images WHERE ImageID IN ({$keys})";

до сих пор приведенный выше код обеспечит что-то вроде этого...

SELECT * FROM images WHERE ImageID IN (?, ?, ?, ?, ?, ?,...?, ?, ?, ?)

Не существует цикла для привязки... Там должен быть небольшой цикл, в который вы должны привязать все параметры, передаваемые в MySQL. Вы переходите от prepare до execute.. При правильной привязке привязка в первую очередь вы хотите.

$stmt = $dbh->prepare($query);
$stmt->execute($imageIds);
// until now, it been fast.  fetch() is the slow part
while ($row = $stmt->fetch()) {
    $rows[] = $row;
}

Теперь у меня есть простой логический вопрос по этой части вопроса...

При использовании запросов SELECT * FROM table WHERE Id IN ( .. ) с большим количеством чем 10000 ключей с использованием PDO с командой prepare()/execute(), производительность ухудшает ~ 10X больше, чем выполнение одного и того же запроса с использованием mysqli с подготовленные заявления или PDO без использования подготовленных инструкций.

Не было бы лучше, если бы тот же запрос был переписан, чтобы вам не нужно было передавать 10000 ключей в качестве параметров?

PDO и MySQLi не имеют существенных различий в таймингах. Плохие письменные запросы. Очень сложные хранимые процедуры иногда могут оказаться медленными, если они недостаточно оптимизированы.

Проверить, может ли другой запрос получить желаемый результат. Например

Создайте небольшую таблицу с именем test

create table `test` (
  `id` int(10) not null,
  `desc` varchar(255)
  ); 
insert into `test` (`id`,`desc`) values (1,'a'),(10,'a1'),(11,'a2'),(12,'a3'),(13,'a4'),(14,'a5'),(15,'a6'),(2,'ab'),(20,'ab1'),(21,'ab2'),(22,'ab3'),(23,'ab4'),(24,'ab5'),(25,'ab6');

Запустите эти простые запросы

select * from `test` where `id` rlike '^1$';
select * from `test` where `id` rlike '^1+';
select * from `test` where `id`=1;
select * from `test` where `id` rlike '^1.$';
select * from `test` where `id` rlike '.2$';
select * from `test` where `id` rlike '^2$';
select * from `test` where `id` rlike '.(2|3)'; // Slower
select * from `test` where `id` IN (12,13,22,23); // Faster
select * from `test` where `id` IN ('12,13,22,23'); // Wrong result
select * from `test` where `id` IN ('12','13','22','23'); // Slower

Последние 4 запроса имеют тот же результат в этом примере. Я думаю, что большую часть времени, если вы проверите его на SQLFiddle, вы получите время запроса, соответствующее метке, которую они дали.

Ответ 3

У вас нет опыта работы с PDO, поэтому я не могу с этим справиться, но этот метод довольно эффективен, хотя он немного уродлив в местах;)

PHP

<?php

$nums = array(); $max = 10000;

for($i=0;$i<$max*10;$i++) $nums[] = $i;

$conn = new mysqli("127.0.0.1", "vldb_dbo", "pass", "vldb_db", 3306);

$sql = sprintf("call list_products_by_id('%s',0)", implode(",",array_rand($nums, $max)));

$startTime = microtime(true);

$result = $conn->query($sql);

echo sprintf("Fetched %d rows in %s secs<br/>", 
    $conn->affected_rows, number_format(microtime(true) - $startTime, 6, ".", ""));

$result->close();
$conn->close();

?>

Результаты

select count(*) from product;
count(*)
========
1000000

Fetched 1000 rows in 0.014767 secs
Fetched 1000 rows in 0.014629 secs

Fetched 2000 rows in 0.027938 secs
Fetched 2000 rows in 0.027929 secs

Fetched 5000 rows in 0.068841 secs
Fetched 5000 rows in 0.067844 secs

Fetched 7000 rows in 0.095199 secs
Fetched 7000 rows in 0.095184 secs

Fetched 10000 rows in 0.138205 secs
Fetched 10000 rows in 0.134356 secs

MySQL

drop procedure if exists list_products_by_id;

delimiter #

create procedure list_products_by_id
(
in p_prod_id_csv text,
in p_show_explain tinyint unsigned
)
proc_main:begin

declare v_id varchar(10);
declare v_done tinyint unsigned default 0;
declare v_idx int unsigned default 1;

    create temporary table tmp(prod_id int unsigned not null)engine=memory; 

    -- split the string into tokens and put into a temp table...

    if p_prod_id_csv is not null then
        while not v_done do
            set v_id = trim(substring(p_prod_id_csv, v_idx, 
                if(locate(',', p_prod_id_csv, v_idx) > 0, 
                        locate(',', p_prod_id_csv, v_idx) - v_idx, length(p_prod_id_csv))));

                if length(v_id) > 0 then
                set v_idx = v_idx + length(v_id) + 1;
                        insert ignore into tmp values(v_id);
                else
                set v_done = 1;
                end if;
        end while;
    end if;

    if p_show_explain then

        select count(*) as count_of_tmp from tmp;

        explain
        select p.* from product p
        inner join tmp on tmp.prod_id = p.prod_id order by p.prod_id;

    end if;

    select p.* from product p
        inner join tmp on tmp.prod_id = p.prod_id order by p.prod_id;

    drop temporary table if exists tmp;

end proc_main #

delimiter ;