La ricorsione in SQL sembra strana, finché non la vedi
La maggior parte delle query restituisce righe da dati che esistono già. Una CTE ricorsiva è diversa: costruisce righe rimettendo in ingresso il proprio output, un passo alla volta, finché non c'è più niente di nuovo da aggiungere. È così che percorri un albero di profondità sconosciuta, o generi i numeri da 1 a 100 senza una tabella di numeri.
La struttura è sempre la stessa:
WITH RECURSIVE name(columns) AS (
-- ancoraggio: le righe di partenza
SELECT ...
UNION ALL
-- ricorsiva: righe derivate dal passo precedente
SELECT ... FROM name WHERE ...
)
SELECT * FROM name;
Ancoraggio sopra, UNION ALL, query ricorsiva sotto. SQLite esegue l'ancoraggio una volta, poi continua a eseguire la parte ricorsiva, usando ogni volta le righe prodotte al giro precedente, finché non restituisce più righe nuove. A quel punto si ferma.
Contare da 1 a 10
La CTE ricorsiva più semplice genera una serie. Non servono tabelle:
Seguila passo per passo:
- L'ancoraggio produce una riga:
n = 1. - Il passo ricorsivo prende quella riga, calcola
n + 1 = 2e, dato che2 < 10è vero, tiene la riga. - L'iterazione successiva prende
n = 2e producen = 3. E così via. - Quando
narriva a10,10 < 10è falso, il passo ricorsivo non restituisce righe e SQLite si ferma.
WHERE n < 10 è la condizione di arresto. Senza, la query va avanti all'infinito.
Generare una serie di date
Stessa idea, utile nei report reali: riempire ogni giorno di un intervallo, anche quelli in cui non è successo niente:
Di solito fai un LEFT JOIN di questa serie con una tabella di eventi per contare correttamente i giorni senza eventi. Un semplice GROUP BY date salta del tutto i giorni vuoti; la serie di date ti dà una riga per ogni giorno, comunque vada.
Percorrere un albero padre-figlio
Il caso d'uso classico. Ecco una tabella di dipendenti in cui ogni riga punta al proprio responsabile:
L'ancoraggio sceglie la radice (la persona senza responsabile). Il passo ricorsivo unisce di nuovo la tabella dei dipendenti alla CTE, trovando tutti quelli il cui manager_id corrisponde a un id già presente nella CTE. Ogni iterazione scende di un livello. depth è solo un contatore che aggiungiamo per indentare l'output.
Funziona per alberi di qualsiasi profondità. Due livelli, dieci livelli: la query non cambia.
Trovare tutti gli antenati di una riga specifica
Inverti la direzione. Invece di scendere dalla radice, risali da un dipendente specifico per trovare l'intera catena dei suoi responsabili:
L'ancoraggio è il dipendente di partenza. Ogni passo ricorsivo salta al genitore. SQLite si ferma quando arriva alla radice: manager_id IS NULL, quindi il join non trova niente.
Questo schema è utile per breadcrumb, commenti annidati, percorsi di categoria e ovunque tu debba "risalire fino in cima".
Condizioni di arresto e cicli infiniti
L'errore più comune è dimenticare la condizione di arresto o scriverne una che non scatta mai. Confronta:
-- Va avanti all'infinito:
WITH RECURSIVE bad(n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM bad
)
SELECT n FROM bad;
Non c'è nessuna clausola WHERE che prima o poi restituisca zero righe. SQLite proverà volentieri a contare fino all'infinito.
Due abitudini difensive:
- Metti sempre nella parte ricorsiva una clausola
WHEREche limiti la crescita. - Aggiungi
LIMITallaSELECTesterna come rete di sicurezza mentre sviluppi: se sbagli la condizione di arresto, la query termina comunque.
La CTE in sé è illimitata, ma LIMIT 5 ferma presto la query esterna. SQLite è abbastanza furbo da non continuare la ricorsione oltre quello che serve a LIMIT. Utile per esplorare; non sostituisce una vera condizione di arresto nel codice di produzione.
Cicli nei grafi
Gli alberi non possono avere cicli. I grafi in generale sì, e una CTE ricorsiva ingenua girerà all'infinito se i dati ne contengono uno. La soluzione è tenere traccia del percorso fatto finora e rifiutarsi di visitare di nuovo i nodi:
path è una stringa di nodi già visitati separati da virgole. Prima di aggiungere un nuovo nodo, la clausola WHERE verifica che non sia già lì. Senza questa protezione, il ciclo 1 → 2 → 3 → 1 andrebbe avanti per sempre.
In SQL non esiste un "insieme dei visitati" integrato: te lo costruisci da solo, di solito come stringa o facendo un join con la CTE costruita fin lì.
CTE ricorsiva o self join
Se ti servono solo uno o due livelli di profondità, un self join è più semplice e più veloce:
Così gestisci "chi è il responsabile diretto di ogni persona". Ma se ti serve "tutti quelli che fanno capo ad Ada, a qualsiasi profondità", cioè con una profondità sconosciuta, solo una CTE ricorsiva lo gestisce in modo pulito. Scegli lo strumento in base alla profondità che ti serve:
- Profondità fissa e piccola: self join, magari due o tre.
- Profondità sconosciuta o arbitraria:
WITH RECURSIVE.
Modello mentale
Una CTE ricorsiva è un ciclo scritto in modo dichiarativo:
- L'ancoraggio è il valore iniziale del ciclo.
- La query ricorsiva è il corpo del ciclo: produce il gruppo successivo di righe a partire da quelle attuali.
- La condizione di arresto è il test di uscita del ciclo: quando restituisce zero righe, il ciclo finisce.
UNION ALLaccumula tutto nel risultato finale.
Una volta che questa corrispondenza ti è chiara, la sintassi smette di sembrarti strana. Stai scrivendo un ciclo for in SQL.
Prossimo passo: gli indici
Le CTE ricorsive percorrono molte righe, e il join dentro il passo ricorsivo viene eseguito a ogni iterazione. Se la colonna del join non è indicizzata, le prestazioni crollano in fretta. Gli indici sono il prossimo capitolo, e manager_id è proprio il tipo di colonna che ne trae vantaggio.
Domande frequenti
Cos'è una CTE ricorsiva in SQLite?
Una CTE ricorsiva è una query WITH RECURSIVE che costruisce un risultato facendo riferimento a se stessa più volte. Ha due parti unite da UNION ALL: una query di ancoraggio che produce le righe di partenza e una query ricorsiva che produce altre righe a partire dal passo precedente. SQLite continua a eseguire la parte ricorsiva finché non restituisce più righe nuove.
Quando conviene usare WITH RECURSIVE in SQLite?
Usala quando devi percorrere un albero o un grafo (dipendenti e responsabili, categorie e sottocategorie, commenti annidati) o generare una sequenza (ogni data in un intervallo, i numeri da 1 a 100). I join normali gestiscono uno o due livelli di profondità; una CTE ricorsiva gestisce una profondità qualsiasi senza doverla conoscere in anticipo.
Come evito i cicli infiniti in una CTE ricorsiva di SQLite?
Assicurati che la query ricorsiva abbia una condizione di arresto: una clausola WHERE che prima o poi restituisce zero righe, o un contatore con un limite. Per i grafi con cicli, tieni traccia del percorso visitato in una colonna ed escludi le righe già presenti. Come rete di sicurezza, aggiungi LIMIT alla query esterna, così una ricorsione fuori controllo non può riempire la memoria.