25th Jun 2024 Tempo di lettura: 14 minuti Come interrogare una struttura ad albero genitore-figlio in SQL Tihomir Babic sql imparare sql cte Indice Cos'è una struttura ad albero genitore-figlio? Struttura ad albero genitore-figlio nei database relazionali Query tipiche eseguite su una struttura ad albero genitore-figlio Introduzione ai dati di esempio Esempio 1: Elenco di tutti i figli di un genitore Esempio 2: Elencare un nodo genitore per un nodo figlio Esempio 3: Ottenere un numero di generazione (o livello dell'albero) per ogni nodo Esempio 4: Elenco di tutti i discendenti Esempio 5: Generare una vista ad albero di dati gerarchici L'interrogazione delle strutture ad albero genitore-figlio diventa sempre più interessante Cosa sono le strutture ad albero genitore-figlio in SQL? In questo articolo rispondiamo a questa domanda, parliamo della gerarchia delle query e mostriamo le cinque query SQL più comuni necessarie per queste strutture di dati. Sì, è possibile utilizzare l'SQL su una struttura ad albero genitore-figlio. In questo articolo vi mostrerò come. Vi illustrerò cinque esempi di query, a partire dalla più semplice fino alla più complessa. Questi esempi utilizzeranno espressioni ricorsive di tabelle comuni (CTE). Se non conoscete le CTE, vi consiglio di seguire il nostro corso interattivo. Query ricorsive interattivo. Contiene oltre 100 esercizi che insegnano a utilizzare le CTE in SQL, partendo dalle basi e arrivando ad argomenti avanzati come le CTE ricorsive. Prima di addentrarci nel codice, vediamo una panoramica della struttura ad albero genitore-figlio e di come viene memorizzata in un database relazionale. Cos'è una struttura ad albero genitore-figlio? Se conoscete i dati gerarchici, probabilmente saprete che si tratta di un sinonimo di struttura genitore-figlio. Entrambi i nomi sono molto logici: una struttura ad albero genitore-figlio è un insieme di dati strutturati gerarchicamente. In altre parole, esistono relazioni gerarchiche tra i dati. Ciò significa che un dato può essere il genitore di un altro dato, che viene chiamato figlio. Gli elementi sono chiamati anche livelli o nodi dell'albero e possono assumere tre forme principali: Nodo radice - Il primo nodo, dove inizia l'albero genitore-figlio. Nodo genitore - È un nodo che ha uno o più nodi discendenti (o figli). Nodo figlio - Qualsiasi nodo che ha un predecessore o un nodo genitore. Esempi reali di strutture genitore-figlio sono le strutture organizzative delle aziende (un'azienda è composta da dipartimenti, i dipartimenti sono composti da team e i team sono composti da dipendenti), gli alberi genealogici (ci sono genitori, figli, nipoti, pronipoti, ecc.) e le tassonomie naturali (gli esseri viventi appartengono a un dominio, un regno, un phylum, una classe, un ordine, una famiglia, un genere e una specie). Anche le cartelle dei computer (disco C, Programmi, Microsoft SQL Server...), i menu (bevande, bevande analcoliche, tè...), i generi artistici e musicali (per esempio, c'era il blues, che ha sviluppato il rhythm and blues, che ha portato al soul, al funk...) e i progetti (un progetto ha sottoprogetti, che hanno compiti, sottocompiti...) possono essere considerati strutture di dati gerarchiche. Struttura ad albero genitore-figlio nei database relazionali Affinché SQL possa fare qualcosa, una struttura ad albero genitore-figlio deve essere memorizzata in un database relazionale. Queste strutture sono solitamente memorizzate in una tabella con due colonne ID, di cui una fa riferimento all'ID di un oggetto genitore. Questo ci permette di determinare la gerarchia tra i dati. In altre parole, sappiamo quale nodo è un nodo genitore di quale nodo figlio e viceversa. Tutto ciò potrebbe sembrare un po' astratto, quindi vi mostrerò come funziona con un semplice esempio. E lo farò in modo letterale! La mia struttura ad albero genitore-figlio mostrerà i dati relativi ai genitori e ai loro figli. Date un'occhiata: idfirst_namelast_nameparent_id 1JimCliffyNULL 2MarkCliffy1 3VeronicaCliffy2 Qui, la colonna id mostra l'ID del figlio. Per scoprire chi è il genitore di quel bambino, bisogna guardare la colonna parent_id, trovare lo stesso numero ID nella colonna id e cercare il nome del genitore in quella riga. In altre parole, Jim Cliffy non ha genitori in questa tabella; il valore della sua colonna parent_id è NULL. Ciò significa che è il nodo radice di questa struttura ad albero. Mark Cliffy è il figlio di Jim Cliffy. Come faccio a saperlo? Perché Mark è parent_id = 1, che è l'ID di Jim Cliffy. Mark Cliffy è un nodo figlio, ma è anche un nodo genitore. Perché? Perché Veronica Cliffy è la figlia di Mark Cliffy. Lo so perché il suo genitore ha parent_id = 2 e la tabella mi dice che è Mark Cliffy. Veronica Cliffy è solo un nodo figlio; ha un nodo genitore, ma non ci sono nodi figli che si diramano da lei. Query tipiche eseguite su una struttura ad albero genitore-figlio Utilizzerò la stessa tabella per ciascuna di queste query. Ha le stesse colonne mostrate sopra, ma con più righe e valori diversi. Introduzione ai dati di esempio La tabella si chiama parent_child e ha le seguenti colonne: id - L'ID del bambino e la chiave primaria (PK) della tabella. first_name - Il nome del bambino. last_name - Il cognome del bambino. parent_id - L'ID del genitore del bambino. Ecco l'intera tabella: idfirst_namelast_nameparent_id 1RosaWellingtonNULL 2JonWellington1 3JoniWellington1 4MargeWellington1 5MaryDijkstra2 6FrankWellington2 7JasonWellington3 8BobbyWellington4 9SammyWellington4 10SarahWellington4 11Sam FrancisDijkstra5 12StephenWellington6 13TrentWellington6 14JuneWellington9 15JosephineWellington9 16SuzyWellington9 È possibile utilizzare questa tabella per verificare se le query che sto per mostrare restituiscono l'output corretto. Esempio 1: Elenco di tutti i figli di un genitore Questa è la query più semplice, quindi la utilizzerò per mettervi a vostro agio con la struttura ad albero. Qui voglio trovare tutti i figli di un genitore specificato. In questo caso, sono interessato a trovare tutti i figli di una persona chiamata Marge Wellington, il cui ID è 4. Ecco la piccola query: SELECT first_name, last_name FROM parent_child WHERE parent_id = 4; Ho semplicemente selezionato il nome e il cognome dalla tabella e ho usato la clausola WHERE per mostrare solo le righe in cui è presente un 4 nella colonna parent_id. Il risultato mostra tre righe: first_namelast_name BobbyWellington SammyWellington SarahWellington Mi dice che Bobby, Sammy e Sarah Wellington sono tutti figli di Marge Wellington. Guardate la tabella originale e vedrete che è vero. Questo era solo un riscaldamento! Passiamo alla prossima. Esempio 2: Elencare un nodo genitore per un nodo figlio Ora, l'output dell'esempio precedente era un po', beh, elementare. Ho elencato solo i nomi dei figli. Potrebbe essere molto utile mostrare anche il nome del genitore. Ed è esattamente quello che farò. Mostrerò sia il nome che il cognome del bambino e del genitore. Invece di cercare i figli di un genitore, ora cercherò i genitori del figlio. Voglio scoprire chi è il genitore di Sam Francis Dijkstra. Oltre ai nomi, voglio vedere anche gli ID. La query è la seguente: SELECT child.id AS child_id, child.first_name AS child_first_name, child.last_name AS child_last_name, parent.first_name AS parent_first_name, parent.last_name AS parent_last_name, parent.id AS parent_id FROM parent_child child JOIN parent_child parent ON child.parent_id = parent.id WHERE child.id = 11; Il concetto principale che sto introducendo è quello di self-join. Ho dato l'alias child alla tabella parent_child e l'ho unita a se stessa usando l'alias parent alias. In questo modo, mi comporto come se stessi lavorando con due tabelle diverse. Una contiene i dati sui bambini; per questo l'ho chiamata child. L'altra contiene i dati sui genitori, per cui l'ho chiamata parent. Le colonne selezionate riflettono questo aspetto. I nomi e gli ID dei bambini sono selezionati dalla tabella "prima". I nomi e gli ID dei genitori sono selezionati dalla "seconda" tabella. Le "tabelle" sono unite dove parent_id è uguale a id. La tabella originale mi dice che l'ID di Sam Francis Dijkstra è 11. Ho usato la clausola WHERE per filtrare i dati e mostrare solo il genitore di Sam Francis. È possibile utilizzare anche la clausola WHERE sulle colonne child.first_name e child.last_name. Ho scelto di filtrare i dati utilizzando l'ID perché in questo modo la query è un po' più breve. Ecco il risultato: child_idchild_first_namechild_last_nameparent_first_nameparent_last_nameparent_id 11Sam FrancisDijkstraMaryDijkstra5 Mostra che la madre di Sam Francis è Mary Dijkstra, il che è vero. Tutto chiaro fino ad ora? Bene. Andiamo avanti! Esempio 3: Ottenere un numero di generazione (o livello dell'albero) per ogni nodo In questo esempio, voglio elencare tutte le persone della tabella e mostrare a quale generazione appartengono. A che scopo? Una volta ottenuti questi dati, posso vedere facilmente chi appartiene a quale generazione: genitori, figli, nipoti, ecc. Per ottenere questo risultato, utilizzerò una CTE, non una CTE qualsiasi, ma una CTE ricorsiva. Se avete bisogno di rinfrescare le vostre conoscenze sulle CTE, ecco un articolo che spiega cos'è una CTE. Ecco la mia domanda: WITH RECURSIVE generation AS ( SELECT id, first_name, last_name, parent_id, 0 AS generation_number FROM parent_child WHERE parent_id IS NULL UNION ALL SELECT child.id, child.first_name, child.last_name, child.parent_id, generation_number+1 AS generation_number FROM parent_child child JOIN generation g ON g.id = child.parent_id ) SELECT first_name, last_name, generation_number FROM generation; Come ogni CTE ricorsiva, la mia inizia con due parole chiave: WITH RECURSIVE. Ho dato un nome alla generazione CTE. Nella prima istruzione SELECT, seleziono ID e nomi. Inoltre, c'è una nuova colonna chiamata generation_number con uno 0 per tutte le righe in cui parent_id = NULL. Perché NULL? Perché so che la persona che è il predecessore di tutte le altre non ha genitori nella tabella. Pertanto, il valore deve essere NULL. Sto usando UNION ALL per unire il risultato di questa istruzione SELECT con la seconda, che sarà responsabile della ricorsione. Affinché UNION ALL funzioni, il numero di colonne e i tipi di dati devono essere gli stessi in entrambe le istruzioni SELECT. Il membro ricorsivo seleziona nuovamente ID e nomi. C'è anche la colonna generation_number con il valore generation_number+1. A ogni ricorsione, verrà aggiunto 1 al valore precedente di questa colonna. Poiché la query inizia con 0, la prima ricorsione produrrà un 1 nella colonna generation_number, la seconda un 2 e così via. Per far funzionare il tutto, ho unito la tabella parent_child con la CTE stessa dove id = parent_id. Si applica lo stesso principio delle tabelle self-joining: la tabella serve come dati sui figli, la CTE serve come dati sui genitori. Dopo aver scritto la CTE, devo utilizzare i suoi dati. L'ho fatto scrivendo una semplice istruzione SELECT che restituisce nomi e numeri di generazione dalla CTE. Bella questa, vero? Ecco come appare il risultato: first_namelast_namegeneration_number RosaWellington0 JonWellington1 JoniWellington1 MargeWellington1 MaryDijkstra2 FrankWellington2 JasonWellington2 BobbyWellington2 SammyWellington2 SarahWellington2 Sam FrancisDijkstra3 StephenWellington3 TrentWellington3 JuneWellington3 JosephineWellington3 SuzyWellington3 Con questo risultato, vedo che Rosa Wellington è il nodo radice perché il suo numero di generazione è 0. Tutte le persone con valore 1 sono i suoi figli, il valore 2 sono i nipoti e il valore 3 sono i pronipoti. Se si controlla la tabella sorgente, si scopre che tutto ciò che ho detto è vero. Esempio 4: Elenco di tutti i discendenti Questo esempio è un'estensione del precedente. Si vuole mostrare come elencare tutti i discendenti di un genitore e mostrare sia i nomi dei genitori che quelli dei figli. Questa è la query: WITH RECURSIVE generation AS ( SELECT id, first_name, last_name, parent_id, 0 AS generation_number FROM parent_child WHERE parent_id IS NULL UNION ALL SELECT child.id, child.first_name, child.last_name, child.parent_id, generation_number+1 AS generation_number FROM parent_child child JOIN generation g ON g.id = child.parent_id ) SELECT g.first_name AS child_first_name, g.last_name AS child_last_name, g.generation_number, parent.first_name AS parent_first_name, parent.last_name AS parent_last_name FROM generation g JOIN parent_child parent ON g.parent_id = parent.id ORDER BY generation_number; Se si confronta questa query con quella precedente, si noterà che la parte CTE è identica. Non c'è bisogno di ripeterla. Ciò che è diverso è l'istruzione SELECT che fa riferimento alla CTE. Ma anche qui non ci sono nuovi concetti SQL. La query seleziona i nomi dei figli e dei genitori e il loro numero di generazione. L'ho fatto unendo di nuovo la CTE con la tabella parent_child. La CTE contiene i dati dei figli, mentre la tabella contiene i dati dei genitori. L'ultima riga di codice ordina il risultato in base al numero di generazione. La query restituisce esattamente ciò che volevo: child_first_namechild_last_namegeneration_numberparent_first_nameparent_last_name MargeWellington1RosaWellington JoniWellington1RosaWellington JonWellington1RosaWellington FrankWellington2JonWellington MaryDijkstra2JonWellington JasonWellington2JoniWellington SarahWellington2MargeWellington SammyWellington2MargeWellington BobbyWellington2MargeWellington Sam FrancisDijkstra3MaryDijkstra TrentWellington3FrankWellington StephenWellington3FrankWellington SuzyWellington3SammyWellington JosephineWellington3SammyWellington JuneWellington3SammyWellington O forse no? Certo, mostra ogni figlio e il nome del suo genitore. Ma manca Rosa Wellington, il nodo principale e la matriarca di questa famiglia. E non ho applicato alcun filtro per escluderla. Che cosa è successo? In realtà ho applicato un filtro utilizzando JOIN nell'ultima istruzione SELECT. Ricordate che JOIN restituisce solo le righe corrispondenti delle tabelle unite. Rosa Wellington manca perché non ha dati sul suo genitore; nel suo caso, non ci sono dati in cui id possa corrispondere a parent_id. Se si vuole includere anche lei, utilizzare LEFT JOIN nell'ultimo SELECT: … FROM generation g LEFT JOIN parent_child parent ON g.parent_id = parent.id … Il risultato completo è qui: child_first_namechild_last_namegeneration_numberparent_first_nameparent_last_name RosaWellington0NULLNULL JoniWellington1RosaWellington JonWellington1RosaWellington MargeWellington1RosaWellington MaryDijkstra2JonWellington JasonWellington2JoniWellington SarahWellington2MargeWellington SammyWellington2MargeWellington BobbyWellington2MargeWellington FrankWellington2JonWellington TrentWellington3FrankWellington StephenWellington3FrankWellington SuzyWellington3SammyWellington JosephineWellington3SammyWellington JuneWellington3SammyWellington Sam FrancisDijkstra3MaryDijkstra Se volete saperne di più su questa query complessa, ecco un articolo dedicato a questo esempio. Esempio 5: Generare una vista ad albero di dati gerarchici L'ultimo esempio è il più complesso, ma anche il più divertente. O almeno lo è il suo risultato. Sarebbe un peccato interrogare le strutture ad albero senza poter mostrare i dati in una qualche forma di albero. In questo caso, il compito è quello di mostrare ogni persona della tabella. Inoltre, ogni discendente deve essere mostrato in modo che sia graficamente evidente di chi è figlio e a quale generazione appartiene. Questa è una vista ad albero. Credo sia meglio che aspettiate di arrivare all'output della query per capire cosa intendo. Mettiamoci al lavoro! Ancora una volta, la CTE ricorsiva salva la situazione: WITH RECURSIVE tree_view AS ( SELECT id, parent_id, first_name, last_name, 0 AS level, CAST(id AS varchar(50)) AS order_sequence FROM parent_child WHERE parent_id IS NULL UNION ALL SELECT parent.id, parent.parent_id, parent.first_name, parent.last_name, level + 1 AS level, CAST(order_sequence || '_' || CAST(parent.id AS VARCHAR (50)) AS VARCHAR(50)) AS order_sequence FROM parent_child parent JOIN tree_view tv ON parent.parent_id = tv.id ) SELECT RIGHT('------------',level*3) || first_name || ' ' || last_name AS parent_child_tree FROM tree_view ORDER BY order_sequence; Sapete già come funzionano le query ricorsive. Questa volta la CTE si chiama tree_view. La prima istruzione SELECT seleziona alcuni dati dalla tabella in cui parent_id è NULL. C'è la colonna level con il valore 0. Ho usato la funzione CAST() per cambiare il tipo di dati id in VARCHAR; capirete perché ne ho bisogno. Utilizziamo nuovamente UNION ALL per unire i risultati di due query. La seconda istruzione SELECT seleziona nuovamente alcuni dati, con la tabella parent_child unita alla CTE stessa. La cosa importante è che a ogni ricorsione verrà aggiunto 1 al livello precedente. Inoltre, il trattino basso e il valore della colonna id saranno aggiunti a ogni ricorsione. Questo piccolo trucco mi serve perché in seguito utilizzerò questa colonna per ordinare l'output. In questo modo, mostrerò correttamente la vista ad albero. Per essere sicuri di capire, ecco una riga della tabella: idfirst_namelast_nameparent_idorder_sequence 1RosaWellingtonNULL1 2JonWellington11_2 6FrankWellington21_2_6 Il valore della colonna per Frank Wellington sarà 1_2_6. Perché? Perché Rosa, come primo livello, riceve il valore 1. Jon Wellington è suo figlio; il suo ID va all'indirizzo order_sequence, che ora diventa 1_2. Poi viene aggiunto l'ID di Frank, che diventa 1_2_6. Procedendo in questo modo lungo tutta la struttura gerarchica, ottengo la colonna che posso utilizzare per mostrare l'output nel modo desiderato. Torniamo alla query. Per ottenere il risultato, è necessario un SELECT che utilizzi i dati CTE. Qui sto usando la funzione RIGHT(). Estrae un numero specifico di caratteri dalla destra. Nel mio caso, rimuove il numero di trattini del livello*3 per ogni livello. Ho anche concatenato questi trattini con il nome e il cognome. Il risultato è ordinato in base a order_sequence. Siete pronti a vedere la vista ad albero? Eccola qui: parent_child_tree Rosa Wellington ---Jon Wellington ------Mary Dijkstra ---------Sam Francis Dijkstra ------Frank Wellington ---------Stephen Wellington ---------Trent Wellington ---Joni Wellington ------Jason Wellington ---Marge Wellington ------Sarah Wellington ------Bobby Wellington ------Sammy Wellington ---------June Wellington ---------Josephine Wellington ---------Suzy Wellington Questa semplice rappresentazione grafica mostra chiaramente i livelli generazionali e chi è chi in questo albero genealogico. Dal numero di trattini, si può facilmente notare che Jon, Joni e Marge Wellington sono figli di Rosa. Mary Dijkstra, Frank, Jason, Sarah, Bobby e Sammy Wellington sono i nipoti di Rosa. È anche facile vedere chi sono i loro genitori. Si può anche vedere chi sono i pronipoti, ma questo lo lascio a voi. Prima di concludere, vorrei anche raccomandare questo articolo su come funziona l'interrogazione delle strutture ad albero in Oracle. L'interrogazione delle strutture ad albero genitore-figlio diventa sempre più interessante Le strutture ad albero genitore-figlio sono piuttosto interessanti. Si tratta di un insieme di dati completamente diverso da quello che di solito si interroga nei database relazionali. Vi ho mostrato cosa sono queste strutture gerarchiche e come vengono rappresentate in una tabella. Soprattutto, vi ho mostrato cinque query che potete utilizzare per risolvere alcuni dei problemi più comuni relativi ai dati gerarchici. Come avete visto, le CTE e le CTE ricorsive sono fondamentali per interrogare gli alberi padre-figlio. Sicuramente vi sarete già imbattuti in dati gerarchici nel vostro lavoro. Probabilmente vi sarete resi conto di dovervi dotare di una conoscenza dettagliata delle query ricorsive per affrontare tali dati. Abbiamo un corso suQuery ricorsive che vi guiderà sistematicamente attraverso le CTE in generale, le query ricorsive e le modalità di interrogazione di dati gerarchici e grafici in SQL. Buona fortuna con l'apprendimento! E sentitevi liberi di utilizzare tutte le query che vi ho mostrato e di adattarle alle vostre esigenze aziendali. Tags: sql imparare sql cte