Rendimiento del operador MySQL "IN" en (¿gran número?) De valores

93

He estado experimentando con Redis y MongoDB últimamente y parece que a menudo hay casos en los que almacenaría una matriz de ID en MongoDB o Redis. Me quedaré con Redis para esta pregunta, ya que estoy preguntando sobre el operador MySQL IN .

Me preguntaba qué tan eficiente es enumerar una gran cantidad (300-3000) de identificadores dentro del operador IN, que se vería así:

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

Imagine algo tan simple como una tabla de productos y categorías a la que normalmente podría UNIRSE para obtener los productos de una categoría determinada . En el ejemplo anterior, puede ver que en una categoría determinada en Redis ( category:4:product_ids) devuelvo todos los identificadores de producto de la categoría con el identificador 4 y los coloco en la SELECTconsulta anterior dentro del INoperador.

¿Qué rendimiento tiene esto?

¿Es esta una situación de "depende"? ¿O hay un concreto "esto es (in) aceptable" o "rápido" o "lento" o debería agregar un LIMIT 25, o eso no ayuda?

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

¿O debería recortar la matriz de identificadores de productos devueltos por Redis para limitarlos a 25 y solo agregar 25 identificadores a la consulta en lugar de 3000 y LIMITponerlos a 25 desde dentro de la consulta?

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

¡Cualquier sugerencia / comentario es muy apreciado!

Michael van Rooijen
fuente
¿No estoy seguro exactamente de lo que estás preguntando? Una consulta con "id IN (1,2,3, ... 3000))" es más rápida que 3000 consultas con "id = value". Pero una combinación con "categoría = 4" será más rápida que las dos anteriores.
Ronnis
Correcto, aunque dado que un producto puede pertenecer a varias categorías, no puede hacer la "categoría = 4". Usando Redis, almacenaría todas las identificaciones de los productos que pertenecen a ciertas categorías y luego consultaría sobre eso. Supongo que la verdadera pregunta es, ¿cómo sería el id IN (1,2,3 ... 3000)rendimiento en comparación con la tabla JOIN de products_categories. ¿O es eso lo que estabas diciendo?
Michael van Rooijen
Solo tenga cuidado con ese error en MySql stackoverflow.com/questions/3417074/…
Itay Moav -Malimovka
Por supuesto, no hay ninguna razón por la que esto no deba ser tan eficiente como cualquier otro método para recuperar filas indexadas; solo depende de si los autores de la base de datos la han probado y optimizado. En términos de complejidad computacional, en el peor de los casos, haremos una ordenación O (n log N) en la INcláusula (esto podría incluso ser lineal en una lista ordenada como la que muestra, según el algoritmo), y luego intersección lineal / búsquedas .
jberryman

Respuestas:

39

En términos generales, si la INlista se vuelve demasiado grande (para algún valor mal definido de 'demasiado grande' que generalmente está en la región de 100 o menos), se vuelve más eficiente usar una combinación, creando una tabla temporal si es necesario para contener los números.

Si los números son un conjunto denso (sin espacios, como sugieren los datos de la muestra), entonces puede hacerlo aún mejor WHERE id BETWEEN 300 AND 3000.

Sin embargo, es de suponer que hay lagunas en el conjunto, en cuyo caso puede ser mejor ir con la lista de valores válidos después de todo (a menos que las lagunas sean relativamente pocas, en cuyo caso podría usar:

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

O cualesquiera que sean las lagunas.

Jonathan Leffler
fuente
46
¿Puede dar un ejemplo de "usar una combinación, crear una tabla temporal"?
Jake
si el conjunto de datos proviene de una interfaz (elemento de selección múltiple) y hay brechas en los datos seleccionados y estas brechas no son una brecha secuencial (faltan: 457, 490, 658, ..) entonces AND id NOT BETWEEN XXX AND XXXno funcionará y es mejor quédate con el equivalente (x = 1 OR x = 2 OR x = 3 ... OR x = 99)como escribió @David Fells.
deepcell
En mi experiencia, al trabajar en sitios web de comercio electrónico, tenemos que mostrar resultados de búsqueda de aproximadamente 50 ID de productos no relacionados, obtuvimos mejores resultados con "1. 50 consultas independientes", en comparación con "2. una consulta con muchos valores en el cláusula"". No tengo ninguna forma de probarlo por el momento, excepto que la consulta # 2 siempre se mostrará como una consulta lenta en nuestros sistemas de monitoreo, mientras que la # 1 nunca aparecerá, independientemente de que la cantidad de ejecuciones esté en los millones ... ¿alguien tiene la misma experiencia? (tal vez podamos relacionarlo con un mejor almacenamiento en caché o permitir que otras consultas se entrelacen entre las consultas ...)
Chaim Klar
24

He estado haciendo algunas pruebas y, como dice David Fells en su respuesta , está bastante bien optimizado. Como referencia, he creado una tabla InnoDB con 1,000,000 de registros y haciendo una selección con el operador "IN" con 500,000 números aleatorios, solo toma 2.5 segundos en mi MAC; seleccionar solo los registros pares lleva 0,5 segundos.

El único problema que tuve es que tuve que aumentar el max_allowed_packetparámetro del my.cnfarchivo. De lo contrario, se genera un misterioso error "MYSQL se ha ido".

Aquí está el código PHP que utilizo para hacer la prueba:

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

Y los resultados:

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
fuente
Por el bien de los demás, agregaré que ejecutándose en VirtualBox (CentOS) en mi MBP de finales de 2013 con un i7, la tercera línea (la que es relevante para la pregunta) del resultado fue: Selección aleatoria = 500744 Tiempo de ejecución de tiempo = 53.458173036575s .. 53 segundos pueden ser tolerables dependiendo de su aplicación. Para mis usos, no realmente. Además, tenga en cuenta que la prueba para números pares no es relevante para la pregunta en cuestión, ya que utiliza el operador módulo ( %) con un operador igual ( =) en lugar de IN().
rinogo
Es relevante porque es una forma de comparar una consulta con el operador IN con una consulta similar sin esta funcionalidad. Puede ser que el tiempo más alto que obtenga es porque es un tiempo de descarga, porque su máquina está intercambiando o trabajando en otra máquina virtual.
jbaylina
14

Puede crear una tabla temporal donde puede poner cualquier número de ID y ejecutar una consulta anidada. Ejemplo:

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

y seleccione:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Vladimir Jotov
fuente
6
que es mejor unirse a su tabla temporal en lugar de utilizar una subconsulta
scharette
3
@loopkin, ¿puede explicar cómo haría esto con una combinación frente a una subconsulta, por favor?
Jeff Solomon
3
@jeffSolomon SELECCIONAR productos.id, nombre, precio DESDE productos ÚNETE a tmp_IDs en productos.id = tmp_IDs.ID;
scharette
ESTA RESPUESTA! es lo que buscaba, muy muy rápido para registros largos
Damián Rafael Lattenero
Muchisimas gracias hombre. Simplemente funciona increíblemente rápido.
mrHalfer
4

De hecho, el uso INcon un gran conjunto de parámetros en una gran lista de registros será lento.

En el caso que resolví recientemente tenía dos cláusulas where, una con 2,50 parámetros y la otra con 3,500 parámetros, consultando una tabla de 40 Millones de registros.

Mi consulta tomó 5 minutos usando el estándar WHERE IN. En su lugar, al usar una subconsulta para la declaración IN (poniendo los parámetros en su propia tabla indexada), reduje la consulta a DOS segundos.

Trabajó tanto para MySQL como para Oracle en mi experiencia.

yoyodunno
fuente
1
No entendí tu punto en "Al usar en su lugar una subconsulta para la instrucción IN (poniendo los parámetros en su propia tabla indexada)". ¿Quiso decir que en lugar de usar "WHERE ID IN (1,2,3)" deberíamos usar "WHERE ID IN (SELECT ID FROM xxx)"?
Istiyak Tailor
4

INestá bien y bien optimizado. Asegúrese de usarlo en un campo indexado y estará bien.

Es funcionalmente equivalente a:

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

En lo que respecta al motor DB.

David Fells
fuente
1
No realmente. Utilizo IN clouse para obtener registros de 5k de la base de datos. IN clouse contiene una lista de PK, por lo que la columna relacionada está indexada y se garantiza que es única. EXPLAIN dice que el escaneo completo de la tabla se realiza en lugar de usar la búsqueda de PK en el estilo "Fifo-Queue-Alike".
Antoniossss
En MySQL no creo que sean "funcionalmente equivalentes" . INutiliza optimizaciones para un mejor rendimiento.
Joshua Pinter
1
Josh, la respuesta fue de 2011: estoy seguro de que las cosas han cambiado desde entonces, pero en el pasado, IN se convirtió en una serie de declaraciones OR.
David Fells
1
Esta respuesta no es correcta. De MySQL de alto rendimiento : no es así en MySQL, que ordena los valores en la lista IN () y usa una búsqueda binaria rápida para ver si un valor está en la lista. Esto es O (log n) en el tamaño de la lista, mientras que una serie equivalente de cláusulas OR es O (n) en el tamaño de la lista (es decir, mucho más lento para listas grandes).
Bert
Bert - sí. Esta respuesta es obsoleta. No dude en sugerir una edición.
David Fells
-2

Cuando proporcione muchos valores para el INoperador, primero debe ordenarlos para eliminar los duplicados. Al menos lo sospecho. Por lo tanto, no sería bueno proporcionar demasiados valores, ya que la clasificación lleva N log N tiempo.

Mi experiencia demostró que dividir el conjunto de valores en subconjuntos más pequeños y combinar los resultados de todas las consultas en la aplicación ofrece el mejor rendimiento. Admito que obtuve experiencia en una base de datos diferente (generalizada), pero lo mismo puede aplicarse a todos los motores. Mi recuento de valores por juego fue 500-1000. Más o menos fue significativamente más lento.

Jarekczek
fuente
Sé que han pasado 7 años, pero el problema con esta respuesta es simplemente que es un comentario basado en una conjetura.
Giacomo 1968