Leistung des MySQL-IN-Operators bei (großer?) Anzahl von Werten

91

Ich habe in letzter Zeit mit Redis und MongoDB experimentiert und es scheint, dass es häufig Fälle gibt, in denen Sie eine Reihe von IDs entweder in MongoDB oder in Redis speichern . Ich bleibe bei Redis für diese Frage, da ich nach dem MySQL IN- Operator frage .

Ich frage mich , wie performant ist es eine große Anzahl (300-3000) von zur Liste ids innerhalb des IN - Operator, die etwa wie folgt aussehen:

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

Stellen Sie sich etwas so Einfaches wie eine Produkt- und Kategorietabelle vor , die Sie normalerweise zusammenfügen, um die Produkte aus einer bestimmten Kategorie zu erhalten . Im obigen Beispiel können Sie sehen, dass category:4:product_idsich unter einer bestimmten Kategorie in Redis ( ) alle Produkt-IDs aus der Kategorie mit der ID 4 zurückgebe und sie in der obigen SELECTAbfrage innerhalb des INOperators platziere.

Wie performant ist das?

Ist das eine "es kommt darauf an" Situation? Oder gibt es ein konkretes "das ist (un) akzeptabel" oder "schnell" oder "langsam" oder sollte ich ein hinzufügen LIMIT 25, oder hilft das nicht?

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

Oder sollte ich das Array der von Redis zurückgegebenen Produkt-IDs kürzen, um es auf 25 zu beschränken, und nur 25 IDs zur Abfrage hinzufügen, anstatt 3000, und LIMITes innerhalb der Abfrage auf 25 hinzufügen ?

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

Anregungen / Feedback wird sehr geschätzt!

Michael van Rooijen
quelle
Ich bin mir nicht sicher, was Sie genau fragen? Eine Abfrage mit "id IN (1,2,3, ... 3000))" ist schneller als 3000 Abfragen mit "id = value". Ein Join mit "category = 4" ist jedoch schneller als beide oben genannten.
Ronnis
Richtig, da ein Produkt zu mehreren Kategorien gehören kann, können Sie die "Kategorie = 4" nicht ausführen. Mit Redis würde ich alle IDs der Produkte speichern, die zu einer bestimmten Kategorie gehören, und diese dann abfragen. Ich denke die eigentliche Frage ist, wie würde die id IN (1,2,3 ... 3000)Leistung im Vergleich zur JOIN-Tabelle von products_categories. Oder haben Sie das gesagt?
Michael van Rooijen
Seien Sie
Itay Moav -Malimovka
Natürlich gibt es keinen Grund, warum dies nicht so effizient sein sollte wie jede andere Methode zum Abrufen indizierter Zeilen. es kommt nur darauf an, ob Datenbankautoren es getestet und optimiert haben. In Bezug auf die Rechenkomplexität werden wir im schlimmsten Fall eine O (n log N) -Sortierung für die INKlausel durchführen (dies kann in einer sortierten Liste, wie Sie zeigen, je nach Algorithmus sogar linear sein) und dann eine lineare Schnittmenge / Suche .
Jberryman

Antworten:

38

Wenn die INListe zu groß wird (für einen schlecht definierten Wert von "zu groß", der normalerweise im Bereich von 100 oder kleiner liegt), wird es im Allgemeinen effizienter, einen Join zu verwenden und bei Bedarf eine temporäre Tabelle zu erstellen die Zahlen zu halten.

Wenn die Zahlen eine dichte Menge sind (keine Lücken - was die Beispieldaten nahe legen), können Sie es noch besser machen WHERE id BETWEEN 300 AND 3000.

Vermutlich gibt es jedoch Lücken in der Menge. An diesem Punkt ist es möglicherweise besser, mit der Liste der gültigen Werte zu beginnen (es sei denn, die Anzahl der Lücken ist relativ gering. In diesem Fall könnten Sie Folgendes verwenden:

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

Oder was auch immer die Lücken sind.

Jonathan Leffler
quelle
45
Können Sie bitte ein Beispiel für "Verwenden eines Joins, Erstellen einer temporären Tabelle" geben?
Jake
Wenn der Datensatz von einer Schnittstelle (Mehrfachauswahlelement) stammt und Lücken in den ausgewählten Daten vorhanden sind und diese Lücken keine sequentielle Lücke sind (fehlt: 457, 490, 658, ..) AND id NOT BETWEEN XXX AND XXX, funktioniert dies nicht und es ist besser, dies zu tun (x = 1 OR x = 2 OR x = 3 ... OR x = 99)bleib bei dem Äquivalent, wie @David Fells schrieb.
Deepcell
Nach meiner Erfahrung - bei der Arbeit an E-Commerce-Websites müssen wir Suchergebnisse von ~ 50 nicht verwandten Produkt-IDs anzeigen. Wir hatten bessere Ergebnisse mit "1. 50 separate Abfragen" gegenüber "2. einer Abfrage mit vielen Werten im" IN " Klausel"". Ich habe im Moment keine Möglichkeit, dies zu beweisen, außer dass die Abfrage Nr. 2 in unseren Überwachungssystemen immer als langsame Abfrage angezeigt wird, während die Abfrage Nr. 1 niemals angezeigt wird, unabhängig davon, wie viele Ausführungen ausgeführt werden die Millionen ... hat jemand die gleiche Erfahrung? (Wir können es vielleicht auf ein besseres Caching beziehen oder zulassen, dass andere Abfragen zwischen Abfragen verschachtelt werden ...)
Chaim Klar
23

Ich habe einige Tests durchgeführt, und wie David Fells in seiner Antwort sagt , ist es ziemlich gut optimiert. Als Referenz habe ich eine InnoDB-Tabelle mit 1.000.000 Registern erstellt und mit dem Operator "IN" mit 500.000 Zufallszahlen eine Auswahl getroffen. Auf meinem MAC dauert dies nur 2,5 Sekunden. Die Auswahl nur der geraden Register dauert 0,5 Sekunden.

Das einzige Problem, das ich hatte, war, dass ich den max_allowed_packetParameter aus der my.cnfDatei erhöhen musste . Wenn nicht, wird ein mysteriöser Fehler "MYSQL ist verschwunden" generiert.

Hier ist der PHP-Code, mit dem ich den Test mache:

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

Und die Ergebnisse:

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
quelle
Für andere möchte ich hinzufügen, dass in VirtualBox (CentOS) auf meinem MBP Ende 2013 mit einem i7 die dritte Zeile (die für die Frage relevante) der Ausgabe lautete: Zufällige Auswahl = 500744 Zeitausführungszeit = 53.458173036575s .. 53 Sekunden können je nach Anwendung tolerierbar sein. Für meine Zwecke nicht wirklich. Beachten Sie auch, dass der Test für gerade Zahlen für die vorliegende Frage nicht relevant ist, da er den Modulo-Operator ( %) mit einem Gleichheitsoperator ( =) anstelle von verwendet IN().
Rinogo
Dies ist relevant, da auf diese Weise eine Abfrage mit dem IN-Operator mit einer ähnlichen Abfrage ohne diese Funktionalität verglichen werden kann. Möglicherweise ist die Zeit, die Sie erhalten, höher, weil es sich um eine Downloadzeit handelt, weil Ihre Maschine austauscht oder in einer anderen virtuellen Maschine arbeitet.
Jbaylina
13

Sie können eine temporäre Tabelle erstellen, in die Sie eine beliebige Anzahl von IDs eingeben und eine verschachtelte Abfrage ausführen können. Beispiel:

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

und wählen Sie:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Vladimir Jotov
quelle
6
Es ist besser, sich Ihrer temporären Tabelle anzuschließen, anstatt eine Unterabfrage zu verwenden
scharette
3
@loopkin Kannst du bitte erklären, wie du das mit einem Join gegen eine Unterabfrage machen würdest?
Jeff Solomon
3
@jeffSolomon SELECT products.id, Name, Preis FROM products JOIN tmp_IDs on products.id = tmp_IDs.ID;
scharette
DIESE ANTWORT! ist das, wonach ich gesucht habe, sehr, sehr schnell für lange Registrierungen
Damián Rafael Lattenero
4

INist in Ordnung und gut optimiert. Stellen Sie sicher, dass Sie es in einem indizierten Feld verwenden und es Ihnen gut geht.

Es ist funktional äquivalent zu:

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

Was den DB-Motor betrifft.

David Fells
quelle
1
Nicht wirklich. Ich benutze IN clouse, um 5k Datensätze aus der DB abzurufen. IN clouse enthält eine Liste von PKs, sodass die zugehörige Spalte indiziert ist und garantiert eindeutig ist. EXPLAIN sagt, dass ein vollständiger Tabellenscan durchgeführt wird, um die PK-Suche im "Fifo-Queue-Alike" -Stil zu verwenden.
Antoniossss
Unter MySQL glaube ich nicht, dass sie "funktional gleichwertig" sind . INverwendet Optimierungen für eine bessere Leistung.
Joshua Pinter
1
Josh, die Antwort war von 2011 - ich bin sicher, dass sich die Dinge seitdem geändert haben, aber damals wurde IN mit Hochdruck in eine Reihe von OP-Anweisungen umgewandelt.
David Fells
1
Diese Antwort ist nicht korrekt. Von High Performance MySQL : Nicht so in MySQL, das die Werte in der IN () -Liste sortiert und eine schnelle binäre Suche verwendet, um festzustellen, ob ein Wert in der Liste enthalten ist. Dies ist O (log n) in der Größe der Liste, während eine äquivalente Reihe von OR-Klauseln O (n) in der Größe der Liste ist (dh bei großen Listen viel langsamer).
Bert
Bert - ja. Diese Antwort ist veraltet. Fühlen Sie sich frei, eine Bearbeitung vorzuschlagen.
David Fells
3

Die Verwendung INmit einem großen Parametersatz für eine große Liste von Datensätzen ist in der Tat langsam.

In dem Fall, den ich kürzlich gelöst habe, hatte ich zwei where-Klauseln, eine mit 2,50 Parametern und die andere mit 3.500 Parametern, die eine Tabelle mit 40 Millionen Datensätzen abfragten.

Meine Anfrage dauerte 5 Minuten mit dem Standard WHERE IN. Indem ich stattdessen eine Unterabfrage für die IN- Anweisung verwendete (die Parameter in eine eigene indizierte Tabelle einfügte ), wurde die Abfrage auf ZWEI Sekunden reduziert.

Arbeitete meiner Erfahrung nach sowohl für MySQL als auch für Oracle.

yoyodunno
quelle
Ich habe Ihren Standpunkt zu "Indem Sie stattdessen eine Unterabfrage für die IN-Anweisung verwenden (die Parameter in ihre eigene indizierte Tabelle einfügen)" nicht verstanden. Meinten Sie, dass wir anstelle von "WHERE ID IN (1,2,3)" "WHERE ID IN (SELECT id FROM xxx)" verwenden sollten?
Istiyak Tailor vor
-1

Wenn Sie viele Werte für den INOperator angeben, muss dieser zuerst sortiert werden, um Duplikate zu entfernen. Zumindest vermute ich das. Es wäre also nicht gut, zu viele Werte anzugeben, da das Sortieren N log N Zeit benötigt.

Meine Erfahrung hat gezeigt, dass das Aufteilen der Wertemenge in kleinere Teilmengen und das Kombinieren der Ergebnisse aller Abfragen in der Anwendung die beste Leistung ergibt. Ich gebe zu, dass ich Erfahrungen in einer anderen Datenbank (Pervasive) gesammelt habe, aber das Gleiche gilt möglicherweise für alle Engines. Meine Anzahl von Werten pro Satz war 500-1000. Mehr oder weniger war deutlich langsamer.

Jarekczek
quelle
Ich weiß, dass dies 7 Jahre später ist, aber das Problem mit dieser Antwort ist einfach, dass es sich um einen Kommentar handelt, der auf einer fundierten Vermutung basiert.
Giacomo1968