web-dev-qa-db-fra.com

Performances SQL: SELECT DISTINCT versus GROUP BY

J'ai essayé d'améliorer les temps de requête pour une application basée sur une base de données Oracle existante qui fonctionnait un peu lentement. L'application exécute plusieurs requêtes volumineuses, comme celle ci-dessous, dont l'exécution peut prendre plus d'une heure. Le remplacement de DISTINCT par une clause GROUP BY Dans la requête ci-dessous a réduit le temps d'exécution de 100 minutes à 10 secondes. J'ai cru comprendre que SELECT DISTINCT Et GROUP BY Fonctionnaient à peu près de la même manière. Pourquoi une si grande disparité entre les temps d'exécution? Quelle est la différence dans la façon dont la requête est exécutée sur le back-end? Y a-t-il jamais une situation où SELECT DISTINCT S'exécute plus rapidement?

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 donnerait environ un dixième de toutes les données disponibles

SQL utilisant 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 utilisant 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 utilisant DISTINCT:

Oracle query plan for query using DISTINCT

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

Oracle query plan for query using GROUP BY

23
woemler

La différence de performances est probablement due à l'exécution de la sous-requête dans la clause SELECT. Je suppose qu'il réexécute cette requête pour chaque ligne avant le distinct. Pour le group by, il s'exécuterait une fois après le groupe par.

Essayez plutôt de le remplacer par une jointure:

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
Gordon Linoff

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 que l'OMI que la sous-requête en ligne est exécutée APRÈS le GROUP BY mais AVANT le DISTINCT.

Donc, si votre requête renvoie 1 million de lignes et est agrégée en 1 000 lignes:

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

Le plan d'explication tkprof aiderait à démontrer cette hypothèse.


Pendant que nous en discutons, 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 elle était écrite 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 (car une bonne requête ne doit pas renvoyer de doublons).

Notez également que 4 tableaux ne sont pas utilisés dans votre sélection d'origine.

16
Vincent Malgrat

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 manque une jointure ou une jointure supplémentaire qui génère des données en double. En regardant votre requête ci-dessus, je suppose que la raison pour laquelle group by est plus rapide (sans voir la requête), c'est que l'emplacement du group by réduit le nombre d'enregistrements qui finissent par être renvoyés. Tandis que distinct souffle l'ensemble de résultats et effectue des comparaisons ligne par ligne.

Mise à jour de l'approche

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

Je crois que c'est la raison exacte d'utiliser une vue matérialisée. Donc, le processus fonctionnerait de cette façon. Vous prenez la requête longue comme l'élément qui construit votre vue matérialisée, car nous savons que l'utilisateur ne se soucie des "nouvelles" données qu'après avoir effectué une 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 actualisée en permanence sur le back-end, la stratégie de persistance impliquée ne doit pas étouffer la vue matérialisée (la persistance de quelques centaines d'enregistrements à la fois n'écrasera rien ). Cela permettra à Oracle de saisir un verrou de lecture (notez que le nombre de sources lisant nos données ne nous intéresse pas, nous ne nous soucions que des écrivains). Dans le pire des cas, un utilisateur aura des données "périmées" pour les microsecondes, donc à moins qu'il ne s'agisse d'un système commercial financier à Wall Street ou d'un système pour un réacteur nucléaire, ces "blips" devraient passer inaperçus, même pour les utilisateurs les plus aigles.

Exemple de code sur la façon de procéder:

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

Maintenant, la clé est que tant que vous n'invoquez pas le rafraîchissement, vous ne perdrez aucune des données persistantes. Ce sera à vous de déterminer à quel moment vous souhaitez "baser" votre vue matérialisée (minuit peut-être?)

8
Woot4Moo