Často vidíme špatně napsané složité SQL dotazy běžící proti tabulce nebo tabulkám v databázích. Tyto dotazy velmi prodlužují dobu provádění a spotřebovávají obrovské množství CPU a dalších zdrojů. Složité dotazy však v mnoha případech poskytují aplikaci/osobě, která je spouští, cenné informace. Proto jsou užitečnými prostředky ve všech variantách aplikací.
Složité dotazy se obtížně ladí
Pokud se blíže podíváme na problematické dotazy, mnohé z nich jsou složité, zejména ty specifické, které se používají v přehledech.
Složité dotazy se často skládají z pěti nebo více velkých tabulek a jsou spojeny mnoha dílčími dotazy. Každý dílčí dotaz má klauzuli WHERE, která provádí jednoduché až složité výpočty a/nebo transformace dat a zároveň spojuje příslušné sloupce tabulek dohromady.
Takové dotazy mohou být náročné na ladění, aniž by spotřebovaly velké množství zdrojů. Důvodem je, že je obtížné určit, zda každý dílčí dotaz a/nebo spojené dílčí dotazy poskytují správné výsledky.
Typický scénář je:zavolají vám pozdě v noci, aby vyřešili problém na vytíženém databázovém serveru se složitým dotazem a vy jej potřebujete rychle opravit. Jako vývojář nebo DBA můžete mít v pozdních hodinách k dispozici velmi omezený čas a systémové prostředky. První věc, kterou potřebujete, je plán, jak odladit problematický dotaz.
Někdy postup ladění probíhá dobře. Někdy to vyžaduje spoustu času a úsilí, než dosáhnete cíle a problém vyřešíte.
Psaní dotazů ve struktuře CTE
Ale co kdyby existoval způsob, jak psát složité dotazy, aby je bylo možné rychle odladit, kousek po kousku?
Existuje takový způsob. Říká se tomu Common Table Expression nebo CTE.
Common Table Expression je standardní funkce ve většině moderních databází, jako je SQLServer, MySQL (od verze 8.0), MariaDB (verze 10.2.1), Db2 a Oracle. Má jednoduchou strukturu, která zapouzdřuje jeden nebo více dílčích dotazů do dočasné pojmenované sady výsledků. Tuto sadu výsledků můžete dále použít v jiných pojmenovaných CTE nebo dílčích dotazech.
Společný tabulkový výraz je do určité míry ZOBRAZENÍ, které pouze existuje a na které se dotaz odkazuje v době provádění.
Transformace složitého dotazu na dotaz ve stylu CTE vyžaduje určité strukturované myšlení. Totéž platí pro OOP se zapouzdřením při přepisování složitého dotazu do struktury CTE.
Musíte přemýšlet o:
- Každou sadu dat, kterou získáváte z každé tabulky.
- Jak jsou spojeny k zapouzdření nejbližších dílčích dotazů do jedné dočasně pojmenované sady výsledků.
Opakujte to pro každý dílčí dotaz a sadu zbývajících dat, dokud nedosáhnete konečného výsledku dotazu. Všimněte si, že každá dočasná pojmenovaná sada výsledků je také dílčím dotazem.
Poslední částí dotazu by měl být velmi „jednoduchý“ výběr, vracející konečný výsledek do aplikace. Jakmile dosáhnete této poslední části, můžete ji vyměnit za dotaz, který vybere data z individuálně pojmenované dočasné sady výsledků.
Tímto způsobem se ladění každé dočasné sady výsledků stává snadným úkolem.
Abychom pochopili, jak můžeme vytvářet naše dotazy od jednoduchých po složité, podívejme se na strukturu CTE. Nejjednodušší forma je následující:
WITH CTE_1 as (
select .... from some_table where ...
)
select ... from CTE_1
where ...
Zde CTE_1 je jedinečný název, který přiřadíte dočasné pojmenované sadě výsledků. Výsledných sad může být tolik, kolik je potřeba. Tím se formulář rozšiřuje na, jak je znázorněno níže:
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
....
Nejprve je každá část CTE vytvořena samostatně. Poté postupuje, jak jsou CTE propojeny, aby se vytvořila konečná sada výsledků dotazu.
Nyní se podívejme na další případ, dotazujeme se na fiktivní databázi prodeje. Chceme vědět, jaké produkty, včetně množství a celkových prodejů, se prodaly v jednotlivých kategoriích předchozí měsíc a které z nich zaznamenaly vyšší celkové prodeje než měsíc před tím.
Náš dotaz konstruujeme do několika částí CTE, kde každá část odkazuje na tu předchozí. Nejprve vytvoříme sadu výsledků, která vypíše podrobná data, která potřebujeme z našich tabulek k vytvoření zbytku dotazu:
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
Dalším krokem je shrnout údaje o množství a celkovém prodeji podle jednotlivých kategorií a názvů produktů:
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
Posledním krokem je vytvoření dvou dočasných sad výsledků představujících data za poslední měsíc a za předchozí měsíc. Poté odfiltrujte data, která mají být vrácena jako konečná sada výsledků:
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
Všimněte si, že v SQLServeru nastavíte getdate() místo CURRENT_DATE.
Tímto způsobem můžeme vyměnit poslední část za výběr, který se dotazuje na jednotlivé části CTE, abychom viděli výsledek vybrané části. V důsledku toho můžeme problém rychle odladit.
Spuštěním vysvětlení pro každou část CTE (a celý dotaz) také odhadneme, jak dobře si každá část a/nebo celý dotaz povede v tabulkách a datech.
V souladu s tím můžete optimalizovat každou část přepsáním a/nebo přidáním správných indexů do příslušných tabulek. Poté vysvětlíte celý dotaz, abyste viděli konečný plán dotazů a v případě potřeby pokračujte v optimalizaci.
Rekurzivní dotazy pomocí struktury CTE
Další užitečnou funkcí CTE je vytváření rekurzivních dotazů.
Rekurzivní dotazy SQL vám umožňují dosáhnout věcí, které byste si s tímto typem SQL a jeho rychlostí nepředstavovali. Můžete vyřešit mnoho obchodních problémů a dokonce přepsat složitou logiku SQL/aplikace na jednoduché rekurzivní volání SQL do databáze.
Existují drobné rozdíly ve vytváření rekurzivních dotazů mezi databázovými systémy. Cíl je však stejný.
Několik příkladů užitečnosti rekurzivního CTE:
- Můžete jej použít k nalezení mezer v datech.
- Můžete vytvářet organizační schémata.
- Můžete vytvořit předem vypočítaná data pro další použití v jiné části CTE
- Nakonec můžete vytvořit testovací data.
Slovo rekurzivní říká vše. Máte dotaz, který se opakovaně nazývá s nějakým počátečním bodem, a VELMI DŮLEŽITÉ, koncový bod (bezpečný východ jak tomu říkám).
Pokud nemáte bezpečný východ nebo váš rekurzivní vzorec jde za něj, máte velké potíže. Dotaz přejde do nekonečnésmyčky což vede k velmi vysokému CPU a velmi vysokému využití LOG. To povede k vyčerpání paměti a/nebo úložiště.
Pokud váš dotaz selže, musíte velmi rychle přemýšlet, abyste jej zakázali. Pokud to nemůžete udělat, okamžitě upozorněte svého správce databází, aby zabránili udušení databázového systému a zabití nekontrolovaného vlákna.
Viz příklad:
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
);
Tento příklad je rekurzivní syntaxí CTE MySQL/MariaDB. S ním vyrobíme tisíc náhodných dat. Úroveň je náš čítač a bezpečný výstup pro bezpečné ukončení rekurzivního dotazu.
Jak bylo ukázáno, řádek 2 je naším výchozím bodem, zatímco řádky 4-5 jsou rekurzivní volání s koncovým bodem v klauzuli WHERE (řádek 6). Řádky 8 a 9 jsou volání při provádění rekurzivního dotazu a načítání dat.
Další příklad:
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);
Tento příklad je syntaxí SQLServeru. Zde necháme část DatesCTE vytvořit všechna data mezi dneškem a 1. lednem předchozího roku. Používáme jej k vrácení všech faktur náležejících k těmto datům.
Výchozím bodem je @1stjanprevyear proměnná a bezpečný konec @dnes . Je možné maximálně 730 dní. Proto je možnost maximální rekurze nastavena na 1000, aby se zajistilo, že se zastaví.
Mohli bychom dokonce přeskočit MaxMinDates část a napište závěrečnou část, jak je uvedeno níže. Může to být rychlejší přístup, protože máme odpovídající klauzuli WHERE.
....
SELECT i.*
FROM InvoiceTable i, DatesCTE t
where i.INVOICE_DATE = t.CalendarDate
OPTION (MAXRECURSION 1000);
Závěr
Celkově jsme stručně probrali a ukázali, jak převést složitý dotaz na CTE strukturovaný dotaz. Když je dotaz rozdělen do různých částí CTE, můžete je použít v jiných částech a volat nezávisle v konečném dotazu SQL pro účely ladění.
Dalším klíčovým bodem je, že použití CTE usnadňuje ladění složitého dotazu, když je rozdělen na zvládnutelné části, aby se vrátila správná a očekávaná sada výsledků. Je důležité si uvědomit, že spuštění vysvětlení pro každou část dotazu a celý dotaz je klíčové pro zajištění toho, aby dotaz a DBMS běžely co nejoptimálněji.
Také jsem ilustroval psaní výkonného rekurzivního dotazu/části CTE při generování dat za běhu, které lze dále použít v dotazu.
Zejména při psaní rekurzivního dotazu buďte VELMI opatrní, abyste NEzapomněli na bezpečný konec . Ujistěte se, že jste znovu zkontrolovali výpočty použité v bezpečném východu pro vytvoření signálu zastavení a/nebo použijte maxrecursion možnost, kterou SQLServer poskytuje.
Podobně mohou ostatní DBMS buď používat cte_max_recursion_depth (MySQL 8.0) nebo max_recursive_iterations (MariaDB 10.3) jako další bezpečné východy.
Přečtěte si také
Vše, co potřebujete vědět o SQL CTE na jednom místě