Wydajność operatora MySQL „IN” dla (dużej?) Liczby wartości

93

Ostatnio eksperymentowałem z Redis i MongoDB i wydaje się, że często zdarza się, że przechowujesz tablicę identyfikatorów w MongoDB lub Redis. W przypadku tego pytania zostanę przy Redis, ponieważ pytam o operator MySQL IN .

Zastanawiałem się, jak wydajne jest umieszczenie dużej liczby (300-3000) identyfikatorów wewnątrz operatora IN, które wyglądałyby mniej więcej tak:

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

Wyobraź sobie coś tak prostego, jak tabela produktów i kategorii, do której normalnie możesz DOŁĄCZYĆ razem, aby uzyskać produkty z określonej kategorii . W powyższym przykładzie widać, że pod daną kategorią w Redis ( category:4:product_ids) zwracam wszystkie identyfikatory produktów z kategorii o id 4 i umieszczam je w powyższym SELECTzapytaniu wewnątrz INoperatora.

Jak wydajne jest to?

Czy jest to sytuacja typu „to zależy”? A może jest konkretne „to jest (nie) akceptowane”, „szybko” lub „wolno”, czy powinienem dodać LIMIT 25, czy to nie pomaga?

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

A może powinienem przyciąć tablicę identyfikatorów produktów zwracanych przez Redis, aby ograniczyć ją do 25 i dodać do zapytania tylko 25, a nie 3000, a LIMITdo 25 z wewnątrz zapytania?

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

Wszelkie sugestie / opinie są bardzo mile widziane!

Michael van Rooijen
źródło
Nie jestem pewien, o co pytasz? Jedno zapytanie z „id IN (1, 2, 3, ... 3000))” jest szybsze niż 3000 zapytań z „id = wartość”. Ale połączenie z „category = 4” będzie szybsze niż oba powyższe.
Ronnis,
Zgadza się, chociaż ponieważ produkt może należeć do wielu kategorii, nie możesz użyć opcji „category = 4”. Korzystając z Redis, zapisywałbym wszystkie identyfikatory produktów, które należą do określonych kategorii, a następnie przeszukiwałbym je. Myślę, że prawdziwe pytanie brzmi, jak wypadłaby id IN (1,2,3 ... 3000)wydajność w porównaniu z tabelą JOIN products_categories. A może to właśnie mówiłeś?
Michael van Rooijen
Tylko uważaj na ten błąd w MySql stackoverflow.com/questions/3417074/ ...
Itay Moav -Malimovka
Oczywiście nie ma powodu, dla którego nie miałoby to być tak wydajne, jak każda inna metoda pobierania indeksowanych wierszy; zależy to tylko od tego, czy autorzy bazy danych przetestowali ją i zoptymalizowali. Jeśli chodzi o złożoność obliczeniową, w najgorszym przypadku wykonamy sortowanie O (n log N) na INklauzuli (może to być nawet liniowe na posortowanej liście, jak pokazujesz, w zależności od algorytmu), a następnie liniowe przecięcie / wyszukiwanie .
jberryman

Odpowiedzi:

39

Ogólnie rzecz biorąc, jeśli INlista stanie się zbyt duża (dla jakiejś źle zdefiniowanej wartości `` zbyt dużej '', która zwykle jest w zakresie 100 lub mniejszym), bardziej efektywne staje się użycie złączenia, tworząc tymczasową tabelę, jeśli zajdzie taka potrzeba. trzymać numery.

Jeśli liczby są gęstym zbiorem (bez luk - co sugerują przykładowe dane), możesz zrobić jeszcze lepiej WHERE id BETWEEN 300 AND 3000.

Jednak przypuszczalnie są luki w zbiorze, w którym to momencie może być lepiej pójść z listą prawidłowych wartości (chyba, że ​​luk jest stosunkowo niewiele, w takim przypadku możesz użyć:

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

Lub jakiekolwiek są luki.

Jonathan Leffler
źródło
46
Czy możesz podać przykład „użyj łączenia, tworząc tymczasową tabelę”?
Jake
jeśli zestaw danych pochodzi z interfejsu (element wielokrotnego wyboru) i są luki w wybranych danych i te luki nie są luką sekwencyjną (brak: 457, 490, 658, ...) to AND id NOT BETWEEN XXX AND XXXnie zadziała i lepiej trzymaj się odpowiednika, (x = 1 OR x = 2 OR x = 3 ... OR x = 99)jak napisał @David Fells.
deepcell
z mojego doświadczenia - pracując na witrynach e-commerce, musimy pokazywać wyniki wyszukiwania ~ 50 niepowiązanych identyfikatorów produktów, lepsze wyniki uzyskaliśmy z „1. ​​50 oddzielnymi zapytaniami”, vs „2. jedno zapytanie z wieloma wartościami w polu„ IN klauzula"". Nie mam w tej chwili żadnego sposobu, aby to udowodnić, poza tym, że zapytanie nr 2 będzie zawsze pojawiać się jako wolne zapytanie w naszych systemach monitorowania, podczas gdy nr 1 nigdy się nie pojawi, niezależnie od ilości wykonań miliony ... czy ktoś ma takie same doświadczenia? (może możemy to odnieść do lepszego buforowania lub pozwalania innym zapytaniom na przeplatanie się między zapytaniami ...)
Chaim Klar,
24

Robiłem kilka testów i jak mówi David Fells w swojej odpowiedzi , jest to dość dobrze zoptymalizowane. Dla porównania, utworzyłem tabelę InnoDB z 1 000 000 rejestrów i dokonując wyboru za pomocą operatora „IN” z 500 000 liczb losowych, zajmuje to tylko 2,5 sekundy na moim MAC; wybranie tylko rejestrów parzystych zajmuje 0,5 sekundy.

Jedyny problem jaki miałem to to, że musiałem zwiększyć max_allowed_packetparametr z my.cnfpliku. Jeśli nie, generowany jest tajemniczy błąd „MYSQL zniknął”.

Oto kod PHP, którego używam do wykonania testu:

$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>";

A wyniki:

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
źródło
Dla dobra innych dodam, że działając w VirtualBox (CentOS) na moim MBP z końca 2013 roku z i7, trzecia linia (ta, która dotyczy pytania) wyjścia brzmiała: Losowy wybór = 500744 Czas wykonania = 53.458173036575s .. 53 sekundy mogą być tolerowane w zależności od aplikacji. Do moich celów, raczej nie. Zwróć też uwagę, że test na liczby parzyste nie jest odpowiedni dla danego pytania, ponieważ używa operatora modulo ( %) z operatorem równości ( =) zamiast IN().
rinogo
Jest to istotne, ponieważ umożliwia porównanie zapytania z operatorem IN z podobnym zapytaniem bez tej funkcji. Może to być dłuższy czas, ponieważ jest to czas pobierania, ponieważ twoja maszyna wymienia się lub pracuje na innej maszynie wirtualnej.
jbaylina
14

Możesz utworzyć tymczasową tabelę, w której możesz umieścić dowolną liczbę identyfikatorów i uruchomić zagnieżdżone zapytanie Przykład:

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

i wybierz:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Vladimir Jotov
źródło
6
lepiej dołączyć do stołu tymczasowego zamiast korzystać z podzapytania
scharette
3
@loopkin Czy możesz wyjaśnić, jak byś to zrobił, używając dołączenia, a nie podzapytania?
Jeff Solomon,
3
@jeffSolomon SELECT products.id, name, price FROM products DOŁĄCZ tmp_IDs on products.id = tmp_IDs.ID;
scharette
TA ODPOWIEDŹ! jest tym, czego szukałem, bardzo, bardzo szybko przy długich rejestrach
Damián Rafael Lattenero
Dziękuję bardzo, stary. Po prostu działa niesamowicie szybko.
mrHalfer
4

Używanie INz dużym parametrem ustawionym na dużej liście rekordów będzie w rzeczywistości powolne.

W przypadku, który ostatnio rozwiązałem, miałem dwie klauzule where, jedną z 2,50 parametrami, a drugą z 3500 parametrami, odpytując tabelę zawierającą 40 milionów rekordów.

Moje zapytanie zajęło 5 minut przy użyciu standardu WHERE IN. Używając zamiast tego podzapytania dla instrukcji IN (umieszczając parametry we własnej indeksowanej tabeli), sprowadziłem zapytanie do DWÓCH sekund.

Z mojego doświadczenia wynika, że ​​pracowałem zarówno dla MySQL, jak i Oracle.

yoyodunno
źródło
1
Nie zrozumiałem twojego punktu w „Zamiast tego, używając podzapytania dla instrukcji IN (umieszczając parametry we własnej indeksowanej tabeli)”. Czy chodziło Ci o to, że zamiast „WHERE ID IN (1,2,3)” powinniśmy użyć „WHERE ID IN (SELECT id FROM xxx)”?
Istiyak Tailor
4

INjest w porządku i dobrze zoptymalizowany. Upewnij się, że używasz go na indeksowanym polu i wszystko w porządku.

Jest funkcjonalnie równoważne z:

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

Jeśli chodzi o silnik DB.

David Fells
źródło
1
Nie specjalnie. Używam IN clouse do pobrania 5k rekordów z DB. W clouse zawiera listę PK, więc powiązana kolumna jest indeksowana i gwarantuje, że jest unikalna. EXPLAIN mówi, że pełne skanowanie tabeli jest wykonywane zaraz po użyciu wyszukiwania PK w stylu „na równi z kolejką fifo”.
Antoniossss
W MySQL nie wierzę, że są „funkcjonalnie równoważne” . INużywa optymalizacji w celu uzyskania lepszej wydajności.
Joshua Pinter
1
Josh, odpowiedź brzmiała z 2011 roku - jestem pewien, że od tamtego czasu wszystko się zmieniło, ale w tamtych czasach IN zostało zamienione na serię stwierdzeń OR.
David Fells
1
Ta odpowiedź jest nieprawidłowa. Z MySQL o wysokiej wydajności : Inaczej jest w MySQL, który sortuje wartości na liście IN () i używa szybkiego wyszukiwania binarnego, aby sprawdzić, czy wartość znajduje się na liście. To jest O (log n) w rozmiarze listy, podczas gdy równoważna seria klauzul OR to O (n) w rozmiarze listy (tj. Znacznie wolniej w przypadku dużych list).
Bert
Bert - tak. Ta odpowiedź jest przestarzała. Zapraszam do zaproponowania zmiany.
David Fells
-2

Jeśli INoperator podaje wiele wartości , musi najpierw je posortować, aby usunąć duplikaty. Przynajmniej to podejrzewam. Nie byłoby więc dobrze podawać zbyt wielu wartości, ponieważ sortowanie zajmuje N log N.

Z mojego doświadczenia wynika, że ​​najlepszą wydajność daje pocięcie zbioru wartości na mniejsze podzbiory i połączenie wyników wszystkich zapytań w aplikacji. Przyznaję, że zbierałem doświadczenie na innej bazie danych (Pervasive), ale to samo może dotyczyć wszystkich silników. Moja liczba wartości w zestawie wynosiła 500-1000. Mniej więcej było znacznie wolniej.

Jarekczek
źródło
Wiem, że minęło 7 lat, ale problem z tą odpowiedzią polega po prostu na tym, że jest to komentarz oparty na świadomym przypuszczeniu.
Giacomo1968