Почему некоторые типы подготовленных запросов с использованием PDO в PHP с MySQL работают медленно? - PullRequest
23 голосов
/ 04 декабря 2010

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

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

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

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

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

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

  • PDO с подготовкой занимает ~ 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's been fast.  fetch() is the slow part
while ($row = $stmt->fetch()) {
    $rows[] = $row;
}

Ответы [ 3 ]

3 голосов
/ 04 декабря 2010

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

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

2 голосов
/ 07 ноября 2017

В примере кода есть несколько серьезных ошибок.Чтобы быть более точным.

// $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's been fast.  fetch() is the slow part
while ($row = $stmt->fetch()) {
    $rows[] = $row;
}

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

При использовании SELECT * FROM table WHERE Id IN ( .. ) запросов с более чем 10000 клавиш с использованием PDO с prepare () / execute () производительность снижается на ~ 10 раз больше, чем при выполнениитот же запрос с использованием 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 , вы получите время запроса, соответствующее метке, которой они были присвоены.

0 голосов
/ 04 декабря 2010

Не имею никакого опыта работы с 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 ;
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...