Tra le novità introdotte con SQL Server 2008 c'è il supporto a nuovi tipi di dati, creati allo scopo di semplificare la gestione di alcune categorie di informazioni: uno di questi è il tipo hierarchyid, con cui è possibile rappresentare la posizione in una gerarchia.
Grazie alle colonne di tipo hierarchyid
si può definire un albero che esprime relazioni di tipo padre-figlio tra i record di una tabella, avendo a disposizione un insieme di funzioni che facilitano la gestione di questo tipo di struttura (ad esempio, abbiamo metodi per recuperare tutti i discendenti di un elemento, il numero totale di livelli dell'albero, etc.).
I dati gerarchici possono essere utili, ad esempio, per rappresentare l'organizzazione dei thread di un forum, l'organigramma di un'azienda oppure la struttura delle componenti di un certo sistema.
Il tipo di dati hierarchyid
è supportato da tutte le versioni di SQL Server 2008, compresa l'edizione Express. Per utilizzarlo con i linguaggi di programmazione .NET, è necessario utilizzare Visual Studio 2008 aggiornato al Service Pack 1.
Rappresentazione di dati gerarchici
Un campo di tipo hierarchyid
contiene informazioni su un singolo nodo in un albero della gerarchia codificando il percorso dalla radice dell'albero al nodo stesso. Ogni nodo è caratterizzato da un livello. Per convenzione, il livello della radice è 0.
All'interno di SQL Server 2008, tale percorso è rappresentato logicamente come una sequenza di etichette dei nodi di tutti gli elementi visitati dopo la radice. La figura seguente mostra il valore dei campi hierarchyid dei record all'interno della gerarchia:
La rappresentazione inizia con una barra, che rappresenta la radice dell'albero. Per i livelli sottostanti, ogni etichetta è codificata come una sequenza di numeri interi separati da barre, ciascuno dei quali identifica il numero del nodo all'interno di un determinato livello (nell'esempio, la sequenza /2/1/2/
rappresenta il secondo nodo del primo figlio del secondo discendente della radice). Ogni livello, infine, è a sua volta seguito da una barra.
È importante ricordare che una colonna di tipo hierarchyid non rappresenta automaticamente un albero, ma solo la posizione di un record all'interno di una gerarchia. E' compito dell'applicazione o di chi inserisce le informazioni generare e assegnare i valori hierarchyid in maniera tale che la relazione desiderata tra le righe sia riflessa in tali valori.
Creazione di una tabella con dati gerarchici
Vediamo adesso come creare una tabella in grado di contenere dati organizzati in modo gerarchico. Supponiamo di voler definire l'organigramma di un'azienda: dobbiamo definire una struttura in cui ogni impiegato ha un superiore, che è anche il responsabile della sua attività.
Eseguiamo innanzi tutto il seguente script SQL all'interno del Management Studio:
CREATE DATABASE HierarchyTest GO USE HierarchyTest GO CREATE TABLE Employees ( OrgNode hierarchyid PRIMARY KEY CLUSTERED, OrgLevel AS OrgNode.GetLevel(), EmployeeID int UNIQUE NOT NULL, EmpName varchar(20) NOT NULL, Title varchar(20) NULL ) GO
In questo script creiamo un database di nome HierarchyTest
e al suo interno definiamo la tabella Employees
. La colonna ORGNODE
è di tipo hierarchyid
: useremo questo campo per gestire la gerarchia tra gli impiegati. Per il nostro esempio, inoltre, inseriamo anche la colonna ORGLEVEL
, un campo calcolato utilizzando il metodo GetLevel
sull'oggetto hierarchyid
: come il nome lascia intuire, esso restituisce il livello del nodo nella struttura gerarchica.
Ora possiamo aggiungere informazioni alla tabella, cominciando naturalmente dalla radice, che sarà occupata da "Paolo Rossi", il presidente della società:
INSERT Employees(OrgNode, EmployeeID, EmpName, Title) VALUES (hierarchyid::GetRoot(), 1, 'Paolo Rossi', 'President')
In questo esempio utilizziamo il metodo hierarchyid::GetRoot()
, che restituisce un oggetto di tipo hierarchyid
che rappresenta la radice della gerarchia.
GetDescendant
Inseriamo un vicepresidente "Mario Bianchi":
DECLARE @President hierarchyid SELECT @President = hierarchyid::GetRoot() FROM Employees INSERT Employees (OrgNode, EmployeeID, EmpName, Title) VALUES(@President.GetDescendant(NULL, NULL), 2, 'Mario Bianchi', 'Vice President')
Qui dichiariamo una variabile di tipo hierarchyid
e le assegniamo il valore di hierarchyid::GetRoot()
eseguito sulla tabella Employees
, in modo da ottenere un riferimento alla radice dell'albero. Questo valore ci serve per eseguire l'istruzione INSERT
seguente, in cui utilizziamo il metodo @President.GetDescendant(NULL, NULL)
per ottenere la posizione nella gerarchia di un nodo figlio.
Proviamo ad eseguire una SELECT
sulla tabella per osservare come stati memorizzati i dati:
SELECT OrgNode.ToString() AS OrgNode, OrgLevel, EmployeeID, EmpName, Title FROM Employees
OrgNode | OrgLevel | EmployeeID | EmpName | Title |
---|---|---|---|---|
/ | 0 | 1 | Paolo Rossi | President |
/1/ | 1 | 2 | Mario Bianchi | Vice President |
Poiché il tipo hierarchyid
è memorizzato internamente come dato binario, utilizziamo il metodo ToString sul campo relativo per averne una rappresentazione in formato stringa.
Osserviamo che il vice-presidente è stato correttamente inserito al livello 1 della gerarchia. Nel nostro esempio, infatti, al metodo GetDescendant(child1, child2)
abbiamo passato due parametri con valore NULL
, perché il nostro obiettivo era inserire un discendente diretto della radice, l'unico nodo della gerarchia. Specificando valori diversi da NULL
, è però possibile far restituire alla funzione una diversa posizione nell'albero, secondo quanto riportato nella tabella seguente:
Parametri | Posizione | |
---|---|---|
child1 | child2 | rispetto al nodo che invoca GetDescendant |
NULL |
NULL |
Figlio del nodo. Impostazione utile per inserire il primo figlio di un nodo |
hierarchyid di un nodo figlio |
NULL |
Figlio del nodo, allo stesso livello di child1 , ma con indice maggiore (in altri termini, un nodo "fratello" di child1 ).Ad esempio, se child1 ha posizione /1/ nella gerarchia, il valore restituito sarà /2/ |
NULL |
hierarchyid di un nodo figlio |
Figlio del nodo, allo stesso livello di child2 , ma con indice minore (nodo "fratello" di child2 ).Ad esempio, se child2 ha posizione /2/ nella gerarchia, il valore restituito sarà /1/ |
hierarchyid di un nodo figlio |
hierarchyid di un nodo figlio |
Figlio del nodo, al livello di child1 e child2 , con indice maggiore di child1 e minore di child2 .Ad esempio, se child1 e child2 hanno, rispettivamente, posizione /4/ e /7/ nella gerarchia, il valore restituito sarà /5/ |
child1 oppure child2 non sono figli del nodo su cui è richiamato il metodo GetDescendant
oppure |
Eccezione |
Utilizzando il solo metodo GetDescendant
è teoricamente possibile creare qualsiasi gerarchia, anche molto complessa. Questo procedimento, tuttavia, non è molto intuitivo, perché richiede di conoscere l'esatta posizione nell'albero di ogni nodo a cui si vogliono aggiungere figli.
Determinare la posizione con GetAncestor
Una soluzione più immediata consiste nel definire una Stored procedure in grado di determinare la posizione nell'albero conoscendo l'ID del responsabile (ovvero l'impiegato "padre"):
CREATE PROCEDURE AddEmployee(@parentID int, @empID int, @name varchar(20), @title varchar(20)) AS BEGIN -- Recupera il nodo padre utilizzando l'ID del responsabile DECLARE @parent hierarchyid, @node hierarchyid SELECT @parent = OrgNode FROM Employees WHERE EmployeeID = @parentID SET TRANSACTION ISOLATION LEVEL SERIALIZABLE BEGIN TRANSACTION -- Recupera il nodo con indice più alto tra quelli il cui padre è -- @parent SELECT @node = MAX(OrgNode) FROM Employees WHERE OrgNode.GetAncestor(1)=@parent -- Inserisce il nodo come discendente di parent -- Poiché si utilizza il metodo @parent.GetDescendant(@lc, NULL), il -- nuovo nodo avrà indice maggiore di quello di @node INSERT Employees(OrgNode, EmployeeID, EmpName, Title) VALUES(@parent.GetDescendant(@node, NULL), @empID, @name, @title) COMMIT END GO
La procedura innanzi tutto recupera, utilizzando il suo ID (parentID
), il nodo corrispondente al responsabile dell'impiegato che si sta inserendo. Successivamente, seleziona il nodo con indice più alto tra quelli che hanno il padre appena identificato: allo scopo, si utilizza il metodo hierarchyid.GetAncestor(n)
, che restituisce l'n-esimo predecessore (quindi, con n=1, si indica il padre del nodo). Infine, effettua l'inserimento vero e proprio, impostando per il nuovo nodo la posizione nella gerarchia ottenuta richiamando il metodo hierarchyid.GetDescendant
, come descritto in precedenza.
Verifichiamo subito il corretto funzionamento della stored procedure eseguendo i seguenti comandi SQL:
EXEC AddEmployee 2, 3, 'Carlo Verdi', 'Marketing' EXEC AddEmployee 2, 4, 'John Doe', 'Human Resources' EXEC AddEmployee 2, 5, 'Andrea Rosa', 'Research & Development' EXEC AddEmployee 3, 6, 'Francesco Neri', 'Europe' -- Responsabile: Carlo Verdi EXEC AddEmployee 5, 7, 'Jane Doe', 'Project Manager 1' -- Responsabile: Andrea Rosa EXEC AddEmployee 5, 8, 'Tizio', 'Project Manager 2' -- Responsabile: Andrea Rosa EXEC AddEmployee 5, 9, 'Caio', 'Project Manager 3' -- Responsabile: Andrea Rosa
Dopo questi inserimenti, complessivamente la tabella Employees
conterrà i seguenti record:
OrgNode | OrgLevel | EmployeeID | EmpName | Title |
---|---|---|---|---|
/ | 0 | 1 | Paolo Rossi | President |
/1/ | 1 | 2 | Mario Bianchi | Vice President |
/1/1/ | 2 | 3 | Carlo Verdi | Marketing |
/1/1/1/ | 3 | 6 | Francesco Neri | Europe |
/1/2/ | 2 | 4 | John Doe | Human Resources |
/1/3/ | 2 | 5 | Andrea Rosa | Research & Development |
/1/3/ | 2 | 5 | Jane Doe | Research & Development |
/1/3/1/ | 3 | 7 | Andrea Rosa | Project Manager 1 |
/1/3/2/ | 3 | 8 | Tizio | Project Manager 2 |
/1/3/3/ | 3 | 9 | Caio | Project Manager 3 |
I valori della colonna ORGNODE
descrivono la struttura gerarchica tra gli impiegati, che possiamo rappresentare graficamente nel modo seguente:
Se, ad esempio, vogliamo aggiungere un programmatore subordinato al Project Manager 2, che ha ID uguale a 8, ci basta indicare tale valore come parentID nella chiamata alla stored procedure AddEmployee:
EXEC AddEmployee 8, 10, 'Sempronio', 'Developer' -- Responsabile: Tizio
Eseguire query su dati gerarchici
Vediamo ora come utilizzare i metodi forniti dal tipo hierarchyid
per recuperare determinate informazioni all'interno della gerarchia.
Iniziamo estraendo il nodo radice della gerarchia, utilizzando il metodo statico GetRoot
sul tipo hierarchyid
:
SELECT OrgNode.ToString() AS OrgNode, OrgLevel, EmployeeID, EmpName, Title FROM Employees WHERE OrgNode = hierarchyid::GetRoot()
Un altro tipo di interrogazione abbastanza comune sui dati gerarchici consiste nel recuperare il padre di un certo nodo. Ad esempio, proviamo ad ottenere il responsabile dell'impiegato "John Doe" (il cui ID
è 4
):
DECLARE @manager hierarchyid SELECT @manager = OrgNode.GetAncestor(1) FROM Employees WHERE EmployeeID = 4 SELECT OrgNode.ToString() AS OrgNode, OrgLevel, EmployeeID, EmpName, Title FROM Employees WHERE OrgNode = @manager
Per avere le informazioni che ci servono, dobbiamo eseguire due query: la prima recupera il padre del nodo corrispondente all'impiegato con ID 4, utilizzando il metodo hierarchyid.GetAncestor(1)
, mentre nella seconda sfruttiamo tale valore per estrarre le informazioni che ci interessano (e quindi, come ci aspettiamo, otteniamo il vice-presidente "Mario Bianchi").
Supponiamo ora di voler recuperare tutti gli impiegati che dipendono da Andrea Rosa (il cui ID
è 5
). Per fare questo, ancora una volta dobbiamo utilizzare il metodo GetAncestor
:
DECLARE @manager hierarchyid SELECT @manager = OrgNode FROM Employees WHERE EmployeeID = 5 SELECT OrgNode.ToString() AS OrgNode, OrgLevel, EmployeeID, EmpName, Title FROM Employees WHERE OrgNode.GetAncestor(1) = @manager
Per ottenere i dipendenti di un impiegato ("Andrea Rosa" nell'esempio), dobbiamo recuperare la sua posizione nella gerarchia, cosa che facciamo con la prima query. In tal modo, possiamo usare il metodo hirerarchyid.GetAncestor(1)
per estrarre tutti i record il cui padre è proprio quell'impiegato ("Jane Doe", "Tizio" e "Caio"). Se avessimo utilizzato il numero 2
come parametro, invece, avremmo ottenuto i nodi il cui "nonno" è "Andrea Rosa", e così via.
Vediamo, infine, un esempio di utilizzo del metodo IsDescendantOf, che restituisce il valore 1
se il nodo su cui è richiamato è discendente del nodo passato come argomento. Ad esempio, per ottenere la lista di coloro che dipendono dal vice-presidente "Mario Bianchi" (il cui ID
è 2
), a qualunque livello, possiamo usare le query:
DECLARE @manager hierarchyid SELECT @manager = OrgNode FROM Employees WHERE EmployeeID = 2 SELECT OrgNode.ToString() AS OrgNode, OrgLevel, EmployeeID, EmpName, Title FROM Employees WHERE EmployeeID <> 2 AND OrgNode.IsDescendantOf(@manager) = 1
Come abbiamo già visto nei casi precedenti, innanzi tutto recuperiamo la posizione nell'albero del nodo che vogliamo usare per il confronto (ovvero quello con ID = 2
). Successivamente, estraiamo dalla gerarchia tutti gli impiegati che discendono da tale nodo, controllando il valore di IsDescendantOf. Ricordiamo che, nella semantica di questa funzione, un nodo è considerato discendente di se stesso: per tale motivo, nella clausola WHERE
abbiamo aggiunto la condizione EmployeeID <> 2
, per non mostrare l'impiegato di cui stiamo ricercando i dipendenti.
Quando utilizzare il tipo hierarchyid
Il tipo hierarchyid
non è l'unica soluzione disponibile per rappresentare relazioni gerarchiche tra i dati memorizzati in una tabella. Le alternative possibili sono l'utilizzo di chiavi esterne per esprimere il rapporto padre/figlio tra due record (ogni record contiene una chiave esterna verso il padre) oppure i campi XML.
Rispetto all'utilizzo di chiavi esterne, le query su sottoalbero, ad esempio per recuperare tutti i discendenti (di qualsiasi livello) di un nodo, sono molto più veloci con hierarchyid
. Al contrario, le interrogazioni che cercano il discendente diretto di un nodo sono leggermente più rapide se le relazioni sono espresse tramite chiavi esterne.
Per quanto riguarda la seconda alternativa, poiché un documento XML è di per sé un albero, un singolo campo di tipo XML è in grado di rappresentare una gerarchia completa. Per tale motivo, l'utilizzo di campi XML è preferibile quando la maggior parte delle interrogazioni deve recuperare un albero completo, quindi senza applicare filtri, oppure le applicazioni necessitano di dati proprio in formato XML: in quest'ultimo caso, infatti, il passaggio da una rappresentazione basata su hierarchyid
ad una struttura XML può avere un costo computazionale non indifferente.
Ricordiamo comunque che, quando si crea un indice di tipo XML, SQL Server 2008 utilizza internamente oggetti di tipo hierarchyid
per tenere traccia delle posizioni nella gerarchia.
Come sempre, quindi, la scelta di usare il tipo hierarchyid
deve essere valutata attentamente sulla base delle reali necessità dell'applicazione che si sta sviluppando, quindi in particolare considerando le modalità di accesso ai dati che saranno applicate.