Продуктивність оператора 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-ing до 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" буде швидшим, ніж обидва вищезазначені.
Ронніс,

Правильно, хоча, оскільки товар може належати до кількох категорій, я не можеш зробити "категорія = 4". Використовуючи Redis, я зберігав би всі ідентифікатори продуктів, що належать до певних категорій, а потім запитував про це. Думаю, справжнє питання полягає в тому, як би id IN (1,2,3 ... 3000)діяв показник порівняно з таблицею JOIN products_categories. Або це ви говорили?
Michael van Rooijen

Тільки будьте обережні з цією помилкою в MySql stackoverflow.com/questions/3417074 / ...
Ітай Moav -Malimovka

Звичайно, немає жодної причини, чому це не повинно бути таким ефективним, як будь-який інший спосіб отримання індексованих рядків; це лише залежить від того, чи перевіряли та оптимізували її автори баз даних. Що стосується обчислювальної складності, ми будемо робити в гіршому випадку сортування 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. один запит із багатьма значеннями в полі пункт "". На даний момент я не маю можливості довести це, крім того, що запит №2 завжди відображатиметься як повільний запит у наших системах моніторингу, тоді як №1 ніколи не відображатиметься, незалежно від того, що кількість виконаних мільйони ... хтось має такий самий досвід? (ми можемо пов’язати це з кращим кешуванням або дозволом інших запитів переплітатись між запитами ...)
Хаїм Клар

24

Я проводив деякі тести, і, як говорить Девід Феллз у своїй відповіді , це досить добре оптимізовано. В якості довідкового матеріалу я створив таблицю InnoDB з 1 000 000 регістрів і, здійснюючи вибір за допомогою оператора "IN" з 500 000 випадкових чисел, на моєму MAC займає всього 2,5 секунди; вибір лише парних регістрів займає 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

Задля інших, я додам, що запуск у VirtualBox (CentOS) на моєму кінці 2013 MBP з i7, третім рядком (відповідним до питання) результату було: Випадковий вибір = 500744 Час виконання часу = 53.458173036575s .. 53 секунди можуть бути допустимими, залежно від вашої програми. Для мого використання, не дуже. Також зауважте, що тест на парні числа не є актуальним для розглядуваного питання, оскільки він використовує оператор modulo ( %) з оператором дорівнює ( =) замість IN().
риного

Це актуально, оскільки це спосіб порівняти запит з оператором 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.


1
Я не зрозумів вашої думки щодо "Натомість використовуючи підзапит для оператора IN (розміщуючи параметри у власній індексованій таблиці)". Ви мали на увазі, що замість використання "WHERE ID IN (1,2,3)" ми повинні використовувати "WHERE ID IN (SELECT ID FROM xxx)"?
Istiyak Tailor

4

INчудово і добре оптимізовано. Переконайтеся, що ви використовуєте його в індексованому полі, і у вас все добре.

Це функціонально еквівалентно:

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

Що стосується механізму БД.


1
Не зовсім. Я використовую IN clouse для отримання 5k записів з БД. IN clouse містить список ПК, тому відповідний стовпець індексується та гарантується як унікальний. EXPLAIN говорить, що повне сканування таблиці виконується з використанням пошуку PK у стилі "fifo-queue-like".
Антоніосссс

На 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.