Производительность оператора MySQL «IN» на (большом?) Количестве значений

93

В последнее время я экспериментировал с Redis и MongoDB, и может показаться, что часто бывают случаи, когда вы сохраняете массив идентификаторов в MongoDB или Redis. Я буду придерживаться Redis для этого вопроса, так как я спрашиваю об операторе MySQL IN .

Мне было интересно, насколько производительно перечислить большое количество (300-3000) идентификаторов внутри оператора IN, что будет выглядеть примерно так:

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 3000)

Представьте себе что-то столь же простое, как таблица продуктов и категорий, которую вы обычно можете ОБЪЕДИНЯТЬ вместе, чтобы получить продукты из определенной категории . В приведенном выше примере вы можете видеть, что в данной категории в Redis ( category:4:product_ids) я возвращаю все идентификаторы продуктов из категории с идентификатором 4 и помещаю их в указанный выше SELECTзапрос внутри INоператора.

Насколько это эффективно?

Это ситуация "в зависимости от обстоятельств"? Или есть конкретное «это (не) приемлемо», «быстро» или «медленно», или я должен добавить LIMIT 25, или это не помогает?

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 3000)
LIMIT 25

Или мне следует обрезать массив идентификаторов продукта, возвращаемых Redis, чтобы ограничить его до 25 и добавить только 25 идентификаторов в запрос, а не 3000, и LIMITувеличить его до 25 изнутри запроса?

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 25)

Любые предложения / отзывы очень ценятся!

Майкл ван Ройен
источник
Я не совсем понимаю, о чем вы спрашиваете? Один запрос с «id IN (1,2,3, ... 3000))» быстрее, чем 3000 запросов с «id = value». Но соединение с категорией = 4 будет быстрее, чем оба вышеперечисленных.
Роннис
Верно, хотя, поскольку продукт может принадлежать к нескольким категориям, вы не можете указать «category = 4». Используя Redis, я бы сохранил все идентификаторы продуктов, которые принадлежат к определенным категориям, а затем запросил бы их. Я предполагаю, что реальный вопрос в том, как будет id IN (1,2,3 ... 3000)работать по сравнению с таблицей JOIN products_categories. Или это то, что вы говорили?
Майкл ван Ройен,
Просто будьте осторожны с этой ошибкой в ​​MySql stackoverflow.com/questions/3417074/…
Итай Моав -Малимовка
Конечно, нет причин, по которым этот метод не должен быть таким же эффективным, как любой другой метод получения индексированных строк; это просто зависит от того, протестировали ли авторы базы данных и оптимизировали ли их для этого. Что касается вычислительной сложности, мы собираемся в худшем случае выполнить сортировку O (n log N) для INпредложения (это может быть даже линейно в отсортированном списке, как вы показываете, в зависимости от алгоритма), а затем линейное пересечение / поиск .
jberryman

Ответы:

39

Вообще говоря, если INсписок становится слишком большим (для некоторого нечетко определенного значения «слишком большой», которое обычно находится в районе 100 или меньше), становится более эффективным использовать соединение, создавая временную таблицу, если это необходимо. держать числа.

Если числа представляют собой плотный набор (без пробелов - о чем свидетельствуют образцы данных), тогда вы можете добиться большего успеха WHERE id BETWEEN 300 AND 3000.

Однако, по-видимому, в наборе есть пробелы, и в этот момент может быть лучше пойти со списком допустимых значений в конце концов (если только пробелов относительно мало, и в этом случае вы можете использовать:

WHERE id BETWEEN 300 AND 3000 AND id NOT BETWEEN 742 AND 836

Или какие бы там ни были пробелы.

Джонатан Леффлер
источник
46
Не могли бы вы привести пример «использования соединения для создания временной таблицы»?
Джейк,
если набор данных поступил из интерфейса (элемент с множественным выбором) и в выбранных данных есть пробелы, и эти пробелы не являются последовательным пробелом (отсутствуют: 457, 490, 658, ..), тогда AND id NOT BETWEEN XXX AND XXXне будет работать, и лучше придерживайтесь эквивалента, (x = 1 OR x = 2 OR x = 3 ... OR x = 99)как написал @David Fells.
deepcell
по моему опыту - работая над веб-сайтами электронной коммерции, мы должны отображать результаты поиска по ~ 50 несвязанным идентификаторам продуктов, у нас были лучшие результаты с «1. 50 отдельными запросами», по сравнению с «2. одним запросом со многими значениями в« IN » пункт "". На данный момент у меня нет никакого способа доказать это, за исключением того, что запрос №2 всегда будет отображаться как медленный запрос в наших системах мониторинга, тогда как №1 никогда не появится, независимо от того, что количество выполнений находится в миллионы ... есть ли у кого-нибудь такой же опыт? (мы можем связать это с улучшением кэширования или возможностью чередования других запросов между запросами ...)
Хаим Клар
24

Я проводил несколько тестов, и, как сказал Дэвид Феллс в своем ответе , он довольно хорошо оптимизирован. Для справки я создал таблицу InnoDB с 1 000 000 регистров и делаю выборку с оператором «IN» с 500 000 случайных чисел, это занимает всего 2,5 секунды на моем MAC; выбор только четных регистров занимает 0,5 секунды.

Единственная проблема, которая у меня возникла, это то, что мне пришлось увеличить max_allowed_packetпараметр из my.cnfфайла. Если нет, генерируется загадочная ошибка «MYSQL ушел».

Вот код PHP, который я использую для тестирования:

$NROWS =1000000;
$SELECTED = 50;
$NROWSINSERT =15000;

$dsn="mysql:host=localhost;port=8889;dbname=testschema";
$pdo = new PDO($dsn, "root", "root");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$pdo->exec("drop table if exists `uniclau`.`testtable`");
$pdo->exec("CREATE  TABLE `testtable` (
        `id` INT NOT NULL ,
        `text` VARCHAR(45) NULL ,
        PRIMARY KEY (`id`) )");

$before = microtime(true);

$Values='';
$SelValues='(';
$c=0;
for ($i=0; $i<$NROWS; $i++) {
    $r = rand(0,99);
    if ($c>0) $Values .= ",";
    $Values .= "( $i , 'This is value $i and r= $r')";
    if ($r<$SELECTED) {
        if ($SelValues!="(") $SelValues .= ",";
        $SelValues .= $i;
    }
    $c++;

    if (($c==100)||(($i==$NROWS-1)&&($c>0))) {
        $pdo->exec("INSERT INTO `testtable` VALUES $Values");
        $Values = "";
        $c=0;
    }
}
$SelValues .=')';
echo "<br>";


$after = microtime(true);
echo "Insert execution time =" . ($after-$before) . "s<br>";

$before = microtime(true);  
$sql = "SELECT count(*) FROM `testtable` WHERE id IN $SelValues";
$result = $pdo->prepare($sql);  
$after = microtime(true);
echo "Prepare execution time =" . ($after-$before) . "s<br>";

$before = microtime(true);

$result->execute();
$c = $result->fetchColumn();

$after = microtime(true);
echo "Random selection = $c Time execution time =" . ($after-$before) . "s<br>";



$before = microtime(true);

$sql = "SELECT count(*) FROM `testtable` WHERE id %2 = 1";
$result = $pdo->prepare($sql);
$result->execute();
$c = $result->fetchColumn();

$after = microtime(true);
echo "Pairs = $c Exdcution time=" . ($after-$before) . "s<br>";

И результаты:

Insert execution time =35.2927210331s
Prepare execution time =0.0161771774292s
Random selection = 499102 Time execution time =2.40285992622s
Pairs = 500000 Exdcution time=0.465420007706s
jbaylina
источник
Ради других я добавлю, что при запуске в VirtualBox (CentOS) на моем MBP конца 2013 года с i7 третья строка ( имеющая отношение к вопросу) вывода была: Случайный выбор = 500744 Время выполнения = 53.458173036575s .. 53 секунды могут быть приемлемыми в зависимости от вашего приложения. Для меня не совсем. Также обратите внимание, что тест на четные числа не имеет отношения к рассматриваемому вопросу, поскольку он использует оператор по модулю ( %) с оператором равенства ( =) вместо IN().
rinogo
Это актуально, потому что это способ сравнить запрос с оператором IN с аналогичным запросом без этой функции. Возможно, вы получите большее время, потому что это время загрузки, потому что ваша машина подкачивается или работает на другой виртуальной машине.
jbaylina
14

Вы можете создать временную таблицу, в которую вы можете поместить любое количество идентификаторов и выполнить вложенный запрос. Пример:

CREATE [TEMPORARY] TABLE tmp_IDs (`ID` INT NOT NULL,PRIMARY KEY (`ID`));

и выберите:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Владимир Жотов
источник
6
лучше присоединиться к вашей временной таблице вместо использования подзапроса
scharette
3
@loopkin, не могли бы вы объяснить, как это сделать с помощью соединения или подзапроса?
Джефф Соломон
3
@jeffSolomon ВЫБРАТЬ products.id, имя, цену ИЗ продуктов ПРИСОЕДИНЯЙТЕСЬ к tmp_IDs на products.id = tmp_IDs.ID;
scharette
ЭТО ОТВЕТ! это то, что я искал, очень-очень быстро для длинных реестров
Дамиан Рафаэль Латтенеро
Спасибо тебе большое, чувак. Просто работает невероятно быстро.
mrHalfer,
4

Использование INс большим набором параметров в большом списке записей на самом деле будет медленным.

В случае, который я решил недавно, у меня было два предложения where, одно с 2,50 параметрами, а другое с 3500 параметрами, запрашивающими таблицу из 40 миллионов записей.

Мой запрос занял 5 минут по стандарту WHERE IN. Вместо этого используя подзапрос для оператора IN (помещая параметры в их собственную индексированную таблицу), я сократил время запроса до ДВУХ секунд.

По моему опыту, работал как с MySQL, так и с Oracle.

yoyodunno
источник
1
Я не понял вашу точку зрения на «Вместо этого используя подзапрос для оператора IN (помещая параметры в их собственную индексированную таблицу)». Вы имели в виду, что вместо использования «WHERE ID IN (1,2,3)» мы должны использовать «WHERE ID IN (SELECT id FROM xxx)»?
Истияк Портной
4

INв порядке и хорошо оптимизирован. Убедитесь, что вы используете его в индексированном поле, и все в порядке.

Функционально это эквивалентно:

(x = 1 OR x = 2 OR x = 3 ... OR x = 99)

Что касается движка БД.

Дэвид Феллс
источник
1
Не действительно. Я использую IN clouse для извлечения 5k записей из БД. IN clouse содержит список PK, поэтому связанный столбец индексируется и гарантированно уникален. EXPLAIN говорит, что полное сканирование таблицы выполняется при использовании поиска по PK в стиле «fifo-queue-alike».
Антониосс
В MySQL я не считаю, что они «функционально эквивалентны» . INиспользует оптимизацию для повышения производительности.
Джошуа Пинтер
1
Джош, ответ был из 2011 года - я уверен, что с тех пор все изменилось, но в тот день IN был полностью преобразован в серию операторов OR.
Дэвид Феллс
1
Это неправильный ответ. Из высокопроизводительного MySQL : не так в MySQL, который сортирует значения в списке IN () и использует быстрый двоичный поиск, чтобы узнать, есть ли значение в списке. Это O (log n) по размеру списка, тогда как эквивалентная серия предложений OR - O (n) по размеру списка (т. Е. Намного медленнее для больших списков).
Берт
Берт - да. Этот ответ устарел. Не стесняйтесь предлагать правку.
Дэвид Феллс
-2

Когда вы предоставляете много значений для INоператора, он сначала должен отсортировать их, чтобы удалить дубликаты. По крайней мере, я так подозреваю. Поэтому было бы нехорошо указывать слишком много значений, так как сортировка занимает N log N времени.

Мой опыт показал, что разделение набора значений на более мелкие подмножества и объединение результатов всех запросов в приложении дает лучшую производительность. Я признаю, что я получил опыт работы с другой базой данных (Pervasive), но то же самое может относиться ко всем движкам. Мое количество значений в наборе составляло 500–1000. Более-менее было значительно медленнее.

Ярекчек
источник
Я знаю, что прошло 7 лет, но проблема с этим ответом просто в том, что это комментарий, основанный на обоснованном предположении.
Giacomo1968