sql >> Database teknologi >  >> RDS >> Database

Hvordan CTE kan hjælpe med at skrive komplekse, kraftfulde forespørgsler:et præstationsperspektiv

Vi ser ofte dårligt skrevne komplekse SQL-forespørgsler, der kører mod en eller flere tabeller i databaser. Disse forespørgsler gør udførelsestiden meget lang og forårsager forbrug af enorme CPU og andre ressourcer. Alligevel giver komplekse forespørgsler værdifuld information til applikationen/personen, der kører dem i mange tilfælde. Derfor er de nyttige aktiver i alle varianter af applikationer.

Komplekse forespørgsler er svære at fejlfinde

Hvis vi ser nærmere på problematiske forespørgsler, er mange af dem komplekse, især de specifikke, der bruges i rapporter.

Komplekse forespørgsler består ofte af fem eller flere store tabeller og er forbundet med mange underforespørgsler. Hver underforespørgsel har en WHERE-sætning, der udfører enkle til komplekse beregninger og/eller datatransformationer, mens de sammenføjer de relevante kolonners tabeller.

Sådanne forespørgsler kan blive udfordrende at fejlfinde uden at forbruge mange ressourcer. Årsagen er, at det er vanskeligt at afgøre, om hver underforespørgsel og/eller sammenkoblede underforespørgsler giver korrekte resultater.

Et typisk scenarie er:de ringer til dig sent om aftenen for at løse et problem på en travl databaseserver med en kompleks forespørgsel involveret, og du skal løse det hurtigt. Som udvikler eller DBA kan du have meget begrænsede tid og systemressourcer til rådighed på et sent tidspunkt. Derfor er det første, du skal bruge, en plan for, hvordan du fejlretter den problematiske forespørgsel.

Nogle gange går fejlfindingsproceduren godt. Nogle gange tager det masser af tid og kræfter, før du når målet og løser problemet.

Skrivning af forespørgsler i CTE-struktur

Men hvad nu hvis der var en måde at skrive komplekse forespørgsler på, så man hurtigt kunne debugge dem, stykke for stykke?

Der er sådan en måde. Det kaldes Common Table Expression eller CTE.

Common Table Expression er en standardfunktion i de fleste moderne databaser som SQLServer, MySQL (fra version 8.0), MariaDB (version 10.2.1), Db2 og Oracle. Det har en simpel struktur, der indkapsler en eller mange underforespørgsler i et midlertidigt navngivet resultatsæt. Du kan bruge dette resultatsæt yderligere i andre navngivne CTE'er eller underforespørgsler.

Et almindeligt tabeludtryk er til en vis grad en VIEW, der kun eksisterer og refereres til af forespørgslen på tidspunktet for udførelsen.

At transformere en kompleks forespørgsel til en forespørgsel i CTE-stil kræver noget struktureret tænkning. Det samme gælder OOP med indkapsling, når en kompleks forespørgsel omskrives til en CTE-struktur.

Du skal tænke på:

  1. Hvert sæt data, du trækker fra hver tabel.
  2. Hvordan de sammenføjes for at indkapsle de nærmeste underforespørgsler i ét midlertidigt navngivet resultatsæt.

Gentag det for hver underforespørgsel og sæt af data, der er tilbage, indtil du når det endelige resultat af forespørgslen. Bemærk, at hvert midlertidigt navngivet resultatsæt også er en underforespørgsel.

Den sidste del af forespørgslen skal være et meget "simpelt" valg, der returnerer det endelige resultat til applikationen. Når du har nået denne sidste del, kan du udveksle den med en forespørgsel, der vælger data fra et individuelt navngivet midlertidigt resultatsæt.

På denne måde bliver fejlretningen af ​​hvert midlertidigt resultatsæt en nem opgave.

For at forstå, hvordan vi kan bygge vores forespørgsler fra simple til komplekse, lad os se på CTE-strukturen. Den enkleste form er som følger:

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

Her CTE_1 er et unikt navn, du giver til det midlertidige navngivne resultatsæt. Der kan være så mange resultatsæt som nødvendigt. Dermed strækker formularen sig til, som vist nedenfor:

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
....

Først oprettes hver CTE-del separat. Derefter skrider det frem, da CTE'er er knyttet sammen for at opbygge det endelige resultatsæt af forespørgslen.

Lad os nu undersøge en anden sag, forespørge på en fiktiv salgsdatabase. Vi vil gerne vide, hvilke produkter, inklusive mængde og samlet salg, der blev solgt i hver kategori den foregående måned, og hvilke af dem der fik det største salg end måneden før det.

Vi konstruerer vores forespørgsel i flere CTE-dele, hvor hver del refererer til den foregående. Først konstruerer vi et resultatsæt, der viser de detaljerede data, vi har brug for fra vores tabeller for at danne resten af ​​forespørgslen:

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

Det næste trin er at opsummere mængden og de samlede salgsdata efter hver kategori og produktnavne:

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

Det sidste trin er at oprette to midlertidige resultatsæt, der repræsenterer den sidste måneds og den foregående måneds data. Filtrer derefter de data fra, der skal returneres som det endelige resultatsæt:

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

Bemærk, at du i SQLServer indstiller getdate() i stedet for CURRENT_DATE.

På denne måde kan vi udveksle den sidste del med et udvalg, der forespørger individuelle CTE-dele for at se resultatet af en valgt del. Som et resultat kan vi hurtigt fejlfinde problemet.

Ved at udføre en forklaring på hver CTE-del (og hele forespørgslen), estimerer vi også, hvor godt hver del og/eller hele forespørgslen vil præstere på tabellerne og dataene.

Tilsvarende kan du optimere hver del ved at omskrive og/eller tilføje korrekte indekser til de involverede tabeller. Derefter forklarer du hele forespørgslen for at se den endelige forespørgselsplan og fortsætter med optimering, hvis det er nødvendigt.

Rekursive forespørgsler ved hjælp af CTE-struktur

En anden nyttig funktion ved CTE er at oprette rekursive forespørgsler.

Rekursive SQL-forespørgsler giver dig mulighed for at opnå ting, du ikke ville forestille dig muligt med denne type SQL og dens hastighed. Du kan løse mange forretningsproblemer og endda omskrive noget kompleks SQL/applikationslogik ned til et simpelt rekursivt SQL-kald til databasen.

Der er små variationer i at skabe rekursive forespørgsler mellem databasesystemer. Målet er dog det samme.

Et par eksempler på nytten af ​​rekursiv CTE:

  1. Du kan bruge det til at finde huller i data.
  2. Du kan oprette organisationsdiagrammer.
  3. Du kan oprette forudberegnet data til at bruge yderligere i en anden CTE-del
  4. Til sidst kan du oprette testdata.

Ordet rekursiv siger det hele. Du har en forespørgsel, der gentagne gange kalder sig selv med et eller andet udgangspunkt, og EKSTREMT VIGTIGT, et slutpunkt (en fejlsikker udgang som jeg kalder det).

Hvis du ikke har en fejlsikker exit, eller din rekursive formel går ud over det, er du i dybe problemer. Forespørgslen vil gå ind i en uendeligløkke hvilket resulterer i meget høj CPU og meget høj LOG-udnyttelse. Det vil føre til udmattelse af hukommelse og/eller lagerplads.

Hvis din forespørgsel går galt, skal du tænke meget hurtigt for at deaktivere den. Hvis du ikke kan gøre det, så advare din DBA med det samme, så de forhindrer databasesystemet i at kvæle og dræber den løbske tråd.

Se eksemplet:

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
);

Dette eksempel er en MySQL/MariaDB rekursiv CTE-syntaks. Med det producerer vi tusind tilfældige datoer. Niveauet er vores tæller og fejlsikre exit for at afslutte den rekursive forespørgsel sikkert.

Som vist er linje 2 vores udgangspunkt, mens linje 4-5 er det rekursive kald med slutpunktet i WHERE-sætningen (linje 6). Linje 8 og 9 er opkaldene til at udføre den rekursive forespørgsel og hente dataene.

Et andet eksempel:

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);

Dette eksempel er en SQLServer-syntaks. Her lader vi DatesCTE-delen producere alle datoer mellem i dag og 1. januar det foregående år. Vi bruger det til at returnere alle fakturaer, der tilhører disse datoer.

Udgangspunktet er @1stjanprevyear variabel og den fejlsikre exit @today . Der er maksimalt mulighed for 730 dage. Således er den maksimale rekursionsindstilling sat til 1000 for at sikre, at den stopper.

Vi kunne endda springe MaxMinDates over del og skriv den sidste del, som vist nedenfor. Det kan være en hurtigere tilgang, da vi har en matchende WHERE-klausul.

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

Konklusion

Sammenlagt har vi kort diskuteret og vist, hvordan man transformerer en kompleks forespørgsel til en CTE-struktureret forespørgsel. Når en forespørgsel er opdelt i forskellige CTE-dele, kan du bruge dem i andre dele og kalde uafhængigt i den endelige SQL-forespørgsel til fejlfindingsformål.

Et andet vigtigt punkt er, at brugen af ​​CTE gør det nemmere at debugge en kompleks forespørgsel, når den er opdelt i håndterbare dele, for at returnere det korrekte og forventede resultatsæt. Det er vigtigt at indse, at det at køre en forklaring på hver forespørgselsdel og hele forespørgslen er afgørende for at sikre, at forespørgslen og DBMS kører så optimalt som muligt.

Jeg har også illustreret, hvordan jeg skriver en kraftfuld rekursiv CTE-forespørgsel/del i generering af data i farten, som kan bruges videre i en forespørgsel.

Især når du skriver en rekursiv forespørgsel, vær MEGET forsigtig med IKKE at glemme den fejlsikre exit . Sørg for at dobbelttjekke de beregninger, der bruges i den fejlsikre udgang for at frembringe et stopsignal og/eller bruge maxrekursionen mulighed, som SQLServer tilbyder.

På samme måde kan andre DBMS enten bruge cte_max_recursion_depth (MySQL 8.0) eller max_recursive_iterations (MariaDB 10.3) som yderligere fejlsikre udgange.

Læs også

Alt du behøver at vide om SQL CTE på ét sted


  1. Sådan opretter du en visning i SQL Server

  2. Sådan fungerer TIME_TO_SEC() i MariaDB

  3. Hvad er forskellene mellem SQL og MySQL

  4. Rails, PostgreSQL og History Triggere