MySQL - SELEZIONA il campo IN (sottoquery) - Molto lento perché?


133

Ho un paio di duplicati in un database che voglio ispezionare, quindi quello che ho fatto per vedere quali sono i duplicati, ho fatto questo:

SELECT relevant_field
FROM some_table
GROUP BY relevant_field
HAVING COUNT(*) > 1

In questo modo, otterrò tutte le righe con il campo rilevante che si verificano più di una volta. Questa query richiede millisecondi per essere eseguita.

Ora, volevo ispezionare ciascuno dei duplicati, quindi ho pensato di poter selezionare ogni riga in some_table con un campo rilevante nella query sopra, quindi ho fatto così:

SELECT *
FROM some_table 
WHERE relevant_field IN
(
    SELECT relevant_field
    FROM some_table
    GROUP BY relevant_field
    HAVING COUNT(*) > 1
)

Questo risulta essere estremamente lento per qualche motivo (richiede minuti). Cosa sta succedendo esattamente qui per renderlo così lento? rilevante_field è indicizzato.

Alla fine ho provato a creare una vista "temp_view" dalla prima query (SELECT relevant_field FROM some_table GROUP BY relevant_field HAVING COUNT(*) > 1)e quindi a fare la mia seconda query in questo modo:

SELECT *
FROM some_table
WHERE relevant_field IN
(
    SELECT relevant_field
    FROM temp_view
)

E funziona benissimo. MySQL lo fa in alcuni millisecondi.

Qualche esperto SQL qui che può spiegare cosa sta succedendo?


cosa vuoi esattamente? vuoi cancellare voci duplicate tranne una ?? Suggerimento: leggi Self Join
diEcho

1
ovviamente è il gruppo che è lento ...
ajreal

La prima query viene eseguita in millisecondi (quella che raggruppa e filtra con HAVING). È solo in combinazione con l'altra query che rende tutto lento (richiede pochi minuti).
quano,

@diEcho, voglio trovare duplicati, ispezionarli ed eliminarne alcuni manualmente.
quano,

Risposte:


112

Riscrivi la query in questo

SELECT st1.*, st2.relevant_field FROM sometable st1
INNER JOIN sometable st2 ON (st1.relevant_field = st2.relevant_field)
GROUP BY st1.id  /* list a unique sometable field here*/
HAVING COUNT(*) > 1

Penso che st2.relevant_fielddebba essere nella selezione, perché altrimenti la havingclausola darà un errore, ma non sono sicuro al 100%

Non usare mai INcon una subquery; questo è notoriamente lento.
Utilizzare sempre e solo INcon un elenco fisso di valori.

Più suggerimenti

  1. Se vuoi fare domande più velocemente, non SELECT *selezionare solo i campi di cui hai veramente bisogno.
  2. Assicurati di avere un indice attivo relevant_fieldper velocizzare l'equi-join.
  3. Assicurati di group bysulla chiave primaria.
  4. Se sei su InnoDB e selezioni solo campi indicizzati (e le cose non sono troppo complesse) di MySQL risolverà la tua query usando solo gli indici, velocizzando le cose.

Soluzione generale per il 90% delle tue IN (select domande

Usa questo codice

SELECT * FROM sometable a WHERE EXISTS (
  SELECT 1 FROM sometable b
  WHERE a.relevant_field = b.relevant_field
  GROUP BY b.relevant_field
  HAVING count(*) > 1) 

1
Puoi anche scriverlo con HAVING COUNT(*) > 1. Di solito è più veloce in MySQL.
ypercubeᵀᴹ

@ypercube, fatto per la query in basso, penso che per la query in alto cambierà il risultato.
Johan,

@Johan: poiché st2.relevant_fieldnon lo è NULL(è già incluso nella ONclausola), non altererà il risultato.
ypercubeᵀᴹ

@ypercube, quindi puoi cambiare count (distante) in count (*) se sei sicuro afieldche non lo sarà mai null, capito. Grazie
Johan,

1
@quano, sì, elenca tutti i duplicati, perché il group byè in st1.id, non su st1.relevant_field.
Johan,

110

La sottoquery viene eseguita per ogni riga perché è una query correlata. È possibile trasformare una query correlata in una query non correlata selezionando tutto dalla sottoquery, in questo modo:

SELECT * FROM
(
    SELECT relevant_field
    FROM some_table
    GROUP BY relevant_field
    HAVING COUNT(*) > 1
) AS subquery

La query finale sarebbe simile a questa:

SELECT *
FROM some_table
WHERE relevant_field IN
(
    SELECT * FROM
    (
        SELECT relevant_field
        FROM some_table
        GROUP BY relevant_field
        HAVING COUNT(*) > 1
    ) AS subquery
)

3
Questo ha funzionato incredibilmente bene per me. Avevo un altro IN (sottoquery) all'interno di un IN (sottoquery), e ci sono voluti più di 10 minuti, così a lungo che ho cercato su Google mentre aspettavo. L'avvolgimento di ogni subquery in SELECT * FROM () come suggerito lo ha ridotto a 2 secondi!
Liam,

GRAZIE, sto cercando di trovare un buon modo per farlo da un paio d'ore. Questo ha funzionato perfettamente. Vorrei poterti dare più voti! Questa dovrebbe essere sicuramente la risposta.
Thaspius,

Funziona perfettamente. Una query che ha richiesto ~ 50 secondi per l'esecuzione è ora istantanea. Vorrei poter votare di più. A volte non è possibile utilizzare i join, quindi questa è la risposta giusta.
simon,

Mi chiedo perché l'ottimizzatore consideri le query con i sindacati correlati ... Comunque, questo trucco ha funzionato come per magia
Brian Leishman,

2
Potresti spiegare cosa rende questa subquery correlata? Comprendo che la subquery diventa correlata, quando utilizza un valore che dipende dalla query esterna. Ma in questo esempio non riesco a vedere alcuna interdipendenza. Darebbe lo stesso risultato per ogni riga restituita dalla query esterna. Ho un esempio simile in fase di implementazione su MariaDB e non riesco a vedere alcun hit di performance (finora), quindi mi piacerebbe vedere chiaramente, quando SELECT *è necessario questo wrapping.
sbnc.eu

6

Sospettavo qualcosa del genere, che la query secondaria fosse in esecuzione per ogni riga.
quano,

Alcune versioni di MySQL non usano nemmeno un indice in IN. Ho aggiunto un altro link.
Edze,

1
MySQL 6 non è ancora stabile, non lo consiglierei per la produzione!
Johan,

1
Non lo raccomanderei. Ma qui viene spiegato come funziona internamente (4.1 / 5.x -> 6). Questo dimostra alcune insidie ​​delle versioni attuali.
Edze,

5
SELECT st1.*
FROM some_table st1
inner join 
(
    SELECT relevant_field
    FROM some_table
    GROUP BY relevant_field
    HAVING COUNT(*) > 1
)st2 on st2.relevant_field = st1.relevant_field;

Ho provato la tua query su uno dei miei database e ho anche provato a riscriverla come join a una query secondaria.

Ha funzionato molto più velocemente, provalo!


Sì, questo probabilmente creerà una tabella temporanea con i risultati del gruppo, quindi avrà la stessa velocità della versione della vista. Ma i piani di query dovrebbero dire la verità.
ypercubeᵀᴹ

3

Prova questo

SELECT t1.*
FROM 
 some_table t1,
  (SELECT relevant_field
  FROM some_table
  GROUP BY relevant_field
  HAVING COUNT (*) > 1) t2
WHERE
 t1.relevant_field = t2.relevant_field;

2

Ho riformattato la tua query sql lenta con www.prettysql.net

SELECT *
FROM some_table
WHERE
 relevant_field in
 (
  SELECT relevant_field
  FROM some_table
  GROUP BY relevant_field
  HAVING COUNT ( * ) > 1
 );

Quando si utilizza una tabella sia nella query che nella sottoquery, è necessario sempre alias entrambi, in questo modo:

SELECT *
FROM some_table as t1
WHERE
 t1.relevant_field in
 (
  SELECT t2.relevant_field
  FROM some_table as t2
  GROUP BY t2.relevant_field
  HAVING COUNT ( t2.relevant_field ) > 1
 );

Questo aiuta?


1
Purtroppo non aiuta. Si esegue altrettanto lentamente.
quano,

Ho aggiornato la mia risposta, puoi riprovare? Anche se il gruppo da è lenta, dovrebbe essere eseguito solo una volta ...
plang

L'ultima volta ho ucciso accidentalmente un server mysql live, quindi temo di non poterlo provare adesso. Dovrò impostare un database di test in seguito. Ma non capisco perché questo dovrebbe influenzare la query. L'istruzione HAVING dovrebbe applicarsi solo alla query in cui si trova, no? Non capisco davvero perché la query "reale" dovrebbe influire sulla query secondaria.
quano,

Ho trovato questo: xaprb.com/blog/2006/04/30/… . Penso che questa potrebbe essere la soluzione. Proverò quando avrò tempo.
quano,

2

In primo luogo puoi trovare righe duplicate e trovare il conteggio delle righe utilizzato quante volte e ordinarlo per numero come questo;

SELECT q.id,q.name,q.password,q.NID,(select count(*) from UserInfo k where k.NID= q.NID) as Count,
(
		CASE q.NID
		WHEN @curCode THEN
			@curRow := @curRow + 1
		ELSE
			@curRow := 1
		AND @curCode := q.NID
		END
	) AS No
FROM UserInfo q,
(
		SELECT
			@curRow := 1,
			@curCode := ''
	) rt
WHERE q.NID IN
(
    SELECT NID
    FROM UserInfo
    GROUP BY NID
    HAVING COUNT(*) > 1
) 

successivamente crea una tabella e inserisci il risultato.

create table CopyTable 
SELECT q.id,q.name,q.password,q.NID,(select count(*) from UserInfo k where k.NID= q.NID) as Count,
(
		CASE q.NID
		WHEN @curCode THEN
			@curRow := @curRow + 1
		ELSE
			@curRow := 1
		AND @curCode := q.NID
		END
	) AS No
FROM UserInfo q,
(
		SELECT
			@curRow := 1,
			@curCode := ''
	) rt
WHERE q.NID IN
(
    SELECT NID
    FROM UserInfo
    GROUP BY NID
    HAVING COUNT(*) > 1
) 

Infine, elimina le righe pubbliche. Non viene avviato 0. Tranne il numero di pugno di ciascun gruppo, elimina tutte le righe pubbliche.

delete from  CopyTable where No!= 0;


1

a volte quando i dati diventano più grandi mysql DOVE IN potrebbe essere piuttosto lento a causa dell'ottimizzazione delle query. Prova a utilizzare STRAIGHT_JOIN per dire a mysql di eseguire la query così com'è, ad es

SELECT STRAIGHT_JOIN table.field FROM table WHERE table.id IN (...)

ma attenzione: nella maggior parte dei casi mysql optimizer funziona abbastanza bene, quindi consiglierei di usarlo solo quando hai questo tipo di problema


0

Questo è simile al mio caso, in cui ho una tabella denominata tabel_buku_besar. Quello di cui ho bisogno sono

  1. Alla ricerca di record che hanno account_code='101.100'in tabel_buku_besarcui hanno companyarea='20000'e anche IDRcomecurrency

  2. Devo ottenere tutti i record dai tabel_buku_besarquali account_code è uguale al passaggio 1 ma con il transaction_numberrisultato del passaggio 1

durante l'utilizzo select ... from...where....transaction_number in (select transaction_number from ....) , la mia query è estremamente lenta e talvolta causa il timeout della richiesta o impedisce alla mia applicazione di rispondere ...

Provo questa combinazione e il risultato ... non male ...

`select DATE_FORMAT(L.TANGGAL_INPUT,'%d-%m-%y') AS TANGGAL,
      L.TRANSACTION_NUMBER AS VOUCHER,
      L.ACCOUNT_CODE,
      C.DESCRIPTION,
      L.DEBET,
      L.KREDIT 
 from (select * from tabel_buku_besar A
                where A.COMPANYAREA='$COMPANYAREA'
                      AND A.CURRENCY='$Currency'
                      AND A.ACCOUNT_CODE!='$ACCOUNT'
                      AND (A.TANGGAL_INPUT BETWEEN STR_TO_DATE('$StartDate','%d/%m/%Y') AND STR_TO_DATE('$EndDate','%d/%m/%Y'))) L 
INNER JOIN (select * from tabel_buku_besar A
                     where A.COMPANYAREA='$COMPANYAREA'
                           AND A.CURRENCY='$Currency'
                           AND A.ACCOUNT_CODE='$ACCOUNT'
                           AND (A.TANGGAL_INPUT BETWEEN STR_TO_DATE('$StartDate','%d/%m/%Y') AND STR_TO_DATE('$EndDate','%d/%m/%Y'))) R ON R.TRANSACTION_NUMBER=L.TRANSACTION_NUMBER AND R.COMPANYAREA=L.COMPANYAREA 
LEFT OUTER JOIN master_account C ON C.ACCOUNT_CODE=L.ACCOUNT_CODE AND C.COMPANYAREA=L.COMPANYAREA 
ORDER BY L.TANGGAL_INPUT,L.TRANSACTION_NUMBER`

0

Trovo che questo sia il più efficiente per scoprire se esiste un valore, la logica può essere facilmente invertita per scoprire se un valore non esiste (cioè IS NULL);

SELECT * FROM primary_table st1
LEFT JOIN comparision_table st2 ON (st1.relevant_field = st2.relevant_field)
WHERE st2.primaryKey IS NOT NULL

* Sostituisci il campo rilevante con il nome del valore che desideri verificare nella tabella

* Sostituire primaryKey con il nome della colonna chiave primaria nella tabella di confronto.

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.