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
:
Voici le plan de requête Oracle pour la requête utilisant GROUP BY
:
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
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:
GROUP BY
la requête aurait exécuté la sous-requête 1000 fois,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.
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?)