Prestazioni dell'operatore MySQL "IN" su un numero (elevato?) Di valori


93

Ultimamente ho sperimentato Redis e MongoDB e sembrerebbe che ci siano spesso casi in cui si memorizza un array di ID in MongoDB o Redis. Continuerò con Redis per questa domanda poiché chiedo dell'operatore MySQL IN .

Mi chiedevo quanto sia efficiente elencare un numero elevato (300-3000) di ID all'interno dell'operatore IN, che sarebbe simile a questo:

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

Immagina qualcosa di semplice come una tabella di prodotti e categorie a cui potresti normalmente UNIRE insieme per ottenere i prodotti da una determinata categoria . Nell'esempio sopra puoi vedere che sotto una data categoria in Redis ( category:4:product_ids) restituisco tutti gli ID prodotto dalla categoria con id 4 e li inserisco nella SELECTquery sopra all'interno INdell'operatore.

Quanto è performante?

È una situazione "dipende"? O c'è un concreto "questo è (non) accettabile" o "veloce" o "lento" o dovrei aggiungere un LIMIT 25, o non aiuta?

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

O dovrei tagliare l'array di ID prodotto restituito da Redis per limitarlo a 25 e aggiungere solo 25 ID alla query anziché 3000 e aggiungerlo LIMITa 25 dall'interno della query?

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

Eventuali suggerimenti / feedback sono molto apprezzati!


Non sono sicuro esattamente di cosa stai chiedendo? Una query con "id IN (1,2,3, ... 3000))" è più veloce di 3000 query con "id = value". Ma un join con "category = 4" sarà più veloce di entrambi i precedenti.
Ronnis

Giusto, anche se poiché un prodotto può appartenere a più categorie I non puoi fare la "categoria = 4". Utilizzando Redis memorizzerei tutti gli ID dei prodotti che appartengono a determinate categorie e quindi interrogherei su quello. Immagino che la vera domanda sia, come si comporterebbe id IN (1,2,3 ... 3000)rispetto alla tabella JOIN di products_categories. O è quello che stavi dicendo?
Michael van Rooijen

Basta essere attenti a che il bug in MySql stackoverflow.com/questions/3417074/...~~V~~3rd
Itay Moav -Malimovka

Ovviamente non c'è motivo per cui questo non dovrebbe essere efficiente come qualsiasi altro metodo per recuperare le righe indicizzate; dipende solo dal fatto che gli autori del database lo abbiano testato e ottimizzato. In termini di complessità computazionale, nel peggiore dei casi faremo un ordinamento O (n log N) sulla INclausola (questo potrebbe anche essere lineare su un elenco ordinato come mostrato, a seconda dell'algoritmo), quindi intersezioni / ricerche lineari .
jberryman

Risposte:


39

In generale, se l' INelenco diventa troppo grande (per un valore mal definito di 'troppo grande' che di solito è nella regione di 100 o inferiore), diventa più efficiente usare un join, creando una tabella temporanea se necessario per tenere i numeri.

Se i numeri sono un insieme denso (senza spazi vuoti, come suggeriscono i dati di esempio), puoi fare ancora meglio con WHERE id BETWEEN 300 AND 3000.

Tuttavia, presumibilmente ci sono delle lacune nell'insieme, a quel punto potrebbe essere meglio andare con l'elenco dei valori validi dopotutto (a meno che gli spazi siano relativamente pochi in numero, nel qual caso potresti usare:

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

O qualunque siano le lacune.


46
Puoi fare un esempio di "usa un join, creando una tabella temporanea"?
Jake

se il set di dati proviene da un'interfaccia (elemento a selezione multipla) e ci sono dei vuoti nei dati selezionati e questi spazi non sono un gap sequenziale (mancante: 457, 490, 658, ..) allora AND id NOT BETWEEN XXX AND XXXnon funzionerà ed è meglio attenersi all'equivalente (x = 1 OR x = 2 OR x = 3 ... OR x = 99)come ha scritto @David Fells.
deepcell

nella mia esperienza - lavorando su siti di e-commerce, dobbiamo mostrare risultati di ricerca di circa 50 ID prodotto non correlati, abbiamo ottenuto risultati migliori con "1. 50 query separate", rispetto a "2. una query con molti valori in" IN clausola"". Non ho modo di dimostrarlo per il momento, tranne che la query n. 2 verrà sempre visualizzata come query lenta nei nostri sistemi di monitoraggio, mentre la n. 1 non verrà mai visualizzata, indipendentemente dal numero di esecuzioni i milioni ... qualcuno ha la stessa esperienza? (possiamo forse collegarlo a una migliore memorizzazione nella cache o consentire ad altre query di interlacciarsi tra le query ...)
Chaim Klar

24

Ho fatto alcuni test e, come dice David Fells nella sua risposta , è abbastanza ben ottimizzato. Come riferimento, ho creato una tabella InnoDB con 1.000.000 di registri e facendo una selezione con l'operatore "IN" con 500.000 numeri casuali, ci vogliono solo 2,5 secondi sul mio MAC; la selezione dei soli registri pari richiede 0,5 secondi.

L'unico problema che ho avuto è che ho dovuto aumentare il max_allowed_packetparametro dal my.cnffile. In caso contrario, viene generato un misterioso errore "MYSQL è andato via".

Ecco il codice PHP che utilizzo per fare il 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>";

E i risultati:

Insert execution time =35.2927210331s
Prepare execution time =0.0161771774292s
Random selection = 499102 Time execution time =2.40285992622s
Pairs = 500000 Exdcution time=0.465420007706s

Per il bene degli altri, aggiungerò che in esecuzione in VirtualBox (CentOS) sul mio MBP di fine 2013 con un i7, la terza riga (quella relativa alla domanda) dell'output era: Selezione casuale = 500744 Tempo di esecuzione del tempo = 53.458173036575s .. 53 secondi potrebbero essere tollerabili a seconda dell'applicazione. Per i miei usi, non proprio. Inoltre, nota che il test per i numeri pari non è rilevante per la domanda in questione poiché utilizza l'operatore modulo ( %) con un operatore uguale ( =) invece di IN().
rinogo

È rilevante perché è un modo per confrontare una query con l'operatore IN con una query simile senza questa funzionalità. Potrebbe essere il tempo più lungo che ottieni perché è un tempo di download, perché la tua macchina sta scambiando o sta lavorando in un'altra macchina virtuale.
jbaylina

14

È possibile creare una tabella temporanea in cui inserire un numero qualsiasi di ID ed eseguire query annidate Esempio:

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

e seleziona:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);

6
è meglio unirsi alla tabella temporanea invece di utilizzare una sottoquery
scharette

3
@loopkin puoi spiegare come faresti con un join e una sottoquery, per favore?
Jeff Solomon

3
@jeffSolomon SELEZIONA products.id, nome, prezzo DA prodotti JOIN tmp_IDs su products.id = tmp_IDs.ID;
scharette

QUESTA RISPOSTA! è quello che stavo cercando, molto molto veloce per lunghe registrazioni
Damián Rafael Lattenero

Grazie mille, amico. Funziona semplicemente incredibilmente veloce.
mrHalfer

4

L'utilizzo INcon un set di parametri di grandi dimensioni su un ampio elenco di record sarà infatti lento.

Nel caso che ho risolto di recente avevo due clausole where, una con 2,50 parametri e l'altra con 3.500 parametri, interrogando una tabella di 40 milioni di record.

La mia query ha richiesto 5 minuti utilizzando lo standard WHERE IN. Utilizzando invece una sottoquery per l' istruzione IN (inserendo i parametri nella propria tabella indicizzata), ho ridotto la query a DUE secondi.

Nella mia esperienza ho lavorato sia per MySQL che per Oracle.


1
Non ho capito il tuo punto "Utilizzando invece una sottoquery per l'istruzione IN (inserendo i parametri nella loro tabella indicizzata)". Intendevi che invece di usare "WHERE ID IN (1,2,3)" dovremmo usare "WHERE ID IN (SELECT id FROM xxx)"?
Istiyak Tailor

4

INva bene e ben ottimizzato. Assicurati di usarlo su un campo indicizzato e stai bene.

È funzionalmente equivalente a:

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

Per quanto riguarda il motore DB.


1
Non veramente. Uso IN clouse per recuperare 5k record dal DB. IN clouse contiene un elenco di PK in modo che la colonna correlata sia indicizzata e garantita come univoca. EXPLAIN dice che la scansione completa della tabella viene eseguita prima di utilizzare la ricerca PK in stile "fifo-queue-alike".
Antoniossss

Su MySQL non credo che siano "funzionalmente equivalenti" . INutilizza ottimizzazioni per prestazioni migliori.
Joshua Pinter

1
Josh, la risposta era del 2011 - Sono sicuro che le cose sono cambiate da allora, ma nel passato IN era completamente convertito in una serie di dichiarazioni OR.
David Fells,

1
Questa risposta non è corretta. Da MySQL ad alte prestazioni : Non così in MySQL, che ordina i valori nell'elenco IN () e utilizza una rapida ricerca binaria per vedere se un valore è nell'elenco. Questo è O (log n) nella dimensione della lista, mentre una serie equivalente di clausole OR è O (n) nella dimensione della lista (cioè, molto più lenta per liste grandi).
Bert

Bert - sì. Questa risposta è obsoleta. Sentiti libero di suggerire una modifica.
David Fells,

-2

Quando si forniscono molti valori per l' INoperatore, è necessario innanzitutto ordinarlo per rimuovere i duplicati. Almeno lo sospetto. Quindi non sarebbe opportuno fornire troppi valori, poiché l'ordinamento richiede N log N tempo.

La mia esperienza ha dimostrato che sezionando l'insieme di valori in sottoinsiemi più piccoli e combinando i risultati di tutte le query nell'applicazione si ottengono le migliori prestazioni. Ammetto di aver raccolto esperienza su un database diverso (Pervasive), ma lo stesso può valere per tutti i motori. Il mio conteggio dei valori per set era 500-1000. Più o meno era significativamente più lento.


So che sono passati 7 anni, ma il problema con questa risposta è semplicemente che si tratta di un commento basato su un'ipotesi plausibile.
Giacomo1968
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.