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

In che modo CTE può aiutare a scrivere query complesse e potenti:una prospettiva sulle prestazioni

Spesso vediamo query SQL complesse scritte male in esecuzione su una tabella o su tabelle nei database. Queste query rendono il tempo di esecuzione molto lungo e causano un consumo enorme di CPU e altre risorse. Tuttavia, le query complesse forniscono informazioni preziose all'applicazione/alla persona che le esegue in molti casi. Pertanto, sono risorse utili in tutte le varietà di applicazioni.

È difficile eseguire il debug di query complesse

Se osserviamo da vicino le query problematiche, molte di esse sono complesse, in particolare quelle specifiche utilizzate nei rapporti.

Le query complesse spesso sono costituite da cinque o più tabelle di grandi dimensioni e sono unite insieme da molte sottoquery. Ogni sottoquery ha una clausola WHERE che esegue calcoli e/o trasformazioni di dati da semplici a complessi unendo insieme le tabelle rilevanti delle colonne.

Tali query possono diventare difficili da eseguire il debug senza consumare molte risorse. Il motivo è che è difficile determinare se ciascuna sottoquery e/o sottoquery unite producono risultati corretti.

Uno scenario tipico è:ti chiamano a tarda notte per risolvere un problema su un server di database occupato con una query complessa coinvolta e devi risolverlo rapidamente. In qualità di sviluppatore o DBA, potresti avere tempo e risorse di sistema molto limitati disponibili a tarda ora. Pertanto, la prima cosa di cui hai bisogno è un piano su come eseguire il debug della query problematica.

A volte, la procedura di debug va bene. A volte, ci vuole molto tempo e fatica prima di raggiungere l'obiettivo e risolvere il problema.

Scrittura di query nella struttura CTE

Ma cosa accadrebbe se ci fosse un modo per scrivere query complesse in modo da poterle eseguire il debug rapidamente, pezzo per pezzo?

C'è un modo. Si chiama Common Table Expression o CTE.

Common Table Expression è una funzionalità standard nella maggior parte dei database moderni come SQLServer, MySQL (a partire dalla versione 8.0), MariaDB (versione 10.2.1), Db2 e Oracle. Ha una struttura semplice che incapsula una o più sottoquery in un set di risultati denominato temporaneo. È possibile utilizzare ulteriormente questo set di risultati in altri CTE o sottoquery denominati.

Un'espressione di tabella comune è, in una certa misura, una VIEW che esiste solo ed è referenziata dalla query al momento dell'esecuzione.

La trasformazione di una query complessa in una query in stile CTE richiede un pensiero strutturato. Lo stesso vale per OOP con incapsulamento quando si riscrive una query complessa in una struttura CTE.

Devi pensare a:

  1. Ogni set di dati che stai estraendo da ogni tabella.
  2. Come vengono uniti per incapsulare le sottoquery più vicine in un set di risultati con nome temporaneo.

Ripetere l'operazione per ogni sottoquery e set di dati rimanenti fino a raggiungere il risultato finale della query. Nota che ogni set di risultati denominato temporaneo è anche una sottoquery.

La parte finale della query dovrebbe essere una selezione molto "semplice", restituendo il risultato finale all'applicazione. Una volta raggiunta questa parte finale, puoi scambiarla con una query che seleziona i dati da un set di risultati temporaneo denominato individualmente.

In questo modo, il debug di ogni set di risultati temporaneo diventa un lavoro facile.

Per capire come possiamo costruire le nostre query da semplici a complesse, diamo un'occhiata alla struttura CTE. La forma più semplice è la seguente:

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

Qui CTE_1 è un nome univoco che assegni al set di risultati denominato temporaneo. Possono esserci tutti i set di risultati necessari. Con ciò, il modulo si estende a, come mostrato di seguito:

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

Inizialmente, ogni parte CTE viene creata separatamente. Quindi procede, poiché i CTE sono collegati tra loro per creare il set di risultati finale della query.

Ora, esaminiamo un altro caso, interrogando un database di vendita fittizio. Vogliamo sapere quali prodotti, inclusa la quantità e le vendite totali, sono stati venduti in ciascuna categoria il mese precedente e quali di loro hanno ottenuto più vendite totali rispetto al mese precedente.

Costruiamo la nostra query in diverse parti CTE, in cui ciascuna parte fa riferimento alla precedente. Innanzitutto, costruiamo un set di risultati per elencare i dati dettagliati di cui abbiamo bisogno dalle nostre tabelle per formare il resto della query:

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

Il passaggio successivo consiste nel riassumere la quantità e i dati di vendita totali per ciascuna categoria e nomi di prodotti:

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

Il passaggio finale consiste nel creare due set di risultati temporanei che rappresentano i dati dell'ultimo mese e del mese precedente. Successivamente, filtra i dati da restituire come set di risultati finali:

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

Nota che in SQLServer imposti getdate() invece di CURRENT_DATE.

In questo modo, possiamo scambiare l'ultima parte con un select che interroga le singole parti CTE per vedere il risultato di una parte selezionata. Di conseguenza, possiamo eseguire rapidamente il debug del problema.

Inoltre, eseguendo una spiegazione su ciascuna parte CTE (e sull'intera query), stimiamo il rendimento di ciascuna parte e/o dell'intera query sulle tabelle e sui dati.

Di conseguenza, puoi ottimizzare ogni parte riscrivendo e/o aggiungendo gli indici appropriati alle tabelle coinvolte. Quindi spieghi l'intera query per vedere il piano di query finale e procedi con l'ottimizzazione, se necessario.

Query ricorsive che utilizzano la struttura CTE

Un'altra utile funzionalità di CTE è la creazione di query ricorsive.

Le query SQL ricorsive ti consentono di ottenere cose che non avresti immaginato possibili con questo tipo di SQL e la sua velocità. Puoi risolvere molti problemi aziendali e persino riscrivere alcune complesse logiche SQL/applicative in una semplice chiamata SQL ricorsiva al database.

Esistono lievi variazioni nella creazione di query ricorsive tra i sistemi di database. Tuttavia, l'obiettivo è lo stesso.

Alcuni esempi dell'utilità del CTE ricorsivo:

  1. Puoi usarlo per trovare le lacune nei dati.
  2. Puoi creare organigrammi.
  3. Puoi creare dati precalcolati da utilizzare ulteriormente in un'altra parte CTE
  4. Infine, puoi creare dati di test.

La parola ricorsiva dice tutto. Hai una query che si richiama ripetutamente con un punto di partenza e, ESTREMAMENTE IMPORTANTE, un punto finale (un uscita di sicurezza come lo chiamo io).

Se non hai un'uscita di sicurezza, o la tua formula ricorsiva va oltre, sei nei guai. La query entrerà in un ciclo infinito con conseguente CPU molto elevata e utilizzo LOG molto elevato. Comporterà l'esaurimento della memoria e/o della memoria.

Se la tua query va in tilt, devi pensare molto velocemente per disabilitarla. Se non puoi farlo, avvisa immediatamente il tuo DBA, in modo che impediscano al sistema di database di soffocare, uccidendo il thread in fuga.

Vedi l'esempio:

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

Questo esempio è una sintassi CTE ricorsiva MySQL/MariaDB. Con esso, produciamo mille date casuali. Il livello è il nostro contatore e l'uscita di sicurezza per uscire dalla query ricorsiva in modo sicuro.

Come dimostrato, la riga 2 è il nostro punto di partenza, mentre le righe 4-5 sono la chiamata ricorsiva con il punto finale nella clausola WHERE (riga 6). Le righe 8 e 9 sono le chiamate nell'esecuzione della query ricorsiva e nel recupero dei dati.

Un altro esempio:

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

Questo esempio è una sintassi di SQLServer. Qui, lasciamo che la parte DatesCTE produca tutte le date tra oggi e il 1 gennaio dell'anno precedente. Lo utilizziamo per restituire tutte le fatture appartenenti a tali date.

Il punto di partenza è il @1stjanprevyear variabile e l'uscita fail-safe @today . È possibile un massimo di 730 giorni. Pertanto, l'opzione di ricorsione massima è impostata su 1000 per assicurarsi che si interrompa.

Potremmo anche saltare i MaxMinDates parte e scrivere la parte finale, come mostrato di seguito. Può essere un approccio più rapido, poiché abbiamo una clausola WHERE corrispondente.

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

Conclusione

Nel complesso, abbiamo brevemente discusso e mostrato come trasformare una query complessa in una query strutturata CTE. Quando una query è suddivisa in diverse parti CTE, puoi usarle in altre parti e chiamare in modo indipendente nella query SQL finale per scopi di debug.

Un altro punto chiave è che l'utilizzo di CTE semplifica il debug di una query complessa quando è suddivisa in parti gestibili, per restituire il set di risultati corretto e previsto. È importante rendersi conto che l'esecuzione di una spiegazione su ciascuna parte della query e l'intera query è fondamentale per garantire che la query e il DBMS funzionino nel modo più ottimale possibile.

Ho anche illustrato la scrittura di una potente query/parte CTE ricorsiva nella generazione di dati al volo da utilizzare ulteriormente in una query.

In particolare, quando si scrive una query ricorsiva, fare MOLTO attenzione a NON dimenticare l'uscita fail-safe . Assicurati di ricontrollare i calcoli utilizzati nell'uscita di sicurezza per produrre un segnale di arresto e/o utilizzare la maxrecursion opzione fornita da SQLServer.

Allo stesso modo, altri DBMS possono utilizzare cte_max_recursion_depth (MySQL 8.0) o max_recursive_iterations (MariaDB 10.3) come uscite fail-safe aggiuntive.

Leggi anche

Tutto ciò che devi sapere su SQL CTE in un unico punto