2010-08-05 1 views

Répondre

45

Résumé: Ceci est un known problem dans MySQL et a été corrigé dans MySQL 5.6.x. Le problème est dû à une optimisation manquante lorsqu'une sous-requête utilisant IN est incorrectement identifiée comme sous-requête dépendante au lieu d'une sous-requête indépendante.


Lorsque vous exécutez EXPLIQUEZ sur la requête d'origine, il retourne ceci:

 
1 'PRIMARY'    'question_law_version' 'ALL' '' '' '' '' 10148 'Using where' 
2 'DEPENDENT SUBQUERY' 'question_law_version' 'ALL' '' '' '' '' 10148 'Using where' 
3 'DEPENDENT SUBQUERY' 'question_law'   'ALL' '' '' '' '' 10040 'Using where' 

Lorsque vous modifiez IN-= vous obtenez ceci:

 
1 'PRIMARY' 'question_law_version' 'ALL' '' '' '' '' 10148 'Using where' 
2 'SUBQUERY' 'question_law_version' 'ALL' '' '' '' '' 10148 'Using where' 
3 'SUBQUERY' 'question_law'   'ALL' '' '' '' '' 10040 'Using where' 

Chaque sous-requête dépendante est exécutée une fois par ligne dans la requête dans laquelle elle est contenue, alors que la sous-requête est exécutée une seule fois. MySQL peut parfois optimiser les sous-requêtes dépendantes quand il y a une condition qui peut être convertie en une jointure mais ici ce n'est pas le cas. Maintenant, cela laisse bien sûr la question de savoir pourquoi MySQL pense que la version IN doit être une sous-requête dépendante. J'ai fait une version simplifiée de la requête pour aider à étudier cela. J'ai créé deux tables 'foo' et 'bar' où la première contient seulement une colonne id, et la seconde contient à la fois un id et un foo id (bien que je n'ai pas créé une contrainte de clé étrangère). Ensuite, je peuplé les deux tables avec 1000 lignes:

CREATE TABLE foo (id INT PRIMARY KEY NOT NULL); 
CREATE TABLE bar (id INT PRIMARY KEY, foo_id INT NOT NULL); 

-- populate tables with 1000 rows in each 

SELECT id 
FROM foo 
WHERE id IN 
(
    SELECT MAX(foo_id) 
    FROM bar 
); 

Cette requête simplifiée a le même problème que précédemment - la sélection intérieure est traitée comme une sous-requête dépendante et aucune optimisation est effectuée, ce qui provoque la requête interne à exécuter une fois par rangée. La requête prend presque une seconde à courir. Changer le IN en = permet à nouveau à la requête de s'exécuter presque instantanément.

Le code que j'ai utilisé pour remplir les tableaux est ci-dessous, au cas où quelqu'un souhaite reproduire les résultats.

CREATE TABLE filler (
     id INT NOT NULL PRIMARY KEY AUTO_INCREMENT 
) ENGINE=Memory; 

DELIMITER $$ 

CREATE PROCEDURE prc_filler(cnt INT) 
BEGIN 
     DECLARE _cnt INT; 
     SET _cnt = 1; 
     WHILE _cnt <= cnt DO 
       INSERT 
       INTO filler 
       SELECT _cnt; 
       SET _cnt = _cnt + 1; 
     END WHILE; 
END 
$$ 

DELIMITER ; 

CALL prc_filler(1000); 

INSERT foo SELECT id FROM filler; 
INSERT bar SELECT id, id FROM filler; 
+2

Existe-t-il un moyen de forcer l'optimiseur à traiter une sous-requête uniquement comme une sous-requête et non comme une sous-requête dépendante? –

+0

@Itay Moav: MySQL devrait être en mesure de déterminer par lui-même quelles sous-requêtes dépendent des requêtes externes. Je suis encore un peu surpris que dans ce cas, il pense que la requête interne est une requête dépendante quand il n'y a clairement aucune référence à la table d'origine. Je pourrais rechercher la base de données de bogues pour voir si n'importe qui a signalé ce problème. –

+0

@Itay Moav: J'ai simplifié la requête et répliqué le même problème sur la requête plus simple. J'ai trouvé un rapport de bogue dans MySQL qui décrit exactement le même problème. Les développeurs MySQL promettent une solution. J'ai mis à jour ma réponse en conséquence. J'espère que cela répond à votre question complètement. PS: +1 pour la bonne question qui m'obligeait à faire des recherches! :) –

0

Les optimiseurs SQL ne font pas toujours ce que vous attendez d'eux. Je ne suis pas sûr qu'il y ait une meilleure réponse que ça. C'est pourquoi vous devez examiner la sortie EXPLAIN PLAN, et profiler vos requêtes pour savoir où le temps est passé.

+0

pour recommander EXPLAIN comme point de départ pour analyser les performances des requêtes . – Cumbayah

1

Il s'agit de requêtes internes a.k.a sous-requêtes vs jointures, pas sur IN vs =, et les raisons sont expliquées dans ce post. La version 5.4 de MySQL est prévue pour introduire un optimiseur amélioré, qui peut réécrire certaines sous-requêtes dans une forme plus efficace.

La pire chose que vous pouvez faire, est d'utiliser ce qu'on appelle sous-requête corrélée http://dev.mysql.com/doc/refman/5.1/en/correlated-subqueries.html

0

Il est intéressant mais le problème peut également être résolu avec les instructions préparées (pas sûr si elle convient à tout le monde), par exemple:

mysql> EXPLAIN SELECT * FROM words WHERE word IN (SELECT word FROM phrase_words); 
+----+--------------------+--------------+... 
| id | select_type  | table  |... 
+----+--------------------+--------------+... 
| 1 | PRIMARY   | words  |... 
| 2 | DEPENDENT SUBQUERY | phrase_words |... 
+----+--------------------+--------------+... 
mysql> EXPLAIN SELECT * FROM words WHERE word IN ('twist','rollers'); 
+----+-------------+-------+... 
| id | select_type | table |... 
+----+-------------+-------+... 
| 1 | SIMPLE  | words |... 
+----+-------------+-------+... 

Il suffit donc de préparer la déclaration dans une procédure stockée , puis exécutez-le.Voici l'idée:

SET @words = (SELECT GROUP_CONCAT(word SEPARATOR '\',\'') FROM phrase_words); 
SET @words = CONCAT("'", @words, "'"); 
SET @query = CONCAT("SELECT * FROM words WHERE word IN (", @words, ");"; 
PREPARE q FROM @query; 
EXECUTE q; 
+0

Si vous voulez aller à cette déroute, alors créer dans le SP une table temporaire avec seulement les valeurs que vous voulez dans l'IN et le joindre à la table principale. –

+0

C'est un bon point, merci beaucoup! – Maksim

Questions connexes