Performance SQL: choisir DISTINCT versus GROUP BY

j'ai essayé d'améliorer les temps de requête pour une application existante gérée par une base de données Oracle qui a été en cours d'exécution un peu léthargique. L'application exécute plusieurs grandes requêtes, comme celle ci-dessous, qui peuvent prendre plus d'une heure à exécuter. Le remplacement de l' DISTINCT avec un GROUP BY la clause dans la requête ci-dessous réduit le temps d'exécution de 100 minutes à 10 Secondes. Ma compréhension était que SELECT DISTINCT et GROUP BY exploité dans à peu près de la même façon. Pourquoi une telle disparité entre temps d'exécution? Quelle est la différence dans la façon dont la requête est exécutée à la fin? Est-il jamais une situation où SELECT DISTINCT court plus vite?

Remarque: Dans la requête suivante, WHERE TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A' ne représente qu'une des nombreuses façons dont les résultats peuvent être filtrés. Cet exemple a été fourni pour montrer le raisonnement pour joindre toutes les tables qui n'ont pas de colonnes incluses dans le SELECT et résulterait en environ un dixième de toutes les données disponibles

SQL à l'aide de DISTINCT:

SELECT DISTINCT 
    ITEMS.ITEM_ID,
    ITEMS.ITEM_CODE,
    ITEMS.ITEMTYPE,
    ITEM_TRANSACTIONS.STATUS,
    (SELECT COUNT(PKID) 
        FROM ITEM_PARENTS 
        WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID
        ) AS CHILD_COUNT
FROM
    ITEMS
    INNER JOIN ITEM_TRANSACTIONS 
        ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID 
        AND ITEM_TRANSACTIONS.FLAG = 1
    LEFT OUTER JOIN ITEM_METADATA 
        ON ITEMS.ITEM_ID = ITEM_METADATA.ITEM_ID
    LEFT OUTER JOIN JOB_INVENTORY 
        ON ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID     
    LEFT OUTER JOIN JOB_TASK_INVENTORY 
        ON JOB_INVENTORY.JOB_ITEM_ID = JOB_TASK_INVENTORY.JOB_ITEM_ID
    LEFT OUTER JOIN JOB_TASKS 
        ON JOB_TASK_INVENTORY.TASKID = JOB_TASKS.TASKID                              
    LEFT OUTER JOIN JOBS 
        ON JOB_TASKS.JOB_ID = JOBS.JOB_ID
    LEFT OUTER JOIN TASK_INVENTORY_STEP 
        ON JOB_INVENTORY.JOB_ITEM_ID = TASK_INVENTORY_STEP.JOB_ITEM_ID 
    LEFT OUTER JOIN TASK_STEP_INFORMATION 
        ON TASK_INVENTORY_STEP.JOB_ITEM_ID = TASK_STEP_INFORMATION.JOB_ITEM_ID
WHERE 
    TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
ORDER BY 
    ITEMS.ITEM_CODE

SQL à l'aide de GROUP BY:

SELECT
    ITEMS.ITEM_ID,
    ITEMS.ITEM_CODE,
    ITEMS.ITEMTYPE,
    ITEM_TRANSACTIONS.STATUS,
    (SELECT COUNT(PKID) 
        FROM ITEM_PARENTS 
        WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID
        ) AS CHILD_COUNT
FROM
    ITEMS
    INNER JOIN ITEM_TRANSACTIONS 
        ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID 
        AND ITEM_TRANSACTIONS.FLAG = 1
    LEFT OUTER JOIN ITEM_METADATA 
        ON ITEMS.ITEM_ID = ITEM_METADATA.ITEM_ID
    LEFT OUTER JOIN JOB_INVENTORY 
        ON ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID     
    LEFT OUTER JOIN JOB_TASK_INVENTORY 
        ON JOB_INVENTORY.JOB_ITEM_ID = JOB_TASK_INVENTORY.JOB_ITEM_ID
    LEFT OUTER JOIN JOB_TASKS 
        ON JOB_TASK_INVENTORY.TASKID = JOB_TASKS.TASKID                              
    LEFT OUTER JOIN JOBS 
        ON JOB_TASKS.JOB_ID = JOBS.JOB_ID
    LEFT OUTER JOIN TASK_INVENTORY_STEP 
        ON JOB_INVENTORY.JOB_ITEM_ID = TASK_INVENTORY_STEP.JOB_ITEM_ID 
    LEFT OUTER JOIN TASK_STEP_INFORMATION 
        ON TASK_INVENTORY_STEP.JOB_ITEM_ID = TASK_STEP_INFORMATION.JOB_ITEM_ID
WHERE 
    TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
GROUP BY
    ITEMS.ITEM_ID,
    ITEMS.ITEM_CODE,
    ITEMS.ITEMTYPE,
    ITEM_TRANSACTIONS.STATUS
ORDER BY 
    ITEMS.ITEM_CODE

voici le plan de requête Oracle pour la requête en utilisant DISTINCT:

Oracle query plan for query using DISTINCT

voici le plan de requête Oracle pour la requête en utilisant GROUP BY:

Oracle query plan for query using GROUP BY

22
demandé sur woemler 2012-12-19 20:27:23

4 réponses

la différence de performance est probablement due à l'exécution du sous-jeu dans le SELECT l'article. Je devine qu'il est en train de ré-exécuter cette requête pour chaque ligne avant l'distinctes. Pour l' group by, il exécute une fois après le groupe de par.

essayez de le remplacer par une jointure, à la place:

select . . .,
       parentcnt
from . . . left outer join
      (SELECT PARENT_ITEM_ID, COUNT(PKID) as parentcnt
       FROM ITEM_PARENTS 
      ) p
      on items.item_id = p.parent_item_id
18
répondu Gordon Linoff 2012-12-19 18:59:10

je suis assez sûr que GROUP BY et DISTINCT ont à peu près le même plan d'exécution.

la différence ici puisque nous devons deviner (puisque nous n'avons pas les plans d'explication) est IMO que le sous-jeu en ligne est exécuté aprèsGROUP BY mais avantDISTINCT.

donc si votre requête retourne des lignes 1M et est agrégée à des lignes 1k:

  • GROUP BY la requête aurait exécuté le sous-jeu 1000 fois,
  • alors que le DISTINCT la requête aurait exécuté le sous-jeu 1000000 fois.

le plan d'explication de tkprof permettrait de démontrer cette hypothèse.


pendant que nous discutons de cela, je pense qu'il est important de noter que la façon dont la requête est écrite est trompeuse à la fois pour le lecteur et pour l'optimiseur: vous voulez évidemment trouver toutes les lignes de item / item_transactions qui ont un TASK_INVENTORY_STEP.STEP_TYPE avec une valeur de "TYPE A".

IMO votre requête aurait un meilleur plan et serait plus facilement lisible si écrit comme ceci:

SELECT ITEMS.ITEM_ID,
       ITEMS.ITEM_CODE,
       ITEMS.ITEMTYPE,
       ITEM_TRANSACTIONS.STATUS,
       (SELECT COUNT(PKID) 
          FROM ITEM_PARENTS 
         WHERE PARENT_ITEM_ID = ITEMS.ITEM_ID) AS CHILD_COUNT
  FROM ITEMS
  JOIN ITEM_TRANSACTIONS 
    ON ITEMS.ITEM_ID = ITEM_TRANSACTIONS.ITEM_ID 
   AND ITEM_TRANSACTIONS.FLAG = 1
 WHERE EXISTS (SELECT NULL
                 FROM JOB_INVENTORY   
                 JOIN TASK_INVENTORY_STEP 
                   ON JOB_INVENTORY.JOB_ITEM_ID=TASK_INVENTORY_STEP.JOB_ITEM_ID
                WHERE TASK_INVENTORY_STEP.STEP_TYPE = 'TYPE A'
                  AND ITEMS.ITEM_ID = JOB_INVENTORY.ITEM_ID)

dans de nombreux cas, un DISTINCT peut être un signe que la requête n'est pas écrite correctement (parce qu'une bonne requête ne devrait pas retourner les doublons).

Notez aussi que 4 tableaux ne sont pas utilisés dans votre select original.

16
répondu Vincent Malgrat 2012-12-19 17:01:38

la première chose à noter est l'utilisation de Distinct indique une odeur de code, alias anti-pattern. Cela signifie généralement qu'il y a une jointure manquante ou une jointure supplémentaire qui génère des données dupliquées. En regardant votre requête ci-dessus, je devine que la raison pourquoi group by est plus rapide( sans voir la requête), est que l'emplacement du group by réduit le nombre d'enregistrements qui finissent par être renvoyé. Alors que les distinct est soufflant sur le jeu de résultats et faisant ligne par ligne comparaison.

mise à Jour à l'approche de

Désolé, j'aurais dû être plus clair. Les enregistrements sont générés lorsque les utilisateurs exécutent certaines tâches dans le système, il n'y a donc pas de calendrier. Un l'utilisateur peut générer un seul enregistrement en une journée ou des centaines par heure. Le ce qui est important, c'est que chaque fois qu'un utilisateur lance une recherche, il est à jour. les dossiers doivent être retournés, ce qui me fait douter qu'une la vue fonctionnerait ici, surtout si la requête le remplissage il faudrait de temps à s'exécuter.

je crois que c'est l'exacte raison de l'utilisation d'une vue matérialisée. Donc le processus fonctionnerait de cette façon. Vous prenez la longue requête en cours d'exécution comme la pièce qui construit votre vue matérialisée, puisque nous savons que l'utilisateur se soucie seulement de "nouvelles" données après qu'ils accomplissent quelque tâche arbitraire dans le système. Donc, ce que vous voulez faire est d'interroger cette vue matérialisée de base, qui peut être rafraîchie en permanence sur la fin, la persistance la stratégie ne doit pas étouffer la vision matérialisée (la persistance de quelques centaines de disques à la fois ne détruira rien). Ce que cela permettra est Oracle pour saisir un verrou de lecture (notez que nous ne nous soucions pas combien de sources lisent nos données, nous ne nous soucions que des rédacteurs). Dans le pire des cas, un utilisateur aura des données "périmées" pour des microsecondes, donc à moins qu'il s'agisse d'un système de trading financier sur Wall Street ou d'un système pour un réacteur nucléaire, ces "blips" devraient passer inaperçus même par les utilisateurs les plus optimistes.

exemple de Code de la façon de faire ceci:

create materialized view dept_mv FOR UPDATE as select * from dept; 

maintenant la clé pour cela est aussi longtemps que vous n'invoquez pas refresh vous ne perdrez aucune des données persistened. Ce sera à vous de déterminer quand vous voulez "ligne de base" de votre vue matérialisée à nouveau (minuit, peut-être?)

8
répondu Woot4Moo 2012-12-19 19:57:57

vous devez utiliser GROUP BY pour appliquer les opérateurs aggregate à chaque groupe et DISTINCT si vous n'avez besoin de supprimer que les doublons.

je pense que la performance est la même.

dans votre cas, je pense que vous devriez utiliser GROUP BY.

-3
répondu cesarfaria 2012-12-19 16:34:02