📋 ITAG Templates · Catalogue 30+ templates pro · Voir tout
← Retour au catalogue 📥 Télécharger ce template

🌳 CTE récursives — SQL (org chart, BOM, graph)

Format : SQL · 12 patterns · PostgreSQL + Snowflake + BigQuery Auteur : Équipe pédagogique ITAG · Data Pack Mise à jour : 2026


🎯 Description

Pack de CTE récursives (Common Table Expressions) pour traverser des hiérarchies et graphes en SQL pur. Org charts, Bills of Materials (BOM), arbres de catégories, graphe de relations, génération de séries.

📋 Patterns inclus (12)

Hiérarchies organisationnelles (3)

  1. Org chart top-down — du PDG aux feuilles
  2. Path to root — chemin du collaborateur jusqu'au PDG
  3. Niveau hiérarchique + count subordinates — managers vs IC

Bills of Materials (3)

  1. Explosion BOM — composants d'un produit fini
  2. Implosion BOM — où est utilisé un composant
  3. Coût total composant — somme récursive

Catégories produits / contenu (2)

  1. Tree categories — arbre catégories e-commerce
  2. Breadcrumbs — chemin "Home > Cat > SubCat > Produit"

Graph traversal (2)

  1. Friend-of-friend — réseau social 2e degré (avec cycle detection)
  2. Path entre deux nœuds — shortest path simplifié

Génération de séries (2)

  1. Calendar generation — table dim_date 10 ans
  2. Number series — split de strings, fenêtres glissantes

🧠 Concepts couverts

💾 Code SQL complet

Données de test — Org chart

-- Table employees : hiérarchie organisationnelle
CREATE TABLE employees (
    id         INT PRIMARY KEY,
    name       VARCHAR(100) NOT NULL,
    manager_id INT REFERENCES employees(id),
    dept       VARCHAR(50)
);

INSERT INTO employees (id, name, manager_id, dept) VALUES (1, 'Alice Martin', NULL, 'Direction'), (2, 'Bob Dupont', 1, 'Tech'), (3, 'Claire Morel', 1, 'Sales'), (4, 'David Leroy', 2, 'Tech'), (5, 'Eva Blanc', 2, 'Tech'), (6, 'Frank Girard', 3, 'Sales'), (7, 'Grace Petit', 4, 'Tech'), (8, 'Hugo Renard', 4, 'Tech'), (9, 'Inès Laurent', 3, 'Sales'), (10, 'Jules Bernard', 6, 'Sales');

Pattern 1 — Org chart top-down (du PDG aux feuilles)

-- Traversal descendant depuis la racine (manager_id IS NULL)
WITH RECURSIVE org_tree AS (
    -- Anchor : le PDG (pas de manager)
    SELECT
        id,
        name,
        manager_id,
        dept,
        0                        AS level,
        CAST(name AS TEXT)       AS path,
        ARRAY[id]                AS ancestors
    FROM employees
    WHERE manager_id IS NULL

UNION ALL

-- Partie récursive : on descend d'un niveau SELECT e.id, e.name, e.manager_id, e.dept, ot.level + 1, ot.path || ' > ' || e.name, ot.ancestors || e.id FROM employees e JOIN org_tree ot ON e.manager_id = ot.id ) SELECT level, REPEAT(' ', level) || name AS org_chart, dept, path FROM org_tree ORDER BY path;

Pattern 2 — Path to root (chemin jusqu'au PDG)

-- Remonte la hiérarchie depuis un employé donné jusqu'à la racine
WITH RECURSIVE path_to_root AS (
    -- Anchor : employé de départ (ex. Grace Petit, id=7)
    SELECT
        id,
        name,
        manager_id,
        0 AS depth
    FROM employees
    WHERE id = 7

UNION ALL

-- Partie récursive : on remonte vers le manager SELECT e.id, e.name, e.manager_id, ptr.depth + 1 FROM employees e JOIN path_to_root ptr ON e.id = ptr.manager_id ) SELECT depth, name, CASE WHEN manager_id IS NULL THEN 'PDG' ELSE 'Employé' END AS role FROM path_to_root ORDER BY depth DESC; -- Affiche PDG en premier

Pattern 3 — Niveau hiérarchique + compte des subordonnés

-- Niveau de chaque employé + nombre total de subordonnés (directs + indirects)
WITH RECURSIVE org_levels AS (
    SELECT id, name, manager_id, 0 AS level
    FROM employees
    WHERE manager_id IS NULL

UNION ALL

SELECT e.id, e.name, e.manager_id, ol.level + 1 FROM employees e JOIN org_levels ol ON e.manager_id = ol.id ), subordinate_counts AS ( -- Compte récursif des subordonnés pour chaque manager SELECT manager_id AS id, COUNT(*) AS direct_reports FROM employees WHERE manager_id IS NOT NULL GROUP BY manager_id ) SELECT ol.name, ol.level, COALESCE(sc.direct_reports, 0) AS direct_reports, CASE WHEN sc.direct_reports > 0 THEN 'Manager' ELSE 'IC' END AS role_type FROM org_levels ol LEFT JOIN subordinate_counts sc ON ol.id = sc.id ORDER BY ol.level, ol.name;


Données de test — BOM (Bill of Materials)

-- Table components : composants d'un produit fini
CREATE TABLE components (
    id        INT PRIMARY KEY,
    name      VARCHAR(100) NOT NULL,
    parent_id INT REFERENCES components(id),
    qty       NUMERIC(10,2) DEFAULT 1,
    unit_cost NUMERIC(10,4) DEFAULT 0
);

INSERT INTO components (id, name, parent_id, qty, unit_cost) VALUES (1, 'Vélo complet', NULL, 1, 0), (2, 'Cadre', 1, 1, 80.00), (3, 'Groupe transmission',1, 1, 0), (4, 'Roue avant', 1, 1, 0), (5, 'Roue arrière', 1, 1, 0), (6, 'Dérailleur avant', 3, 1, 25.00), (7, 'Dérailleur arrière',3, 1, 35.00), (8, 'Chaîne', 3, 1, 12.00), (9, 'Jante avant', 4, 1, 18.00), (10, 'Pneu avant', 4, 1, 8.00), (11, 'Rayons avant', 4, 32, 0.50), (12, 'Jante arrière', 5, 1, 18.00), (13, 'Pneu arrière', 5, 1, 8.00), (14, 'Rayons arrière', 5, 32, 0.50);

Pattern 4 — Explosion BOM (composants d'un produit fini)

-- Liste tous les composants d'un produit, avec niveau et quantité cumulée
WITH RECURSIVE bom_explosion AS (
    SELECT
        id,
        name,
        parent_id,
        qty,
        unit_cost,
        0             AS level,
        qty           AS total_qty,
        CAST(name AS TEXT) AS bom_path
    FROM components
    WHERE id = 1  -- Produit racine : Vélo complet

UNION ALL

SELECT c.id, c.name, c.parent_id, c.qty, c.unit_cost, bom.level + 1, bom.total_qty * c.qty, -- Quantité cumulée en descendant bom.bom_path || ' > ' || c.name FROM components c JOIN bom_explosion bom ON c.parent_id = bom.id ) SELECT level, REPEAT(' ', level) || name AS component, qty AS unit_qty, total_qty AS cumulative_qty, unit_cost, total_qty * unit_cost AS extended_cost FROM bom_explosion ORDER BY bom_path;

Pattern 5 — Implosion BOM (où est utilisé un composant)

-- Remonte pour trouver tous les produits finis qui utilisent un composant donné
WITH RECURSIVE bom_implosion AS (
    SELECT id, name, parent_id, qty, 0 AS level
    FROM components
    WHERE id = 14  -- Composant : Rayons arrière

UNION ALL

SELECT c.id, c.name, c.parent_id, c.qty, bi.level + 1 FROM components c JOIN bom_implosion bi ON c.id = bi.parent_id ) SELECT level, REPEAT(' ', level) || name AS parent_component, qty FROM bom_implosion ORDER BY level DESC;

Pattern 6 — Coût total récursif d'un composant

-- Calcule le coût total récursif de chaque sous-ensemble
WITH RECURSIVE cost_rollup AS (
    -- Feuilles : pas d'enfants, coût = unit_cost * qty
    SELECT
        c.id,
        c.name,
        c.parent_id,
        c.qty,
        c.unit_cost,
        c.unit_cost * c.qty  AS total_cost
    FROM components c
    WHERE NOT EXISTS (
        SELECT 1 FROM components child WHERE child.parent_id = c.id
    )

UNION ALL

SELECT p.id, p.name, p.parent_id, p.qty, p.unit_cost, p.unit_cost p.qty + SUM(cr.total_cost p.qty) OVER (PARTITION BY p.id) FROM components p JOIN cost_rollup cr ON cr.parent_id = p.id ) -- Coût total du produit fini SELECT name, unit_cost, SUM(total_cost) AS bom_total_cost FROM cost_rollup GROUP BY id, name, unit_cost ORDER BY bom_total_cost DESC;


Données de test — Categories

CREATE TABLE categories (
    id        INT PRIMARY KEY,
    name      VARCHAR(100) NOT NULL,
    parent_id INT REFERENCES categories(id),
    slug      VARCHAR(100)
);

INSERT INTO categories (id, name, parent_id, slug) VALUES (1, 'Tout', NULL, 'tout'), (2, 'Électronique', 1, 'electronique'), (3, 'Mode', 1, 'mode'), (4, 'Téléphones', 2, 'telephones'), (5, 'Ordinateurs', 2, 'ordinateurs'), (6, 'Smartphones', 4, 'smartphones'), (7, 'Accessoires', 4, 'accessoires'), (8, 'Laptops', 5, 'laptops'), (9, 'Hommes', 3, 'hommes'), (10, 'Femmes', 3, 'femmes');

Pattern 7 — Tree categories (arbre e-commerce)

WITH RECURSIVE cat_tree AS (
    SELECT id, name, parent_id, slug, 0 AS depth,
           ARRAY[id] AS path_ids
    FROM categories
    WHERE parent_id IS NULL

UNION ALL

SELECT c.id, c.name, c.parent_id, c.slug, ct.depth + 1, ct.path_ids || c.id FROM categories c JOIN cat_tree ct ON c.parent_id = ct.id ) SELECT depth, REPEAT('── ', depth) || name AS tree, slug, depth + 1 AS level, cardinality(path_ids) AS total_levels FROM cat_tree ORDER BY path_ids;

Pattern 8 — Breadcrumbs (Home > Cat > SubCat > Produit)

WITH RECURSIVE breadcrumb AS (
    SELECT id, name, parent_id, slug,
           CAST(name AS TEXT) AS crumb,
           CAST(slug AS TEXT) AS url_path
    FROM categories
    WHERE id = 6  -- Cible : Smartphones

UNION ALL

SELECT c.id, c.name, c.parent_id, c.slug, c.name || ' > ' || b.crumb, c.slug || '/' || b.url_path FROM categories c JOIN breadcrumb b ON c.id = b.parent_id ) SELECT crumb AS breadcrumb, url_path FROM breadcrumb WHERE parent_id IS NULL;


Données de test — Graph

CREATE TABLE edges (
    from_id INT,
    to_id   INT,
    weight  NUMERIC(5,2) DEFAULT 1
);

INSERT INTO edges VALUES (1, 2, 1.0), (1, 3, 4.0), (2, 3, 2.0), (2, 4, 5.0), (3, 4, 1.0), (3, 5, 3.0), (4, 5, 2.0), (4, 6, 6.0), (5, 6, 1.0), -- Relation bidirectionnelle (réseau social non orienté) (2, 1, 1.0), (3, 1, 4.0), (3, 2, 2.0), (4, 2, 5.0), (4, 3, 1.0), (5, 3, 3.0);

Pattern 9 — Friend-of-friend avec cycle detection

-- Trouve tous les nœuds accessibles depuis un nœud donné (avec détection des cycles)
WITH RECURSIVE graph_walk AS (
    SELECT
        from_id       AS start_node,
        to_id         AS current_node,
        weight        AS total_weight,
        1             AS hops,
        ARRAY[from_id, to_id] AS visited  -- Cycle detection via array
    FROM edges
    WHERE from_id = 1  -- Point de départ

UNION ALL

SELECT gw.start_node, e.to_id, gw.total_weight + e.weight, gw.hops + 1, gw.visited || e.to_id FROM edges e JOIN graph_walk gw ON e.from_id = gw.current_node WHERE e.to_id <> ALL(gw.visited) -- Évite les cycles AND gw.hops < 5 -- Profondeur max ) SELECT start_node, current_node AS reachable_node, hops, ROUND(total_weight::NUMERIC, 2) AS total_weight FROM graph_walk ORDER BY hops, total_weight;

Pattern 10 — Shortest path entre deux nœuds

-- Chemin le plus court (Dijkstra simplifié) entre nœud 1 et nœud 6
WITH RECURSIVE shortest_path AS (
    SELECT
        from_id,
        to_id,
        weight            AS cost,
        ARRAY[from_id, to_id] AS path,
        1 AS hops
    FROM edges
    WHERE from_id = 1

UNION ALL

SELECT sp.from_id, e.to_id, sp.cost + e.weight, sp.path || e.to_id, sp.hops + 1 FROM edges e JOIN shortest_path sp ON e.from_id = sp.to_id WHERE e.to_id <> ALL(sp.path) AND sp.hops < 10 ) SELECT path, ROUND(cost::NUMERIC, 2) AS total_cost, hops FROM shortest_path WHERE to_id = 6 ORDER BY cost LIMIT 1;


Pattern 11 — Calendar generation (dim_date 10 ans)

-- Génère une table calendrier sur 10 ans sans table physique
WITH RECURSIVE calendar AS (
    SELECT CAST('2020-01-01' AS DATE) AS d

UNION ALL

SELECT d + INTERVAL '1 day' FROM calendar WHERE d < '2029-12-31' ) SELECT TO_CHAR(d, 'YYYYMMDD')::INT AS date_key, d AS full_date, EXTRACT(YEAR FROM d)::INT AS year, EXTRACT(QUARTER FROM d)::INT AS quarter, EXTRACT(MONTH FROM d)::INT AS month, EXTRACT(WEEK FROM d)::INT AS week, TO_CHAR(d, 'Day') AS day_name, EXTRACT(DOW FROM d)::INT AS day_of_week, CASE WHEN EXTRACT(DOW FROM d) IN (0,6) THEN TRUE ELSE FALSE END AS is_weekend FROM calendar ORDER BY d;

Pattern 12 — Split de string et génération de séries

-- Divise une chaîne CSV en lignes individuelles via CTE récursive
WITH RECURSIVE split_string AS (
    -- Chaîne source à splitter
    SELECT
        'SQL,Python,dbt,Spark,Airflow,Kafka'::TEXT AS str,
        1                                            AS pos,
        ''::TEXT                                     AS token
    UNION ALL
    SELECT
        str,
        pos + 1,
        SPLIT_PART(str, ',', pos)
    FROM split_string
    WHERE SPLIT_PART(str, ',', pos) <> ''
)
SELECT
    pos - 1  AS index,
    token    AS value
FROM split_string
WHERE token <> ''
ORDER BY pos;

-- Génération d'une série de nombres (alternative à generate_series) WITH RECURSIVE num_series AS ( SELECT 1 AS n UNION ALL SELECT n + 1 FROM num_series WHERE n < 100 ) SELECT n, n * n AS squared, SQRT(n::FLOAT)::NUMERIC(6,4) AS sqrt_n FROM num_series;

💼 Cas d'usage

📥 Télécharger ce template

Télécharger le fichier .md · ← Retour catalogue · Parcours Data →


ITAG · Patterns inspirés du standard SQL:1999 et SQL:2003.

Template prêt pour vous

Tous les templates ITAG sont produits par notre équipe pédagogique. Téléchargement gratuit, usage libre.

📋 Catalogue complet 📅 Coach 1:1