Vi ser ofta dåligt skrivna komplexa SQL-frågor som körs mot en tabell eller tabeller i databaser. Dessa frågor gör körningstiden mycket lång och förbrukar enorma CPU- och andra resurser. Ändå ger komplexa frågor värdefull information till applikationen/personen som kör dem i många fall. Därför är de användbara tillgångar i alla varianter av applikationer.
Komplexa frågor är svåra att felsöka
Om vi tittar noga på problematiska frågor är många av dem komplexa, särskilt de specifika som används i rapporter.
Komplexa frågor består ofta av fem eller fler stora tabeller och sammanfogas av många underfrågor. Varje underfråga har en WHERE-sats som utför enkla till komplexa beräkningar och/eller datatransformationer samtidigt som de relevanta kolumntabellerna sammanfogas.
Sådana frågor kan bli utmanande att felsöka utan att förbruka mycket resurser. Anledningen är att det är svårt att avgöra om varje delfråga och/eller sammanfogade delfrågor ger korrekta resultat.
Ett typiskt scenario är:de ringer dig sent på kvällen för att lösa ett problem på en upptagen databasserver med en komplex fråga inblandad, och du måste åtgärda det snabbt. Som utvecklare eller DBA kan du ha mycket begränsade tid- och systemresurser tillgängliga på en sen timme. Det första du behöver är alltså en plan för hur man felsöker den problematiska frågan.
Ibland går felsökningsproceduren bra. Ibland tar det mycket tid och ansträngning innan du når målet och löser problemet.
Skriva frågor i CTE-struktur
Men tänk om det fanns ett sätt att skriva komplexa frågor så att man kunde felsöka dem snabbt, bit för bit?
Det finns ett sådant sätt. Det kallas Common Table Expression eller CTE.
Common Table Expression är en standardfunktion i de flesta moderna databaser som SQLServer, MySQL (från och med version 8.0), MariaDB (version 10.2.1), Db2 och Oracle. Den har en enkel struktur som kapslar in en eller flera underfrågor i en temporär namngiven resultatuppsättning. Du kan använda denna resultatuppsättning vidare i andra namngivna CTE:er eller underfrågor.
Ett vanligt tabelluttryck är, till viss del, en VIEW som endast existerar och refereras till av frågan vid tidpunkten för exekvering.
Att omvandla en komplex fråga till en fråga i CTE-stil kräver en del strukturerat tänkande. Detsamma gäller OOP med inkapsling vid omskrivning av en komplex fråga till en CTE-struktur.
Du måste tänka på:
- Varje uppsättning data som du hämtar från varje tabell.
- Hur de sammanfogas för att kapsla in de närmaste underfrågorna i en temporär namngiven resultatuppsättning.
Upprepa det för varje underfråga och uppsättning data som återstår tills du når det slutliga resultatet av frågan. Observera att varje temporärt namngiven resultatuppsättning också är en underfråga.
Den sista delen av frågan bör vara ett mycket "enkelt" val som returnerar det slutliga resultatet till applikationen. När du har nått denna sista del kan du byta ut den med en fråga som väljer data från en individuellt namngiven tillfällig resultatuppsättning.
På så sätt blir felsökningen av varje tillfällig resultatuppsättning ett enkelt jobb.
För att förstå hur vi kan bygga våra frågor från enkla till komplexa, låt oss titta på CTE-strukturen. Den enklaste formen är följande:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Här CTE_1 är ett unikt namn du ger till den tillfälliga namngivna resultatuppsättningen. Det kan finnas hur många resultat som behövs. Därmed sträcker sig formuläret till, som visas nedan:
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
....
Till en början skapas varje CTE-del separat. Sedan fortskrider det, eftersom CTE:er länkas samman för att bygga upp den slutliga resultatuppsättningen av frågan.
Låt oss nu undersöka ett annat fall, fråga efter en fiktiv försäljningsdatabas. Vi vill veta vilka produkter, inklusive kvantitet och total försäljning, som såldes i varje kategori föregående månad, och vilka av dem som fick mer total försäljning än månaden innan dess.
Vi konstruerar vår fråga i flera CTE-delar, där varje del refererar till den föregående. Först konstruerar vi en resultatuppsättning för att lista ut de detaljerade data vi behöver från våra tabeller för att bilda resten av frågan:
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
Nästa steg är att sammanfatta kvantiteten och totala försäljningsdata efter varje kategori och produktnamn:
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 sista steget är att skapa två tillfälliga resultatuppsättningar som representerar den senaste månadens och föregående månads data. Efter det, filtrera bort data som ska returneras som den slutliga resultatuppsättningen:
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
Observera att i SQLServer ställer du in getdate() istället för CURRENT_DATE.
På så sätt kan vi byta ut den sista delen med ett urval som frågar enskilda CTE-delar för att se resultatet av en vald del. Som ett resultat kan vi snabbt felsöka problemet.
Genom att köra en förklaring på varje CTE-del (och hela frågan) uppskattar vi hur väl varje del och/eller hela frågan kommer att prestera på tabellerna och data.
På motsvarande sätt kan du optimera varje del genom att skriva om och/eller lägga till korrekta index i de inblandade tabellerna. Sedan förklarar du hela frågan för att se den slutliga frågeplanen och fortsätter med optimering om det behövs.
Rekursiva frågor med CTE-struktur
En annan användbar funktion i CTE är att skapa rekursiva frågor.
Rekursiva SQL-frågor låter dig uppnå saker du inte kan föreställa dig vara möjliga med den här typen av SQL och dess hastighet. Du kan lösa många affärsproblem och till och med skriva om lite komplex SQL/applikationslogik till ett enkelt rekursivt SQL-anrop till databasen.
Det finns små variationer i att skapa rekursiva frågor mellan databassystem. Målet är dock detsamma.
Några exempel på användbarheten av rekursiv CTE:
- Du kan använda den för att hitta luckor i data.
- Du kan skapa organisationsscheman.
- Du kan skapa förberäknad data att använda vidare i en annan CTE-del
- Äntligen kan du skapa testdata.
Ordet rekursiv säger allt. Du har en fråga som upprepade gånger anropar sig själv med en utgångspunkt, och EXTREMT VIKTIGT, en slutpunkt (en felsäker utgång som jag kallar det).
Om du inte har en felsäker utgång, eller din rekursiva formel går utöver den, är du i djupa problem. Frågan går in i en oändligslinga vilket resulterar i mycket hög CPU och mycket hög LOG-användning. Det kommer att leda till minne och/eller lagringsutmattning.
Om din fråga går galet måste du tänka mycket snabbt för att inaktivera den. Om du inte kan göra det, varna din DBA omedelbart, så att de förhindrar att databassystemet kvävs och dödar den skenande tråden.
Se exemplet:
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
);
Det här exemplet är en MySQL/MariaDB rekursiv CTE-syntax. Med den producerar vi tusen slumpmässiga datum. Nivån är vår räknare och felsäkra utgång för att lämna den rekursiva frågan på ett säkert sätt.
Som visats är linje 2 vår startpunkt, medan raderna 4-5 är det rekursiva anropet med slutpunkten i WHERE-satsen (rad 6). Raderna 8 och 9 är anropen för att utföra den rekursiva frågan och hämta data.
Ett annat exempel:
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);
Det här exemplet är en SQLServer-syntax. Här låter vi DatesCTE-delen producera alla datum mellan idag och 1 januari föregående år. Vi använder den för att returnera alla fakturor som hör till dessa datum.
Utgångspunkten är @1stjanprevyear variabeln och den felsäkra utgången @today . Högst 730 dagar är möjligt. Således är det maximala rekursionsalternativet satt till 1000 för att se till att det stoppar.
Vi kan till och med hoppa över MaxMinDates del och skriv den sista delen, som visas nedan. Det kan vara ett snabbare tillvägagångssätt, eftersom vi har en matchande WHERE-klausul.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Slutsats
Sammantaget har vi kort diskuterat och visat hur man omvandlar en komplex fråga till en CTE-strukturerad fråga. När en fråga är uppdelad i olika CTE-delar kan du använda dem i andra delar och anropa den slutliga SQL-frågan oberoende av varandra i felsökningssyfte.
En annan viktig punkt är att användningen av CTE gör det enklare att felsöka en komplex fråga när den är uppdelad i hanterbara delar, för att returnera den korrekta och förväntade resultatuppsättningen. Det är viktigt att inse att det är avgörande att köra en förklaring på varje frågedel och hela frågan för att säkerställa att frågan och DBMS körs så optimalt som möjligt.
Jag har också illustrerat att skriva en kraftfull rekursiv CTE-fråga/del för att generera data i farten för att använda vidare i en fråga.
Särskilt när du skriver en rekursiv fråga, var MYCKET försiktig så att du INTE glömmer den felsäkra utgången . Se till att dubbelkolla beräkningarna som används i den felsäkra utgången för att producera en stoppsignal och/eller använd maxrekursionen alternativ som SQLServer tillhandahåller.
På liknande sätt kan andra DBMS antingen använda cte_max_recursion_depth (MySQL 8.0) eller max_rekursiva_iterationer (MariaDB 10.3) som ytterligare felsäkra utgångar.
Läs också
Allt du behöver veta om SQL CTE på ett ställe