MySQL - Champ SELECT WHERE IN (sous-requête) - Extrêmement lent pourquoi?

133

J'ai quelques doublons dans une base de données que je veux inspecter, alors ce que j'ai fait pour voir lesquels sont des doublons, je l'ai fait:

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

De cette façon, j'obtiendrai toutes les lignes avec relevant_field se produisant plus d'une fois. Cette requête prend quelques millisecondes pour s'exécuter.

Maintenant, je voulais inspecter chacun des doublons, donc j'ai pensé que je pourrais SELECT chaque ligne dans some_table avec un relevant_field dans la requête ci-dessus, donc j'ai fait comme ceci:

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

Cela s'avère extrêmement lent pour une raison quelconque (cela prend quelques minutes). Que se passe-t-il exactement ici pour le rendre aussi lent? relevant_field est indexé.

Finalement, j'ai essayé de créer une vue "temp_view" à partir de la première requête (SELECT relevant_field FROM some_table GROUP BY relevant_field HAVING COUNT(*) > 1), puis de créer ma deuxième requête comme ceci à la place:

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

Et cela fonctionne très bien. MySQL le fait en quelques millisecondes.

Y a-t-il des experts SQL ici qui peuvent expliquer ce qui se passe?

quano
la source
qu'est-ce que tu veux exactement? voulez supprimer les entrées en double sauf une ?? Suggestion: s'il vous plaît lire Self Join
diEcho
1
est évidemment le group-by qui est lent ...
ajreal
La première requête s'exécute en millisecondes (celle regroupant et filtrant avec HAVING). Ce n'est qu'en combinaison avec l'autre requête qui ralentit tout (cela prend quelques minutes).
quano
@diEcho, je veux trouver des doublons, les inspecter et en supprimer manuellement.
quano

Réponses:

112

Réécrivez la requête dans ce

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

Je pense que st2.relevant_fielddoit être dans le select, car sinon la havingclause donnera une erreur, mais je ne suis pas sûr à 100%

Ne jamais utiliser INavec une sous-requête; c'est notoirement lent.
Ne jamais utiliser INqu'avec une liste fixe de valeurs.

Plus de conseils

  1. Si vous souhaitez accélérer les requêtes, ne sélectionnez pas SELECT *uniquement les champs dont vous avez vraiment besoin.
  2. Assurez-vous d'avoir un index activé relevant_fieldpour accélérer l'équi-jointure.
  3. Assurez-vous de group bysur la clé primaire.
  4. Si vous êtes sur InnoDB et que vous ne sélectionnez que des champs indexés (et que les choses ne sont pas trop complexes), MySQL résoudra votre requête en utilisant uniquement les index, accélérant ainsi les choses.

Solution générale pour 90% de vos IN (select requêtes

Utilisez ce code

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) 
Johan
la source
1
Vous pouvez également écrire cela avec HAVING COUNT(*) > 1. C'est généralement plus rapide dans MySQL.
ypercubeᵀᴹ
@ypercube, fait pour la requête du bas, je pense que pour la requête du haut, cela modifiera le résultat.
Johan
@Johan: Puisque ce st2.relevant_fieldn'est pas le cas NULL(c'est déjà inclus dans la ONclause), cela ne modifiera pas le résultat.
ypercubeᵀᴹ
@ypercube, afin que vous puissiez changer count (à distance) en count (*) si vous êtes sûr que afieldce ne sera jamais le cas null. Merci
Johan
1
@quano, oui il répertorie tous les doublons parce que le group byest sur st1.id, pas st1.relevant_field.
Johan
110

La sous-requête est exécutée pour chaque ligne car il s'agit d'une requête corrélée. On peut transformer une requête corrélée en une requête non corrélée en sélectionnant tout dans la sous-requête, comme ceci:

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

La requête finale ressemblerait à ceci:

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
)
quano
la source
3
Cela a très bien fonctionné pour moi. J'avais une autre IN (sous-requête) dans une IN (sous-requête), et cela prenait plus de 10 minutes, si longtemps que j'ai cherché sur Google pendant que j'attendais. L'emballage de chaque sous-requête dans SELECT * FROM () comme vous l'avez suggéré l'a réduit à 2 secondes!
Liam
MERCI, j'essaie de trouver un bon moyen de le faire depuis quelques heures maintenant. Cela a parfaitement fonctionné. J'aimerais pouvoir vous donner plus de votes positifs! Cela devrait certainement être la réponse.
thaspius
Fonctionne parfaitement. Une requête qui a pris environ 50 secondes pour s'exécuter est désormais instantanée. J'aimerais pouvoir voter davantage. Parfois, vous ne pouvez pas utiliser de jointures, c'est donc la bonne réponse.
simon
Je me demande pourquoi l'optimiseur considère les requêtes avec les syndicats corrélées ... Quoi qu'il en soit, cette astuce a fonctionné comme par magie
Brian Leishman
2
Pouvez-vous expliquer ce qui en fait une sous-requête corrélée? Ma compréhension que la sous-requête devient corrélée, lorsqu'elle utilise une valeur qui dépend de la requête externe. Mais dans cet exemple, je ne vois aucune interdépendance. Cela donnerait le même résultat pour chaque ligne renvoyée par la requête externe. J'ai un exemple similaire en cours d'implémentation sur MariaDB et je ne vois aucun impact sur les performances (jusqu'à présent), donc j'aimerais voir clairement, quand cet SELECT *emballage est nécessaire.
sbnc.eu
6

Sous-requêtes vs jointures

http://www.scribd.com/doc/2546837/New-Subquery-Optimizations-In-MySQL-6

Edze
la source
J'ai suspecté quelque chose comme ça, que la sous-requête est exécutée pour chaque ligne.
quano
Certaines versions de MySQL n'utilisent même pas d'index dans IN. J'ai ajouté un autre lien.
edze
1
MySQL 6 n'est pas encore stable, je ne recommanderais pas cela pour la production!
Johan
1
Je ne le recommanderais pas. Mais voici comment il fonctionne en interne (4.1 / 5.x -> 6). Cela démontre quelques écueils des versions actuelles.
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;

J'ai essayé votre requête sur l'une de mes bases de données et l'ai également réécrite en tant que jointure à une sous-requête.

Cela a fonctionné beaucoup plus rapidement, essayez-le!

ceteras
la source
Oui, cela créera probablement une table temporaire avec les résultats du groupe, donc ce sera la même vitesse que la version d'affichage. Mais les plans de requête doivent dire la vérité.
ypercubeᵀᴹ
3

Essaye ça

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;
user2244323
la source
2

J'ai reformaté votre requête SQL lente avec www.prettysql.net

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

Lorsque vous utilisez une table à la fois dans la requête et la sous-requête, vous devez toujours aliaser les deux, comme ceci:

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
 );

Est ce que ça aide?

plang
la source
1
Cela n'aide malheureusement pas. Il s'exécute tout aussi lentement.
quano
J'ai mis à jour ma réponse, pouvez-vous réessayer? Même si le groupe est lent, il ne devrait être exécuté qu'une seule fois ...
plang
J'ai accidentellement tué un serveur mysql en direct la dernière fois, donc j'ai bien peur de ne pas pouvoir l'essayer maintenant. Je devrai mettre en place une base de données de test plus tard. Mais je ne comprends pas pourquoi cela devrait affecter la requête. L'instruction HAVING ne devrait s'appliquer qu'à la requête dans laquelle elle se trouve, n'est-ce pas? Je ne comprends vraiment pas pourquoi la "vraie" requête devrait affecter la sous-requête.
quano
J'ai trouvé ceci: xaprb.com/blog/2006/04/30/… . Je pense que cela pourrait être la solution. J'essaierai quand j'aurai le temps.
quano
2

Tout d'abord, vous pouvez trouver des lignes en double et trouver le nombre de lignes utilisé combien de fois et le classer par numéro comme ceci;

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
) 

après cela, créez une table et insérez-y le résultat.

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
) 

Enfin, supprimez les lignes en double. Non est le début 0. À l'exception du premier numéro de chaque groupe, supprimez toutes les lignes en double.

delete from  CopyTable where No!= 0;

harun ugur
la source
1

parfois, lorsque les données grossissent, mysql WHERE IN peut être assez lent à cause de l'optimisation des requêtes. Essayez d'utiliser STRAIGHT_JOIN pour dire à mysql d'exécuter la requête telle quelle, par exemple

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

mais attention: dans la plupart des cas, l'optimiseur mysql fonctionne plutôt bien, donc je recommanderais de ne l'utiliser que lorsque vous avez ce genre de problème

Andrey Posudevsky
la source
0

C'est similaire à mon cas, où j'ai une table nommée tabel_buku_besar. Ce dont j'ai besoin c'est

  1. Vous cherchez pour l' enregistrement qui ont account_code='101.100'en tabel_buku_besarqui ont companyarea='20000'et aussi IDRcommecurrency

  2. J'ai besoin d'obtenir tous les enregistrements tabel_buku_besardont le code_compte est identique à l'étape 1 mais qui a le résultat transaction_numberde l'étape 1

lors de l'utilisation select ... from...where....transaction_number in (select transaction_number from ....), ma requête s'exécute extrêmement lentement et entraîne parfois l'expiration de la requête ou empêche mon application de répondre ...

J'essaye cette combinaison et le résultat ... pas mal ...

`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`
Hilarius L. Doren
la source
0

Je trouve que c'est le plus efficace pour trouver si une valeur existe, la logique peut facilement être inversée pour trouver si une valeur n'existe pas (c'est-à-dire 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

* Remplacez relevant_field par le nom de la valeur dont vous souhaitez vérifier l'existence dans votre table

* Remplacez primaryKey par le nom de la colonne de clé primaire sur la table de comparaison.

Mat
la source