web-dev-qa-db-fra.com

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

J'ai expérimenté avec Redis et MongoDB récemment et il semblerait qu'il y ait souvent des cas où vous stockeriez un tableau de id's dans MongoDB ou Redis. Je vais rester avec Redis pour cette question car je pose des questions sur l'opérateur MySQL DANS .

Je me demandais à quel point il est performant de répertorier un grand nombre (300-3000) de id's à l'intérieur de l'opérateur IN, qui ressemblerait à quelque chose comme ceci:

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

Imaginez quelque chose d'aussi simple qu'une table produits et 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 ID de produit de la catégorie avec l'ID 4 et les place dans la requête SELECT ci-dessus à l'intérieur de l'opérateur IN.

Comment est-ce performant?

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

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

Ou dois-je couper le tableau des identifiants de produit retournés par Redis pour le limiter à 25 et n'ajouter que 25 identifiants à la requête plutôt que 3000 et LIMIT- à 25 de 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!

76
Michael van Rooijen

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

Si les nombres sont un ensemble dense (sans lacunes - ce que les données de l'échantillon 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 (sauf si les lacunes sont 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.

33

J'ai fait quelques tests, et comme David Fells le dit dans sa réponse , il est assez bien optimisé. Comme référence, j'ai créé une table InnoDB avec 1 000 000 de 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, c'est que j'ai dû augmenter le max_allowed_packet paramètre du my.cnf fichier. Sinon, une mystérieuse erreur "MYSQL a disparu" 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
20
jbaylina

Vous pouvez créer une table temporaire dans laquelle vous pouvez mettre 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);
11
Vladimir Jotov

IN est très bien et bien optimisé. Assurez-vous de l'utiliser sur un champ indexé et tout va bien.

Il est fonctionnellement équivalent à:

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

En ce qui concerne le moteur DB.

4
David Fells

L'utilisation de IN avec un grand paramètre défini 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 plutôt une sous-requête pour l'instruction DANS (en mettant les paramètres dans leur propre table indexée), j'ai réduit la requête à DEUX secondes.

A travaillé pour MySQL et Oracle dans mon expérience.

2
yoyodunno