2012-12-22 3 views
2

MySQL semble être incapable d'optimiser un select avec une sous-requête GROUP BY et se retrouve dans des temps d'exécution longs. Il doit y avoir une optimisation connue pour un tel scénario commun. Supposons que nous essayons de renvoyer tous les ordres de la base de données, avec un drapeau indiquant si c'est la première commande pour le client.Sous-requête MySQL avec groupe par jointure gauche - optimisation

CREATE TABLE orders (order int, customer int, date date); 

La récupération des premières commandes par le client est très rapide.

SELECT customer, min(order) as first_order FROM orders GROUP BY customer; 

Cependant, il devient très lent une fois que nous associons cela avec l'ensemble de la commande à l'aide d'un sous-requête

SELECT order, first_order FROM orders LEFT JOIN ( 
    SELECT customer, min(order) as first_order FROM orders GROUP BY customer 
) AS first_orders ON orders.order=first_orders.first_order; 

J'espère qu'il ya une astuce simple que nous manque, parce qu'il serait autrement 1000x plus rapide à faire

CREATE TEMPORARY TABLE tmp_first_order AS 
    SELECT customer, min(order) as first_order FROM orders GROUP BY customer; 
CREATE INDEX tmp_boost ON tmp_first_order (first_order) 

SELECT order, first_order FROM orders LEFT JOIN tmp_first_order 
    ON orders.order=tmp_first_order.first_order; 

EDIT:
Inspiré par @ruakh proposent d option 3, il existe en effet une solution de contournement moins laide utilisant INNER JOIN et UNION, qui a des performances acceptables mais ne nécessite pas de tables temporaires. Cependant, c'est un peu spécifique à notre cas et je me demande s'il existe une optimisation plus générique.

SELECT order, "YES" as first FROM orders INNER JOIN ( 
    SELECT min(order) as first_order FROM orders GROUP BY customer 
) AS first_orders_1 ON orders.order=first_orders_1.first_order 
UNION 
SELECT order, "NO" as first FROM orders INNER JOIN ( 
    SELECT customer, min(order) as first_order FROM orders GROUP BY customer 
) AS first_orders_2 ON first_orders_2.customer = orders.customer 
    AND orders.order > first_orders_2.first_order; 
+0

Quelques idées: analyser le plan d'exécution (Expliquer la requête); Un index; une sous-requête au lieu d'une jointure à gauche. –

+0

kristox, avez-vous vérifié ma réponse? –

Répondre

3

Voici quelques choses que vous pouvez essayer:

  1. Suppression customer du champ liste de la sous-requête, car il ne fait rien de toute façon:

    SELECT order, 
         first_order 
        FROM orders 
        LEFT 
        JOIN (SELECT MIN(order) AS first_order 
          FROM orders 
          GROUP 
          BY customer 
         ) AS first_orders 
        ON orders.order = first_orders.first_order 
    ; 
    
  2. A l'inverse, l'ajout customer à la clause ON, de sorte qu'il fait réellement quelque chose pour vous:

    SELECT order, 
         first_order 
        FROM orders 
        LEFT 
        JOIN (SELECT customer, 
           MIN(order) AS first_order 
          FROM orders 
          GROUP 
          BY customer 
         ) AS first_orders 
        ON orders.customer = first_orders.customer 
        AND orders.order = first_orders.first_order 
    ; 
    
  3. Idem, mais en utilisant un INNER JOIN au lieu d'un LEFT JOIN, et la conversion de votre clause ON originale en une expression CASE:

    SELECT order, 
         CASE WHEN first_order = order THEN first_order END AS first_order 
        FROM orders 
    INNER 
        JOIN (SELECT customer, 
           MIN(order) AS first_order 
          FROM orders 
          GROUP 
          BY customer 
         ) AS first_orders 
        ON orders.customer = first_orders.customer 
    ; 
    
  4. Remplacement toute approche JOIN avec un décorrélé IN -subquery dans une expression CASE:

    SELECT order, 
         CASE WHEN order IN 
            (SELECT MIN(order) 
             FROM orders 
            GROUP 
             BY customer 
           ) 
          THEN order 
         END AS first_order 
        FROM orders 
    ; 
    
  5. Remplacement de l'approche globale JOIN avec une corrélation EXISTS -subquery dans une expression CASE:

    SELECT order, 
         CASE WHEN NOT EXISTS 
            (SELECT 1 
             FROM orders AS o2 
            WHERE o2.customer = o1.customer 
             AND o2.order < o1.order 
           ) 
          THEN order 
         END AS first_order 
        FROM orders AS o1 
    ; 
    

(Il est très probable que certains de ce qui précède sera effectivement effectuer pire, mais je pense qu'ils sont tous ça vaut le coup d'essayer.)

+0

quelle réponse géniale ... –

+0

Bonne réponse @ruakh. L'option 3 est intéressante, mais dans votre exemple, elle ne retournera que les premières commandes. C'est à dire. Si vous avez 100 clients et 2000 commandes, cela ne renverra que les 100 premières commandes. Inspiré par votre suggestion, j'ai essayé quelque chose avec "UNION" qui semble fonctionner. – kristox

+0

@kristox: Re: "Si vous avez 100 clients et 2000 commandes, alors [option 3] ne retournera que les 100 premières commandes": Ce n'est pas vrai. Êtes-vous sûr de bien avoir copié la clause 'ON'? – ruakh

1

Je pense que ce soit plus rapide lors de l'utilisation d'une variable au lieu de LEFT JOIN:

SELECT 
    `order`, 
    If(@previous_customer<>(@previous_customer:=`customer`), 
    `order`, 
    NULL 
) AS first_order 
FROM orders 
JOIN (SELECT @previous_customer := -1) x 
ORDER BY customer, `order`; 

C'est ce que mon exemple sur SQL Fiddle retours:

CUSTOMER ORDER FIRST_ORDER 
1   1  1 
1   2  (null) 
1   3  (null) 
2   4  4 
2   5  (null) 
3   6  6 
4   7  7 
+0

[Section 9.4 du * Manuel de référence MySQL *] (http://dev.mysql.com/doc/refman/5.6/fr/user-variables.html) déconseille d'attribuer une valeur à une variable utilisateur et lisez [ing] la valeur dans la même instruction ", sous prétexte que vous ne pouvez pas garantir qu'elle produira toujours les résultats escomptés (en cas de modification des versions de MySQL, de modification des plans d'exécution, etc.). – ruakh

Questions connexes