Database
 sql >> Base de données >  >> RDS >> Database

Comment CTE peut aider à écrire des requêtes complexes et puissantes :une perspective de performance

Nous voyons souvent des requêtes SQL complexes mal écrites s'exécuter sur une table ou des tables dans les bases de données. Ces requêtes rendent le temps d'exécution très long et consomment énormément de CPU et d'autres ressources. Pourtant, les requêtes complexes fournissent des informations précieuses à l'application/à la personne qui les exécute dans de nombreux cas. Par conséquent, ce sont des atouts utiles dans toutes les variétés d'applications.

Les requêtes complexes sont difficiles à déboguer

Si nous examinons de près les requêtes problématiques, beaucoup d'entre elles sont complexes, en particulier celles spécifiques utilisées dans les rapports.

Les requêtes complexes se composent souvent de cinq grandes tables ou plus et sont reliées par de nombreuses sous-requêtes. Chaque sous-requête a une clause WHERE qui effectue des calculs simples à complexes et/ou des transformations de données tout en joignant les colonnes des tables pertinentes ensemble.

De telles requêtes peuvent devenir difficiles à déboguer sans consommer beaucoup de ressources. La raison en est qu'il est difficile de déterminer si chaque sous-requête et/ou sous-requête jointe produit des résultats corrects.

Un scénario typique est :ils vous appellent tard dans la nuit pour résoudre un problème sur un serveur de base de données occupé avec une requête complexe impliquée, et vous devez le résoudre rapidement. En tant que développeur ou administrateur de base de données, vous pouvez disposer de ressources système et de temps très limités à une heure tardive. Ainsi, la première chose dont vous avez besoin est un plan sur la façon de déboguer la requête problématique.

Parfois, la procédure de débogage se passe bien. Parfois, il faut beaucoup de temps et d'efforts avant d'atteindre l'objectif et de résoudre le problème.

Rédaction de requêtes dans la structure CTE

Mais que se passerait-il s'il existait un moyen d'écrire des requêtes complexes afin de pouvoir les déboguer rapidement, pièce par pièce ?

Il existe un tel moyen. Elle s'appelle Common Table Expression ou CTE.

Common Table Expression est une fonctionnalité standard dans la plupart des bases de données modernes telles que SQLServer, MySQL (à partir de la version 8.0), MariaDB (version 10.2.1), Db2 et Oracle. Il a une structure simple qui encapsule une ou plusieurs sous-requêtes dans un jeu de résultats nommé temporaire. Vous pouvez utiliser cet ensemble de résultats dans d'autres CTE nommés ou sous-requêtes.

Une expression de table commune est, dans une certaine mesure, une vue qui n'existe et est référencée par la requête qu'au moment de l'exécution.

Transformer une requête complexe en une requête de style CTE nécessite une réflexion structurée. Il en va de même pour la POO avec encapsulation lors de la réécriture d'une requête complexe dans une structure CTE.

Vous devez penser à :

  1. Chaque ensemble de données que vous extrayez de chaque table.
  2. Comment sont-elles réunies pour encapsuler les sous-requêtes les plus proches dans un ensemble de résultats nommé temporaire.

Répétez cette opération pour chaque sous-requête et ensemble de données restant jusqu'à ce que vous atteigniez le résultat final de la requête. Notez que chaque jeu de résultats nommé temporaire est également une sous-requête.

La dernière partie de la requête doit être une sélection très "simple", renvoyant le résultat final à l'application. Une fois que vous avez atteint cette dernière partie, vous pouvez l'échanger avec une requête qui sélectionne les données à partir d'un ensemble de résultats temporaire nommé individuellement.

De cette façon, le débogage de chaque jeu de résultats temporaire devient une tâche facile.

Pour comprendre comment nous pouvons construire nos requêtes du plus simple au plus complexe, regardons la structure CTE. La forme la plus simple est la suivante :

WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...

Ici CTE_1 est un nom unique que vous donnez au jeu de résultats nommé temporaire. Il peut y avoir autant d'ensembles de résultats que nécessaire. Ainsi, le formulaire s'étend à, comme indiqué ci-dessous :

WITH CTE_1 as (
select .... from some_table where ...
), CTE_2 as (
select .... from some_other_table where ...
)
select ... from CTE_1 c1,CTE_2 c2
where c1.col1 = c2.col1
....

Au début, chaque partie CTE est créée séparément. Ensuite, il progresse, au fur et à mesure que les CTE sont liés pour créer l'ensemble de résultats final de la requête.

Examinons maintenant un autre cas, en interrogeant une base de données Sales fictive. Nous voulons savoir quels produits, y compris la quantité et les ventes totales, ont été vendus dans chaque catégorie le mois précédent, et lesquels d'entre eux ont généré plus de ventes totales que le mois précédent.

Nous construisons notre requête en plusieurs parties CTE, où chaque partie fait référence à la précédente. Tout d'abord, nous construisons un ensemble de résultats pour répertorier les données détaillées dont nous avons besoin dans nos tables pour former le reste de la requête :

WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
)
select dt.*
from detailed_data dt.
order by dt.order_date desc, dt.category_name, dt.product_name

L'étape suivante consiste à résumer les données sur la quantité et les ventes totales par catégorie et par nom de produit :

WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
)
select ps.*
from product_sales ps
order by ps.year desc, ps.month desc, ps.category_name,ps.product_name

La dernière étape consiste à créer deux ensembles de résultats temporaires représentant les données du mois dernier et du mois précédent. Après cela, filtrez les données à renvoyer comme ensemble de résultats final :

WITH detailed_data as (
select o.order_date, c.category_name,p.product_name,oi.quantity, oi.listprice, oi.discount
from Orders o, Order_Item oi, Products p, Category c
where o.order_id = oi.order_id
and oi.product_id = p.product_id
and p.category_id = c.category_id
), product_sales as (
select year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name,sum(dt.quantity) total_quantity, sum(dt.listprice * (1 - dt.discount)) total_product_sales
from detailed_data dt
group by year(dt.order_date) year, month(dt.order_date) month, dt.category_name,dt.product_name
), last_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -1 
and ps.month = month(CURRENT_DATE) -1
), prev_month_data (
select ps.*
from product_sales ps.
where ps.year = year(CURRENT_DATE) -2
and ps.month = month(CURRENT_DATE) -2
)
select lmd.*
from last_month_data lmd, prev_month_data pmd
where lmd.category_name = pmd.category_name
and lmd.product_name = pmd.product_name
and ( lmd.total_quantity > pmd.total_quantity
or lmd.total_product_sales > pmd.total_product_sales )
order by lmd.year desc, lmd.month desc, lmd.category_name,lmd.product_name, lmd.total_product_sales desc, lmd.total_quantity desc

Notez que dans SQLServer, vous définissez getdate() au lieu de CURRENT_DATE.

De cette façon, nous pouvons échanger la dernière partie avec une sélection qui interroge les parties CTE individuelles pour voir le résultat d'une partie sélectionnée. En conséquence, nous pouvons rapidement déboguer le problème.

De plus, en exécutant une explication sur chaque partie CTE (et sur l'ensemble de la requête), nous estimons les performances de chaque partie et/ou de l'ensemble de la requête sur les tables et les données.

En conséquence, vous pouvez optimiser chaque partie en réécrivant et/ou en ajoutant des index appropriés aux tables concernées. Ensuite, vous expliquez l'ensemble de la requête pour voir le plan de requête final et procédez à l'optimisation si nécessaire.

Requêtes récursives utilisant la structure CTE

Une autre fonctionnalité utile de CTE est la création de requêtes récursives.

Les requêtes SQL récursives vous permettent de réaliser des choses que vous n'auriez pas imaginées possibles avec ce type de SQL et son débit. Vous pouvez résoudre de nombreux problèmes métier et même réécrire une logique SQL/application complexe en un simple appel SQL récursif à la base de données.

Il existe de légères variations dans la création de requêtes récursives entre les systèmes de base de données. Cependant, l'objectif est le même.

Quelques exemples de l'utilité du CTE récursif :

  1. Vous pouvez l'utiliser pour trouver des lacunes dans les données.
  2. Vous pouvez créer des organigrammes.
  3. Vous pouvez créer des données pré-calculées à utiliser ultérieurement dans une autre partie CTE
  4. Enfin, vous pouvez créer des données de test.

Le mot récursif dit tout. Vous avez une requête qui s'appelle à plusieurs reprises avec un point de départ, et, EXTRÊMEMENT IMPORTANT, un point de fin (une sortie de secours comme je l'appelle).

Si vous n'avez pas de sortie de sécurité, ou si votre formule récursive va au-delà, vous avez de gros problèmes. La requête ira dans une boucle infinie résultant en une utilisation CPU très élevée et une utilisation LOG très élevée. Cela entraînera un épuisement de la mémoire et/ou du stockage.

Si votre requête se détraque, vous devez réfléchir très vite pour la désactiver. Si vous ne pouvez pas le faire, alertez immédiatement votre administrateur de base de données afin qu'il empêche le système de base de données de s'étouffer et de tuer le thread incontrôlable.

Voir l'exemple :

with RECURSIVE mydates (level,nextdate) as (
select 1 level, FROM_UNIXTIME(RAND()*2147483647) nextdate from DUAL
union all 
select level+1, FROM_UNIXTIME(RAND()*2147483647) nextdate
from mydates
where level < 1000
)
SELECT nextdate from mydates
);

Cet exemple est une syntaxe CTE récursive MySQL/MariaDB. Avec lui, nous produisons mille dates aléatoires. Le niveau est notre compteur et notre sortie de sécurité pour quitter la requête récursive en toute sécurité.

Comme démontré, la ligne 2 est notre point de départ, tandis que les lignes 4-5 sont l'appel récursif avec le point final dans la clause WHERE (ligne 6). Les lignes 8 et 9 sont les appels pour exécuter la requête récursive et récupérer les données.

Autre exemple :

DECLARE @today as date;
DECLARE @1stjanprevyear as date;
select @today = DATEADD(DAY, 0, DATEDIFF(DAY, 0, getdate())),
   	@1stjanprevyear = DATEFROMPARTS(YEAR(GETDATE())-1, 1, 1) ;
WITH DatesCTE as (
   SELECT @1stjanprevyear  as CalendarDate
   UNION ALL
   SELECT dateadd(day , 1, CalendarDate) AS CalendarDate FROM DatesCTE
   WHERE dateadd (day, 1, CalendarDate) < @today
), MaxMinDates as (
SELECT Max(CalendarDate) MaxDate,Min(CalendarDate) MinDate
  FROM DatesCTE
)
SELECT i.*
FROM InvoiceTable i, MaxMinDates t
where i.INVOICE_DATE between t.MinDate and t.MaxDate
OPTION (MAXRECURSION 1000);

Cet exemple est une syntaxe SQLServer. Ici, nous laissons la partie DatesCTE produire toutes les dates entre aujourd'hui et le 1er janvier de l'année précédente. Nous l'utilisons pour renvoyer toutes les factures appartenant à ces dates.

Le point de départ est le @1stjanprevyear variable et la sortie de sécurité @today . Un maximum de 730 jours est possible. Ainsi, l'option de récursivité maximale est définie sur 1000 pour s'assurer qu'elle s'arrête.

Nous pourrions même ignorer les MaxMinDates partie et écrivez la partie finale, comme indiqué ci-dessous. Cela peut être une approche plus rapide, car nous avons une clause WHERE correspondante.

....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);

Conclusion

Au total, nous avons brièvement discuté et montré comment transformer une requête complexe en une requête structurée CTE. Lorsqu'une requête est divisée en différentes parties CTE, vous pouvez les utiliser dans d'autres parties et les appeler indépendamment dans la requête SQL finale à des fins de débogage.

Un autre point clé est que l'utilisation de CTE simplifie le débogage d'une requête complexe lorsqu'elle est divisée en parties gérables, pour renvoyer l'ensemble de résultats correct et attendu. Il est important de réaliser que l'exécution d'une explication sur chaque partie de la requête et sur l'ensemble de la requête est cruciale pour garantir que la requête et le SGBD s'exécutent de la manière la plus optimale possible.

J'ai également illustré l'écriture d'une puissante requête/partie CTE récursive dans la génération de données à la volée à utiliser ultérieurement dans une requête.

Notamment, lors de l'écriture d'une requête récursive, faites TRÈS attention à ne PAS oublier la sortie de sécurité . Assurez-vous de revérifier les calculs utilisés dans la sortie de sécurité pour produire un signal d'arrêt et/ou utiliser la maxrecursion option fournie par SQLServer.

De même, d'autres SGBD peuvent soit utiliser cte_max_recursion_depth (MySQL 8.0) ou max_recursive_iterations (MariaDB 10.3) comme exits de sécurité supplémentaires.

Lire aussi

Tout ce que vous devez savoir sur SQL CTE en un seul endroit