Versione per la stampa       Invia     
Valuta il contenuto e lascia un commento
TechNet
Libreria TechNet
Articoli tecnici
SQL Server
SQL Server 2005
 Query ricorsive con SQL Server 2005
Query ricorsive con SQL Server 2005
Da Andrea Benedetti - MVP Windows Server System - SQL Server

Chi ha o ha avuto a che fare con un database relazionale si sarà trovato, almeno una volta, a dover risolvere un problema in maniera ricorsiva. Cerchiamo, in questo articolo, di analizzare le specifiche standard ISO (International Organization for Standardization) delle query ricorsive e come Microsoft SQL Server 2005 le implementa.

In questa pagina

La ricorsione La ricorsione
Lo scenario Lo scenario
CTE ricorsive CTE ricorsive

La ricorsione

Per definizione la ricorsione è una tecnica di programmazione tramite la quale una funzione richiama se stessa. Il problema maggiore da tenere in considerazione è l'eventualità che tale funzione non ritorni un risultato, ovvero chiami se stessa all'infinito.

Per questo motivo la progettazione di una ricorsione è una fase molto importante.Se esiste la possibilità che la ricorsione possa essere infinita si può decidere di implementare un meccanismo che tenga conto del numero di chiamate e terminarne l'esecuzione al raggiungimento di un limite stabilito.

Le CTE, Common Table Expression, sono una parte delle specifiche ISO SQL:1999 e rappresentano un'ulteriore conformità con queste specifiche di Microsoft SQL Server.

Questo un breve esempio di sintassi di query ricorsiva:

WITH [ RECURSIVE ] <query_alias_name> [ ( <column_list> ) ]
AS ( <select_query> )
<query_using_query_alias_name>

Possiamo pensare alle CTE come ad una sorta di resultset temporanei, ovvero come delle tabelle virtuali, la cui vita dipende esclusivamente dalla query in cui sono invocate.

use adventureworks
go

with cteAddress
as
(
select city from Person.Address
)

select top 1 * from cteAddress

select 'qualcosa d’altro...'

/* 
se dovessi rieseguire la select sulla cte avrei un errore!
l'ambito di visibilità è già terminato

select top 1 * from cteAddress
*/

Procedendo passo passo, ipotizziamo uno scenario e proviamo ad effettuare ragionamenti su di esso.

Lo scenario

Supponiamo di avere a che fare con una biblioteca.L'archivio che abbiamo di fronte prevede una tabella [categorieBiblioteca] in cui vengono memorizzate, in forma gerarchica, tutte le possibili categorie dei volumi presenti.Questa la sua semplice struttura:

IF EXISTS (SELECT 1 FROM   INFORMATION_SCHEMA.TABLES
           WHERE  TABLE_SCHEMA = USER
             AND  TABLE_NAME = 'categorieBiblioteca')
   DROP TABLE categorieBiblioteca
GO

CREATE TABLE categorieBiblioteca
(
idRecord int primary key identity(1,1),
idPadre int foreign key references categorieBiblioteca(idRecord),
descrizione varchar(120)
)

Costruita la tabella la andiamo a popolare:

-- elemento padre
insert categorieBiblioteca values (NULL, 'biblioteca')
-- principali cateogorie
insert categorieBiblioteca values (1, 'storia')
insert categorieBiblioteca values (1, 'filosofia')
insert categorieBiblioteca values (1, 'scienze')
-- storia
insert categorieBiblioteca values (2, 'storia moderna')
insert categorieBiblioteca values (2, 'storia dell''arte')
insert categorieBiblioteca values (2, 'storia urbana medievale')
insert categorieBiblioteca values (2, 'storia agraria medievale')
-- storia agraria medievale
insert categorieBiblioteca values (8, 'Vite e vino nel Medioevo')
insert categorieBiblioteca values (8, 'I mulini nell''Europa medievale')
-- filosofia
insert categorieBiblioteca values (3, 'filosofia della religione')
insert categorieBiblioteca values (3, 'filosofia morale')
insert categorieBiblioteca values (3, 'bibliografia critica')
insert categorieBiblioteca values (3, 'epistemologia')
-- bibliografia critica
insert categorieBiblioteca values (13, 'antica')
insert categorieBiblioteca values (13, 'medievale')
insert categorieBiblioteca values (13, 'moderna')
-- epistemologia
insert categorieBiblioteca values (14, 'logica')
insert categorieBiblioteca values (14, 'storia e filosofia della scienza')
insert categorieBiblioteca values (14, 'filosofia del linguaggio')
-- scienze
insert categorieBiblioteca values (4, 'scienze politiche')
insert categorieBiblioteca values (4, 'scienze matematiche')
insert categorieBiblioteca values (4, 'scienze giuridiche')
-- scienze politiche
insert categorieBiblioteca values (21, 'Politiche di mercato dei beni industriali')
insert categorieBiblioteca values (21, 'Politiche pubbliche')
insert categorieBiblioteca values (21, 'Popolazione e territorio')

La base della sintassi della CTE è la clausola WITH, utilizzata per definire un resultset temporaneo seguito da istruzioni select, insert, update o delete. Questa clausola è sempre seguita dal nome che verrà assegnato alla CTE. Nel caso in cui il corpo della nostra espressione sia definita un'istruzione SELECT, la CTE produce un set di righe definito dall'istruzione stessa.

Potremmo quindi provare a scrivere la nostra prima espressione, in maniera semplice (quanto inutile), come:

;with primaCteRealizzata
as
(
select descrizione from categorieBiblioteca
)

Per poi utilizzarla come:

select * from primaCteRealizzata

Scritta così si capisce che una common table expression può essere scelta, ad esempio, per semplificare la leggibilità e la manutenibilità di query più o meno complesse. Il punto e virgola (;) iniziale è un'istruzione richiesta dalla clausola WITH che impone che l'istruzione precedente termini proprio con questo carattere quando l'espressione della CTE viene utilizzata all'interno di un batch.

CTE ricorsive

La grande capacità di una CTE, abbiamo detto, è la possibilità di essere utilizzata in maniera ricorsiva.

La sintassi, allora, soltanto per il corpo della tabella, impone una scrittura leggermente differente.

Il corpo di questa tipologia di CTE deve contenere almeno due definizioni di query: un membro non ricorsivo ed uno ricorsivo.

In primis quindi costruiamo un'istruzione di select in grado di recuperare l'elemento padre della nostra ricorsione.

Successivamente costruiamo una seconda istruzione di select che, messa in join con la CTE stessa, sarà in grado di definire le regole padre-figlio dei nodi successivi.

Alla fine uniamo i due resultset tramite l'operatore UNION ALL.

Si capisce da subito, dovendo utilizzare l'operatore di unione, che i resultset dei due membri definiti dovrà necessariamente avere lo stesso numero e lo stesso tipo di colonne.

Ricordo anche che tutte le colonne restituite avranno il supporto al valore NULL, a prescindere dalla definizione in tabella delle colonne stesse.

Supponiamo quindi di scegliere una categoria della nostra biblioteca (la categoria "storia") e di voler conoscere tutti i nodi che seguono la categoria indicata.

Volendo identificare il padre del nostro record possiamo quindi scrivere:

select idRecord from categorieBiblioteca where descrizione = 'storia'

Quindi la nostra CTE diventa:

-- Costruzione CTE ricorsiva
; with cteCategorie
as
(
select 
idRecord, idPadre, descrizione
from categorieBiblioteca
where idPadre = 
(select idRecord from categorieBiblioteca where descrizione = 'storia')

union all

select 
Cat.idRecord, Cat.idPadre, Cat.descrizione
from categorieBiblioteca Cat
join cteCategorie CTE on Cat.idPadre = CTE.idRecord
)

-- Visualizzazione dei dati
select descrizione as FigliCercati from cteCategorie

Il risultato:

FigliCercati
----------------------------------
storia moderna
storia dell'arte
storia urbana medievale
storia agraria medievale
Vite e vino nel Medioevo
I mulini nell'Europa medievale

Certamente il risultato ottenuto con il precedente esempio è valido, ma non rende l'idea della posizione degli elementi all'interno dei rami.

Costruiamo allora un secondo esempio decidendo di voler visualizzare (in forma grafica) tutto l'albero delle gerarchie delle categorie.

In questo caso non è necessario recuperare il valore di idPadre per definire il primo elemento della CTE in quanto, volendo disegnare tutte le informazioni, sicuramente il primo elemento (root) sarà l'unico record che non ha padre.

Quindi ci è sufficiente indicare, nella clausola where: idPadre is null.

-- Costruzione CTE ricorsiva
; with cteCategorie (idRecord, idPadre, descrizione, livello, ordinamento)
as
(
select 
idRecord, idPadre, descrizione, 
0 as livello,
CAST(idRecord AS VARBINARY(900)) as ordinamento
from categorieBiblioteca
where idPadre is null

union all

select 
Cat.idRecord, Cat.idPadre, Cat.descrizione, 
cteCategorie.livello + 1 ,
 CAST(ordinamento + CAST(Cat.idRecord AS BINARY(4)) AS VARBINARY(900))
from categorieBiblioteca Cat
join cteCategorie on Cat.idPadre = cteCategorie.idRecord
)

-- Visualizzazione dati
select 
cast( REPLICATE('|  ', livello) + descrizione as varchar(50)) as Albero, 
idRecord, idPadre
from cteCategorie
order by ordinamento

Il risultato:

Albero                                             idRecord    idPadre
-------------------------------------------------- ----------- -----------
biblioteca                                         1           NULL
|  storia                                          2           1
|  |  storia moderna                               5           2
|  |  storia dell'arte                             6           2
|  |  storia urbana medievale                      7           2
|  |  storia agraria medievale                     8           2
|  |  |  Vite e vino nel Medioevo                  9           8
|  |  |  I mulini nell'Europa medievale            10          8
|  filosofia                                       3           1
|  |  filosofia della religione                    11          3
|  |  filosofia morale                             12          3
|  |  bibliografia critica                         13          3
|  |  |  antica                                    15          13
|  |  |  medievale                                 16          13
|  |  |  moderna                                   17          13
|  |  epistemologia                                14          3
|  |  |  logica                                    18          14
|  |  |  storia e filosofia della scienza          19          14
|  |  |  filosofia del linguaggio                  20          14
|  scienze                                         4           1
|  |  scienze politiche                            21          4
|  |  |  Politiche di mercato dei beni industriali 24          21
|  |  |  Politiche pubbliche                       25          21
|  |  |  Popolazione e territorio                  26          21
|  |  scienze matematiche                          22          4
|  |  scienze giuridiche                           23          4

Il punto è la colonna ordinamento, una colonna varbinary su cui viene ordinata la query.

Di fatto la colona contiene la gerarchia completa del campo idRecord che conduce al padre del nodo interessato.

Il "disegno" grafico riusciamo invece ad ottenerlo grazie alla funzione REPLICATE in grado di ripetere i caratteri indicati per un numero di volte richiesto (il livello del nodo).

All’inizio dell’articolo abbiamo sottolineato come una scrittura errata del ciclo di ricorsione potrebbe generare un loop infinito.

Una valida indicazione allora, è quella di utilizzare, se e quando conosciamo a priori il massimo livello di ricorsione desiderato, l’opzione OPTION (MAXRECURSION n).

Il nostro ultimo esempio, supponendo che il numero massimo di ricorsioni sia uguale a tre, diventerebbe quindi:

select 
cast( REPLICATE('|  ', livello) + descrizione as varchar(50)) as Albero, 
idRecord, idPadre
from cteCategorie
order by ordinamento
OPTION (MAXRECURSION 3);

Facendo attenzione, se impostassimo una ricorsione massima uguale ad 1, che tale opzione potrebbe sollevare il messaggio di errore 530:

Msg 530, Level 16, State 1, Line 3
The statement terminated. The maximum recursion 1 has been exhausted before statement completion.

Ovvero il messaggio che indica come non sia stato possibile completare la query richiesta in quanto il numero di ricorsioni impostato è minore rispetto al numero necessario.

Certamente questi risultati, ovvero l'utilizzo della ricorsione, con SQL Server 2000 sono abbastanza facili da ottenere tramite la scrittura di una UDF (User Defined Function) ricorsiva che cicli ogni record della tabella formattandone il risultato.

E' anche vero che le prestazioni (certo non con venti record ;-)) sono nettamente peggiori, oltre al fatto di dover scrivere codice con una maggiore complessità (e quindi più difficile da leggere e manutenere).

© 2009 Microsoft Corporation. Tutti i diritti riservati. Condizioni per l'utilizzo | Marchi | Informativa sulla privacy
Page view tracker