sql >> Database >  >> RDS >> Database

Hoe CTE kan helpen bij het schrijven van complexe, krachtige zoekopdrachten:een prestatieperspectief

We zien vaak slecht geschreven complexe SQL-query's draaien tegen een tabel of tabellen in databases. Die query's maken de uitvoeringstijd erg lang en veroorzaken enorme CPU- en andere bronnen. Toch leveren complexe zoekopdrachten in veel gevallen waardevolle informatie op voor de applicatie/persoon die ze uitvoert. Daarom zijn ze nuttige troeven in allerlei toepassingen.

Complexe zoekopdrachten zijn moeilijk te debuggen

Als we goed kijken naar problematische zoekopdrachten, zijn veel ervan complex, vooral die specifieke die in rapporten worden gebruikt.

Complexe query's bestaan ​​vaak uit vijf of meer grote tabellen en worden samengevoegd door vele subquery's. Elke subquery heeft een WHERE-clausule die eenvoudige tot complexe berekeningen en/of gegevenstransformaties uitvoert terwijl de relevante tabellen in de kolommen worden samengevoegd.

Dergelijke query's kunnen een uitdaging worden om te debuggen zonder veel bronnen te verbruiken. De reden is dat het moeilijk is om te bepalen of elke subquery en/of gekoppelde subquery's de juiste resultaten opleveren.

Een typisch scenario is:ze bellen je 's avonds laat om een ​​probleem op te lossen op een drukke databaseserver met een complexe vraag, en je moet het snel oplossen. Als ontwikkelaar of DBA heb je op een laat uur misschien zeer beperkte tijd en systeembronnen beschikbaar. Het eerste dat u dus nodig hebt, is een plan om de problematische query te debuggen.

Soms gaat de foutopsporingsprocedure goed. Soms kost het veel tijd en moeite voordat je het doel bereikt en het probleem oplost.

Query's schrijven in CTE-structuur

Maar wat als er een manier was om complexe query's te schrijven, zodat je ze snel, stuk voor stuk, kon debuggen?

Er is zo'n manier. Het wordt Common Table Expression of CTE genoemd.

Common Table Expression is een standaardfunctie in de meeste moderne databases zoals SQLServer, MySQL (vanaf versie 8.0), MariaDB (versie 10.2.1), Db2 en Oracle. Het heeft een eenvoudige structuur die een of meerdere subquery's inkapselt in een tijdelijke benoemde resultatenset. U kunt deze resultatenset verder gebruiken in andere genoemde CTE's of subquery's.

Een Common Table Expression is tot op zekere hoogte een VIEW die alleen bestaat en waarnaar wordt verwezen door de query op het moment van uitvoering.

Het transformeren van een complexe query naar een CTE-query vereist enig gestructureerd denken. Hetzelfde geldt voor OOP met inkapseling bij het herschrijven van een complexe query in een CTE-structuur.

Je moet nadenken over:

  1. Elke set gegevens die u uit elke tabel haalt.
  2. Hoe ze worden samengevoegd om de dichtstbijzijnde subquery's in te kapselen in één tijdelijke benoemde resultatenset.

Herhaal dit voor elke subquery en set van resterende gegevens totdat u het uiteindelijke resultaat van de query bereikt. Merk op dat elke tijdelijke benoemde resultaatset ook een subquery is.

Het laatste deel van de zoekopdracht moet een zeer "eenvoudige" selectie zijn, waarbij het uiteindelijke resultaat naar de toepassing wordt teruggestuurd. Zodra je dit laatste deel hebt bereikt, kun je het uitwisselen met een query die de gegevens selecteert uit een individueel benoemde tijdelijke resultatenset.

Op deze manier wordt het debuggen van elke tijdelijke resultatenset een gemakkelijke klus.

Laten we eens kijken naar de CTE-structuur om te begrijpen hoe we onze zoekopdrachten van eenvoudig naar complex kunnen opbouwen. De eenvoudigste vorm is als volgt:

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

Hier CTE_1 is een unieke naam die u aan de tijdelijke benoemde resultaatset geeft. Er kunnen zoveel resultatensets zijn als nodig is. Daarmee strekt het formulier zich uit tot, zoals hieronder weergegeven:

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

In eerste instantie wordt elk CTE-onderdeel afzonderlijk gemaakt. Daarna vordert het, aangezien de CTE's aan elkaar worden gekoppeld om de uiteindelijke resultatenset van de zoekopdracht op te bouwen.

Laten we nu een ander geval onderzoeken, een fictieve verkoopdatabase doorzoeken. We willen weten welke producten, inclusief hoeveelheid en totale verkoop, de vorige maand in elke categorie zijn verkocht en welke van hen meer totale verkopen hebben dan de maand ervoor.

We bouwen onze query op in verschillende CTE-onderdelen, waarbij elk onderdeel verwijst naar het vorige. Eerst stellen we een resultatenset samen om de gedetailleerde gegevens op te sommen die we nodig hebben uit onze tabellen om de rest van de query te vormen:

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

De volgende stap is het samenvatten van de hoeveelheid en totale verkoopgegevens per categorie en productnamen:

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

De laatste stap is het maken van twee tijdelijke resultatensets die de gegevens van de afgelopen maand en de voorgaande maand vertegenwoordigen. Filter daarna de gegevens die moeten worden geretourneerd als de uiteindelijke resultaatset:

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

Merk op dat je in SQLServer getdate() instelt in plaats van CURRENT_DATE.

Op deze manier kunnen we het laatste onderdeel uitwisselen met een select die individuele CTE-onderdelen bevraagt ​​om het resultaat van een geselecteerd onderdeel te zien. Als gevolg hiervan kunnen we het probleem snel debuggen.

Door een uitleg uit te voeren op elk CTE-onderdeel (en de hele zoekopdracht), schatten we ook hoe goed elk onderdeel en/of de hele zoekopdracht zal presteren op de tabellen en gegevens.

Dienovereenkomstig kunt u elk onderdeel optimaliseren door de juiste indexen te herschrijven en/of toe te voegen aan de betrokken tabellen. Vervolgens legt u de hele query uit om het uiteindelijke queryplan te zien en gaat u indien nodig verder met optimalisatie.

Recursieve zoekopdrachten met CTE-structuur

Een andere handige functie van CTE is het maken van recursieve zoekopdrachten.

Met recursieve SQL-query's kunt u dingen bereiken die u met dit type SQL en zijn snelheid niet voor mogelijk zou houden. U kunt veel zakelijke problemen oplossen en zelfs een aantal complexe SQL-/toepassingslogica herschrijven tot een eenvoudige recursieve SQL-aanroep naar de database.

Er zijn kleine verschillen in het maken van recursieve query's tussen databasesystemen. Het doel is echter hetzelfde.

Een paar voorbeelden van het nut van recursieve CTE:

  1. Je kunt het gebruiken om hiaten in gegevens te vinden.
  2. U kunt organigrammen maken.
  3. U kunt vooraf berekende gegevens maken om verder te gebruiken in een ander CTE-onderdeel
  4. Ten slotte kunt u testgegevens maken.

Het woord recursief dat zegt alles. Je hebt een vraag die zichzelf herhaaldelijk aanroept met een beginpunt, en, EXTREEM BELANGRIJK, een eindpunt (een fail-safe exit zoals ik het noem).

Als je geen fail-safe exit hebt, of als je recursieve formule verder gaat, heb je grote problemen. De query gaat in een oneindige lus wat resulteert in een zeer hoge CPU en een zeer hoog LOG-gebruik. Het zal leiden tot geheugen- en/of opslaguitputting.

Als uw vraag in de war raakt, moet u heel snel denken om deze uit te schakelen. Als u dit niet kunt doen, waarschuw dan onmiddellijk uw DBA, zodat ze voorkomen dat het databasesysteem verstikt en de op hol geslagen thread doodt.

Zie het voorbeeld:

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

Dit voorbeeld is een recursieve CTE-syntaxis van MySQL/MariaDB. Hiermee produceren we duizend willekeurige datums. Het niveau is onze teller en een fail-safe exit om de recursieve query veilig af te sluiten.

Zoals aangetoond, is regel 2 ons startpunt, terwijl regels 4-5 de recursieve aanroep zijn met het eindpunt in de WHERE-clausule (regel 6). Regels 8 en 9 zijn de aanroepen bij het uitvoeren van de recursieve query en het ophalen van de gegevens.

Nog een voorbeeld:

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

Dit voorbeeld is een SQLServer-syntaxis. Hier laten we het DatesCTE-gedeelte alle datums produceren tussen vandaag en 1 januari van het voorgaande jaar. We gebruiken het om alle facturen die bij die datums horen te retourneren.

Het startpunt is de @1stjanprevyear variabele en de fail-safe exit @today . Maximaal 730 dagen is mogelijk. De maximale recursie-optie is dus ingesteld op 1000 om ervoor te zorgen dat deze stopt.

We kunnen zelfs de MaxMinDates overslaan deel en schrijf het laatste deel, zoals hieronder getoond. Het kan een snellere aanpak zijn, omdat we een overeenkomende WHERE-clausule hebben.

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

Conclusie

Al met al hebben we kort besproken en getoond hoe u een complexe query kunt omzetten in een CTE-gestructureerde query. Wanneer een query is opgedeeld in verschillende CTE-delen, kunt u deze in andere delen gebruiken en onafhankelijk aanroepen in de uiteindelijke SQL-query voor foutopsporingsdoeleinden.

Een ander belangrijk punt is dat het gebruik van CTE het eenvoudiger maakt om een ​​complexe query te debuggen wanneer deze is opgedeeld in hanteerbare delen, om de juiste en verwachte resultatenset te retourneren. Het is belangrijk om te beseffen dat het uitvoeren van een uitleg op elk onderdeel van de query en de hele query cruciaal is om ervoor te zorgen dat de query en het DBMS zo optimaal mogelijk worden uitgevoerd.

Ik heb ook geïllustreerd hoe u een krachtige recursieve CTE-query/onderdeel schrijft bij het direct genereren van gegevens om verder in een query te gebruiken.

Met name, wanneer u een recursieve query schrijft, wees ZEER voorzichtig om de fail-safe exit niet te vergeten . Zorg ervoor dat u de berekeningen die in de fail-safe exit zijn gebruikt, dubbel controleert om een ​​stopsignaal te produceren en/of gebruik de maxrecursion optie die SQLServer biedt.

Evenzo kunnen andere DBMS ofwel cte_max_recursion_depth . gebruiken (MySQL 8.0) of max_recursive_iterations (MariaDB 10.3) als extra fail-safe uitgangen.

Lees ook

Alles wat u moet weten over SQL CTE op één plek


  1. Hoe veel rijen samen te voegen met dezelfde id in sql?

  2. Is er zoiets als een zip()-functie in PostgreSQL die twee arrays combineert?

  3. Automatisch verhogen na verwijderen in MySQL

  4. Bibliotheek niet geladen:/usr/local/opt/readline/lib/libreadline.6.2.dylib