Performances de l'opérateur MySQL «IN» sur un (grand?) Nombre de valeurs

93

J'ai récemment expérimenté Redis et MongoDB et il semblerait qu'il y ait souvent des cas où vous stockeriez un tableau d' identifiants dans MongoDB ou Redis. Je vais m'en tenir à Redis pour cette question puisque je pose la question sur l' opérateur MySQL IN .

Je me demandais à quel point il était performant de lister un grand nombre (300-3000) d' identifiants dans l'opérateur IN, ce qui ressemblerait à ceci:

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

Imaginez quelque chose d'aussi simple qu'un tableau de produits et de catégories que vous pourriez normalement JOINDRE ensemble pour obtenir les produits d'une certaine catégorie . Dans l'exemple ci-dessus, vous pouvez voir que sous une catégorie donnée dans Redis ( category:4:product_ids), je renvoie tous les identifiants de produit de la catégorie avec l'ID 4 et les place dans la SELECTrequête ci-dessus à l'intérieur de l' INopérateur.

À quel point est-ce performant?

Est-ce une situation «ça dépend»? Ou y a-t-il un concret "ceci est (in) acceptable" ou "rapide" ou "lent" ou devrais-je ajouter un LIMIT 25, ou cela n'aide-t-il pas?

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

Ou devrais-je réduire le tableau des identifiants de produit renvoyés par Redis pour le limiter à 25 et ajouter seulement 25 identifiants à la requête plutôt que 3000 et le faire LIMITpasser à 25 à l'intérieur de la requête?

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

Toutes les suggestions / commentaires sont très appréciés!

Michael van Rooijen
la source
Je ne sais pas exactement ce que vous demandez? Une requête avec "id IN (1,2,3, ... 3000))" est plus rapide que 3000 requêtes avec "id = valeur". Mais une jointure avec "category = 4" sera plus rapide que les deux.
Ronnis
Bien, mais comme un produit peut appartenir à plusieurs catégories, vous ne pouvez pas faire la "catégorie = 4". En utilisant Redis, je stockais tous les identifiants des produits appartenant à certaines catégories, puis je ferais des requêtes à ce sujet. Je suppose que la vraie question est de savoir comment la id IN (1,2,3 ... 3000)performance serait-elle comparée à la table JOIN de products_categories. Ou est-ce ce que vous disiez?
Michael van Rooijen
Faites
Bien sûr, il n'y a aucune raison pour que cela ne soit pas aussi efficace que toute autre méthode de récupération des lignes indexées; cela dépend simplement du fait que les auteurs de bases de données ont testé et optimisé pour cela. En termes de complexité de calcul, nous allons faire au pire un tri O (n log N) sur la INclause (cela pourrait même être linéaire sur une liste triée comme vous le montrez, en fonction de l'algorithme), puis intersection / recherches linéaires .
jberryman

Réponses:

39

De manière générale, si la INliste devient trop grande (pour une valeur mal définie de `` trop grande '' qui est généralement de l'ordre de 100 ou moins), il devient plus efficace d'utiliser une jointure, créant une table temporaire si besoin est pour tenir les chiffres.

Si les nombres sont un ensemble dense (pas de lacunes - ce que les données d'exemple suggèrent), vous pouvez faire encore mieux avec WHERE id BETWEEN 300 AND 3000 .

Cependant, il y a probablement des lacunes dans l'ensemble, auquel cas il peut être préférable de suivre la liste des valeurs valides après tout (à moins que les lacunes ne soient relativement peu nombreuses, auquel cas vous pouvez utiliser:

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

Ou quelles que soient les lacunes.

Jonathan Leffler
la source
46
Pouvez-vous s'il vous plaît donner un exemple de «utiliser une jointure, créer une table temporaire»?
Jake
si l'ensemble de données provenait d'une interface (élément à sélection multiple) et qu'il y a des lacunes dans les données sélectionnées et que ces lacunes ne sont pas une lacune séquentielle (manquante: 457, 490, 658, ..) alors AND id NOT BETWEEN XXX AND XXXne fonctionnera pas et il vaut mieux s'en tenir à l'équivalent (x = 1 OR x = 2 OR x = 3 ... OR x = 99)comme l'a écrit @David Fells.
deepcell
d'après mon expérience - en travaillant sur des sites Web de commerce électronique, nous devons afficher des résultats de recherche d'environ 50 ID de produit non liés, nous avons obtenu de meilleurs résultats avec "1. 50 requêtes séparées", contre "2. une requête avec de nombreuses valeurs dans le" IN clause"". Je n'ai aucun moyen de le prouver pour le moment, sauf que la requête n ° 2 apparaîtra toujours comme une requête lente dans nos systèmes de surveillance, alors que la requête n ° 1 n'apparaîtra jamais, quel que soit le montant des exécutions. les millions ... est-ce que quelqu'un a la même expérience? (nous pouvons peut-être l'associer à une meilleure mise en cache, ou à permettre à d'autres requêtes de s'entrelacer entre les requêtes ...)
Chaim Klar
24

J'ai fait quelques tests, et comme David Fells le dit dans sa réponse , c'est assez bien optimisé. Pour référence, j'ai créé une table InnoDB avec 1 000 000 registres et en faisant une sélection avec l'opérateur "IN" avec 500 000 nombres aléatoires, cela ne prend que 2,5 secondes sur mon MAC; sélectionner uniquement les registres pairs prend 0,5 seconde.

Le seul problème que j'ai eu est que j'ai dû augmenter le max_allowed_packetparamètre du my.cnffichier. Sinon, une mystérieuse erreur «MYSQL est parti» est générée.

Voici le code PHP que j'utilise pour faire le test:

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

Et les résultats:

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
la source
Pour le bien des autres, j'ajouterai que s'exécutant dans VirtualBox (CentOS) sur mon MBP de fin 2013 avec un i7, la troisième ligne (celle qui concerne la question) de la sortie était: Sélection aléatoire = 500744 Temps d'exécution = 53,458173036575s .. 53 secondes peuvent être tolérées selon votre application. Pour mes utilisations, pas vraiment. Notez également que le test des nombres pairs n'est pas pertinent pour la question en question car il utilise l'opérateur modulo ( %) avec un opérateur égal ( =) au lieu de IN().
rinogo
C'est pertinent car c'est un moyen de comparer une requête avec l'opérateur IN avec une requête similaire sans cette fonctionnalité. Peut-être que le temps le plus long que vous obtenez est parce que c'est un temps de téléchargement, parce que votre machine est en train de swapipng ou de travailler dans une autre machine virtuelle.
jbaylina
14

Vous pouvez créer une table temporaire dans laquelle vous pouvez placer n'importe quel nombre d'ID et exécuter une requête imbriquée. Exemple:

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

et sélectionnez:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Vladimir Jotov
la source
6
il vaut mieux rejoindre votre table temporaire au lieu d'utiliser une sous
scharette
3
@loopkin pouvez-vous expliquer comment vous feriez cela avec une jointure par rapport à une sous-requête s'il vous plaît?
Jeff Solomon
3
@jeffSolomon SELECT products.id, name, price FROM products JOIN tmp_IDs on products.id = tmp_IDs.ID;
scharette
CETTE RÉPONSE! est ce que je cherchais, très très rapide pour les longs registres
Damián Rafael Lattenero
Merci beaucoup, mec. Cela fonctionne incroyablement vite.
mrHalfer le
4

L'utilisation INavec un grand jeu de paramètres sur une grande liste d'enregistrements sera en fait lente.

Dans le cas que j'ai résolu récemment, j'avais deux clauses where, l'une avec 2,50 paramètres et l'autre avec 3 500 paramètres, interrogeant une table de 40 millions d'enregistrements.

Ma requête a pris 5 minutes en utilisant la norme WHERE IN. En utilisant à la place une sous-requête pour IN instruction (en plaçant les paramètres dans leur propre table indexée), j'ai réduit la requête à DEUX secondes.

J'ai travaillé pour MySQL et Oracle dans mon expérience.

yoyodunno
la source
1
Je n'ai pas compris votre point de vue "En utilisant à la place une sous-requête pour l'instruction IN (en mettant les paramètres dans leur propre table indexée)". Vouliez-vous dire qu'au lieu d'utiliser "WHERE ID IN (1,2,3)", nous devrions utiliser "WHERE ID IN (SELECT id FROM xxx)"?
Istiyak Tailor
4

INc'est bien, et bien optimisé. Assurez-vous de l'utiliser sur un champ indexé et tout va bien.

C'est fonctionnellement équivalent à:

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

En ce qui concerne le moteur DB.

David Fells
la source
1
Pas vraiment. J'utilise IN clouse pour récupérer 5k enregistrements de la base de données. IN clouse contient la liste des PK, donc la colonne associée est indexée et garantie d'être unique. EXPLAIN indique que l'analyse complète de la table est effectuée au moment de l'utilisation de la recherche PK dans le style "fifo-queue-alike".
Antoniossss
Sur MySQL, je ne pense pas qu'ils soient "fonctionnellement équivalents" . INutilise des optimisations pour de meilleures performances.
Joshua Pinter
1
Josh, la réponse était de 2011 - je suis sûr que les choses ont changé depuis, mais à l'époque, IN était carrément converti en une série d'instructions OR.
David Fells
1
Cette réponse n'est pas correcte. De MySQL haute performance : pas le cas dans MySQL, qui trie les valeurs dans la liste IN () et utilise une recherche binaire rapide pour voir si une valeur est dans la liste. C'est O (log n) dans la taille de la liste, alors qu'une série équivalente de clauses OR est O (n) dans la taille de la liste (c'est-à-dire beaucoup plus lent pour les grandes listes).
Bert
Bert - oui. Cette réponse est obsolète. N'hésitez pas à suggérer une modification.
David Fells
-2

Lorsque vous fournissez de nombreuses valeurs pour IN opérateur, il doit d'abord les trier pour supprimer les doublons. Au moins je soupçonne cela. Il ne serait donc pas bon de fournir trop de valeurs, car le tri prend N log N temps.

Mon expérience a prouvé que le découpage de l'ensemble de valeurs en sous-ensembles plus petits et la combinaison des résultats de toutes les requêtes dans l'application donnent les meilleures performances. J'avoue avoir acquis de l'expérience sur une base de données différente (Pervasive), mais la même chose peut s'appliquer à tous les moteurs. Mon nombre de valeurs par ensemble était de 500 à 1000. Plus ou moins était significativement plus lent.

Jarekczek
la source
Je sais que c'est 7 ans plus tard, mais le problème avec cette réponse est simplement que c'est un commentaire basé sur une supposition éclairée.
Giacomo1968