Les CTE récursives sont l'une des fonctionnalités les plus puissantes de SQL, permettant de travailler avec des données hiérarchiques et des structures arborescentes. Dans cette leçon, nous explorerons comment utiliser les expressions de table commune récursives pour interroger des données ayant des relations parent-enfant, telles que les organigrammes, les arborescences de catégories et les nomenclatures.
Une CTE récursive est une expression de table commune qui se référence elle-même, permettant de parcourir des structures de données hiérarchiques. Contrairement aux CTE ordinaires qui s'exécutent une fois, les CTE récursives s'exécutent de manière itérative jusqu'à ce qu'une condition de terminaison soit remplie.
Cas d'utilisation courants des CTE récursives :
La syntaxe générale d'une CTE récursive est :
WITH RECURSIVE nom_cte AS (
-- Membre ancre (cas de base)
SELECT ...
FROM table
WHERE condition
UNION ALL
-- Membre récursif (cas récursif)
SELECT ...
FROM table
JOIN nom_cte ON condition
)
SELECT * FROM nom_cte;
Composants :
Le processus d'exécution :
Commençons par un exemple simple qui génère une séquence de nombres :
WITH RECURSIVE sequence_nombres AS (
-- Membre ancre : commencer avec 1
SELECT 1 AS n
UNION ALL
-- Membre récursif : ajouter 1 à la valeur précédente
SELECT n + 1
FROM sequence_nombres
WHERE n < 10
)
SELECT n
FROM sequence_nombres;
Résultat :
n
--
1
2
3
4
5
6
7
8
9
10
Comment cela fonctionne :
11 + 1 = 22 + 1 = 3n < 10 soit faux10, mais 10 < 10 est faux, donc la récursion s'arrêteCréons une table représentant une structure organisationnelle :
-- Table d'employés exemple
CREATE TABLE employee (
employee_id INT PRIMARY KEY,
employee_name VARCHAR(100),
manager_id INT,
title VARCHAR(100)
);
-- Données exemple
INSERT INTO employee VALUES
(1, 'Alice Johnson', NULL, 'PDG'),
(2, 'Bob Smith', 1, 'VP Ingénierie'),
(3, 'Carol White', 1, 'VP Ventes'),
(4, 'David Brown', 2, 'Responsable Ingénierie'),
(5, 'Eve Davis', 2, 'Responsable Ingénierie'),
(6, 'Frank Miller', 3, 'Responsable Ventes'),
(7, 'Grace Wilson', 4, 'Développeur Senior'),
(8, 'Henry Moore', 4, 'Développeur'),
(9, 'Ivy Taylor', 5, 'Développeur'),
(10, 'Jack Anderson', 6, 'Représentant Commercial');
Pour trouver tous les employés qui relèvent d'un manager spécifique (directement ou indirectement) :
WITH RECURSIVE subordonnes AS (
-- Ancre : Commencer avec le manager
SELECT
employee_id,
employee_name,
manager_id,
title,
0 AS niveau
FROM
employee
WHERE
employee_name = 'Bob Smith'
UNION ALL
-- Récursif : Trouver les rapports directs
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
e.title,
s.niveau + 1
FROM
employee e
INNER JOIN
subordonnes s ON e.manager_id = s.employee_id
)
SELECT
employee_id,
employee_name,
title,
niveau
FROM
subordonnes
ORDER BY
niveau, employee_name;
Résultat :
employee_id | employee_name | title | niveau
------------|-----------------|------------------------|--------
2 | Bob Smith | VP Ingénierie | 0
4 | David Brown | Responsable Ingénierie | 1
5 | Eve Davis | Responsable Ingénierie | 1
7 | Grace Wilson | Développeur Senior | 2
8 | Henry Moore | Développeur | 2
9 | Ivy Taylor | Développeur | 2
Pour afficher la hiérarchie complète depuis le PDG :
WITH RECURSIVE organigramme AS (
-- Ancre : Commencer avec le PDG (pas de manager)
SELECT
employee_id,
employee_name,
manager_id,
title,
0 AS niveau,
CAST(employee_name AS VARCHAR(1000)) AS chemin
FROM
employee
WHERE
manager_id IS NULL
UNION ALL
-- Récursif : Ajouter chaque niveau
SELECT
e.employee_id,
e.employee_name,
e.manager_id,
e.title,
oc.niveau + 1,
CONCAT(oc.chemin, ' > ', e.employee_name)
FROM
employee e
INNER JOIN
organigramme oc ON e.manager_id = oc.employee_id
)
SELECT
REPEAT(' ', niveau) || employee_name AS hierarchie,
title,
niveau,
chemin
FROM
organigramme
ORDER BY
chemin;
Résultat :
hierarchie | title | niveau | chemin
-------------------------------|------------------------|--------|----------------------------------
Alice Johnson | PDG | 0 | Alice Johnson
Bob Smith | VP Ingénierie | 1 | Alice Johnson > Bob Smith
David Brown | Responsable Ingénierie | 2 | Alice Johnson > Bob Smith > David Brown
Grace Wilson | Développeur Senior | 3 | Alice Johnson > Bob Smith > David Brown > Grace Wilson
Henry Moore | Développeur | 3 | Alice Johnson > Bob Smith > David Brown > Henry Moore
Eve Davis | Responsable Ingénierie | 2 | Alice Johnson > Bob Smith > Eve Davis
Ivy Taylor | Développeur | 3 | Alice Johnson > Bob Smith > Eve Davis > Ivy Taylor
Carol White | VP Ventes | 1 | Alice Johnson > Carol White
Frank Miller | Responsable Ventes | 2 | Alice Johnson > Carol White > Frank Miller
Jack Anderson | Représentant Commercial| 3 | Alice Johnson > Carol White > Frank Miller > Jack Anderson
Travaillons avec une hiérarchie de catégories de produits :
-- Table de catégories exemple
CREATE TABLE category (
category_id INT PRIMARY KEY,
category_name VARCHAR(100),
parent_category_id INT
);
-- Données exemple
INSERT INTO category VALUES
(1, 'Électronique', NULL),
(2, 'Ordinateurs', 1),
(3, 'Téléphones', 1),
(4, 'Portables', 2),
(5, 'Bureautique', 2),
(6, 'Portables Gaming', 4),
(7, 'Portables Professionnels', 4),
(8, 'Smartphones', 3),
(9, 'Téléphones Basiques', 3);
Pour trouver toutes les sous-catégories sous "Ordinateurs" :
WITH RECURSIVE arbre_categories AS (
-- Ancre : Commencer avec Ordinateurs
SELECT
category_id,
category_name,
parent_category_id,
0 AS profondeur,
CAST(category_name AS VARCHAR(1000)) AS chemin
FROM
category
WHERE
category_name = 'Ordinateurs'
UNION ALL
-- Récursif : Obtenir toutes les sous-catégories
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
ct.profondeur + 1,
CONCAT(ct.chemin, ' > ', c.category_name)
FROM
category c
INNER JOIN
arbre_categories ct ON c.parent_category_id = ct.category_id
)
SELECT
category_id,
REPEAT(' ', profondeur) || category_name AS hierarchie_categorie,
profondeur,
chemin
FROM
arbre_categories
ORDER BY
chemin;
Résultat :
category_id | hierarchie_categorie | profondeur | chemin
------------|----------------------------|------------|--------------------------------------
2 | Ordinateurs | 0 | Ordinateurs
4 | Portables | 1 | Ordinateurs > Portables
6 | Portables Gaming | 2 | Ordinateurs > Portables > Portables Gaming
7 | Portables Professionnels| 2 | Ordinateurs > Portables > Portables Professionnels
5 | Bureautique | 1 | Ordinateurs > Bureautique
Pour trouver toutes les catégories parentes d'une catégorie spécifique :
WITH RECURSIVE ancetres_categorie AS (
-- Ancre : Commencer avec Portables Gaming
SELECT
category_id,
category_name,
parent_category_id,
0 AS niveau_superieur
FROM
category
WHERE
category_name = 'Portables Gaming'
UNION ALL
-- Récursif : Obtenir les catégories parentes
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
ca.niveau_superieur + 1
FROM
category c
INNER JOIN
ancetres_categorie ca ON c.category_id = ca.parent_category_id
)
SELECT
category_id,
category_name,
niveau_superieur
FROM
ancetres_categorie
ORDER BY
niveau_superieur;
Résultat :
category_id | category_name | niveau_superieur
------------|-------------------|------------------
6 | Portables Gaming | 0
4 | Portables | 1
2 | Ordinateurs | 2
1 | Électronique | 3
Un cas d'utilisation classique des CTE récursives est l'exploration des nomenclatures (pièces et sous-pièces) :
-- Table de pièces exemple
CREATE TABLE parts (
part_id INT PRIMARY KEY,
part_name VARCHAR(100),
quantity INT
);
-- Table de nomenclature exemple
CREATE TABLE bom (
parent_part_id INT,
child_part_id INT,
quantity_needed INT,
PRIMARY KEY (parent_part_id, child_part_id)
);
-- Données exemple
INSERT INTO parts VALUES
(1, 'Vélo', 1),
(2, 'Cadre', 1),
(3, 'Roue', 2),
(4, 'Pneu', 1),
(5, 'Jante', 1),
(6, 'Rayon', 36);
INSERT INTO bom VALUES
(1, 2, 1), -- Vélo nécessite 1 Cadre
(1, 3, 2), -- Vélo nécessite 2 Roues
(3, 4, 1), -- Roue nécessite 1 Pneu
(3, 5, 1), -- Roue nécessite 1 Jante
(5, 6, 36); -- Jante nécessite 36 Rayons
Pour trouver toutes les pièces nécessaires pour construire un vélo :
WITH RECURSIVE explosion_pieces AS (
-- Ancre : Commencer avec le produit de niveau supérieur
SELECT
p.part_id,
p.part_name,
1 AS quantite,
0 AS niveau,
CAST(p.part_name AS VARCHAR(1000)) AS chemin
FROM
parts p
WHERE
p.part_name = 'Vélo'
UNION ALL
-- Récursif : Exploser la nomenclature
SELECT
p.part_id,
p.part_name,
pe.quantite * b.quantity_needed,
pe.niveau + 1,
CONCAT(pe.chemin, ' > ', p.part_name)
FROM
explosion_pieces pe
INNER JOIN
bom b ON pe.part_id = b.parent_part_id
INNER JOIN
parts p ON b.child_part_id = p.part_id
)
SELECT
part_id,
REPEAT(' ', niveau) || part_name AS hierarchie_pieces,
quantite,
niveau,
chemin
FROM
explosion_pieces
ORDER BY
chemin;
Résultat :
part_id | hierarchie_pieces | quantite | niveau | chemin
--------|-------------------|----------|--------|------------------------
1 | Vélo | 1 | 0 | Vélo
2 | Cadre | 1 | 1 | Vélo > Cadre
3 | Roue | 2 | 1 | Vélo > Roue
4 | Pneu | 2 | 2 | Vélo > Roue > Pneu
5 | Jante | 2 | 2 | Vélo > Roue > Jante
6 | Rayon | 72 | 3 | Vélo > Roue > Jante > Rayon
Notez que nous avons besoin de 72 rayons au total : 2 roues × 1 jante par roue × 36 rayons par jante = 72 rayons.
Les CTE récursives peuvent créer des boucles infinies s'il y a des références circulaires dans vos données. Voici des stratégies pour éviter cela :
WITH RECURSIVE recursion_limitee AS (
SELECT
category_id,
category_name,
parent_category_id,
0 AS profondeur
FROM
category
WHERE
parent_category_id IS NULL
UNION ALL
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
lr.profondeur + 1
FROM
category c
INNER JOIN
recursion_limitee lr ON c.parent_category_id = lr.category_id
WHERE
lr.profondeur < 10 -- Limite de profondeur maximum
)
SELECT * FROM recursion_limitee;
WITH RECURSIVE parcours_securise AS (
SELECT
category_id,
category_name,
parent_category_id,
ARRAY[category_id] AS ids_visites
FROM
category
WHERE
parent_category_id IS NULL
UNION ALL
SELECT
c.category_id,
c.category_name,
c.parent_category_id,
st.ids_visites || c.category_id
FROM
category c
INNER JOIN
parcours_securise st ON c.parent_category_id = st.category_id
WHERE
NOT (c.category_id = ANY(st.ids_visites)) -- Prévenir les cycles
)
SELECT * FROM parcours_securise;
Toujours indexer les colonnes utilisées dans les jointures récursives :
CREATE INDEX idx_employee_manager ON employee(manager_id);
CREATE INDEX idx_category_parent ON category(parent_category_id);
Utilisez des clauses WHERE pour limiter la portée de la récursion :
WITH RECURSIVE subordonnes AS (
SELECT employee_id, employee_name, manager_id, 0 AS niveau
FROM employee
WHERE employee_name = 'Bob Smith'
UNION ALL
SELECT e.employee_id, e.employee_name, e.manager_id, s.niveau + 1
FROM employee e
INNER JOIN subordonnes s ON e.manager_id = s.employee_id
WHERE s.niveau < 3 -- Aller seulement 3 niveaux de profondeur
)
SELECT * FROM subordonnes;
INNER JOIN lorsque vous voulez seulement des lignes correspondantesLEFT JOIN lorsque vous voulez inclure des nœuds feuilles sans enfantsUn modèle courant d'application web est les commentaires imbriqués ou les fils de forum :
CREATE TABLE comments (
comment_id INT PRIMARY KEY,
parent_comment_id INT,
user_name VARCHAR(100),
comment_text TEXT,
created_at TIMESTAMP
);
WITH RECURSIVE fil_commentaires AS (
-- Ancre : Commentaires de premier niveau
SELECT
comment_id,
parent_comment_id,
user_name,
comment_text,
0 AS profondeur,
CAST(comment_id AS VARCHAR(1000)) AS chemin_tri
FROM
comments
WHERE
parent_comment_id IS NULL
UNION ALL
-- Récursif : Réponses imbriquées
SELECT
c.comment_id,
c.parent_comment_id,
c.user_name,
c.comment_text,
ct.profondeur + 1,
CONCAT(ct.chemin_tri, '-', LPAD(c.comment_id::TEXT, 10, '0'))
FROM
comments c
INNER JOIN
fil_commentaires ct ON c.parent_comment_id = ct.comment_id
)
SELECT
REPEAT(' ', profondeur) || user_name AS utilisateur_indente,
comment_text,
profondeur
FROM
fil_commentaires
ORDER BY
chemin_tri;
Les CTE récursives sont un outil essentiel pour travailler avec des données structurées en arbre et hiérarchiques en SQL. Elles transforment des opérations complexes multi-requêtes en solutions élégantes à requête unique.
Dans le module suivant, nous explorerons les fonctions de fenêtrage pour une analyse de données avancée.