Skip to Content

Snippets SQL

Bouts de code SQL réutilisables, MySQL / PostgreSQL / SQLite.

Info : Pour un référentiel plus complet (pièges, auto-jointures, CTEs récursives, fonctions fenêtrées avancées), voir Requêtes SQL — Modèles courants.

📐 Formatage de chaîne et dates

Concaténation

-- PostgreSQL SELECT prenom || ' ' || nom AS nom_complet FROM utilisateurs; -- MySQL SELECT CONCAT(prenom, ' ', nom) AS nom_complet FROM utilisateurs; -- SQLite SELECT prenom || ' ' || nom AS nom_complet FROM utilisateurs;

Gestion des NULL

-- Coalesce : retourne la première valeur non-null SELECT nom, COALESCE(telephone, email, 'N/C') AS moyen_contact FROM utilisateurs; -- NULLIF : évite les divisions par zéro SELECT montant / NULLIF(nb_clients, 0) AS par_client FROM statistiques;

Formatage de date

-- PostgreSQL SELECT TO_CHAR(date_cree, 'YYYY-MM-DD') AS date_fmt FROM users; -- MySQL SELECT DATE_FORMAT(date_cree, '%Y-%m-%d') AS date_fmt FROM users;

Piège : DATE_FORMAT (MySQL) et TO_CHAR (PostgreSQL) utilisent des formats différents. Vérifier la base cible avant de copier-coller.

🔗 Jointures essentielles

INNER JOIN

SELECT u.nom, o.total FROM utilisateurs u INNER JOIN commandes o ON u.id = o.utilisateur_id WHERE o.cree_a > '2024-01-01';

LEFT JOIN avec filtre sur la table droite

SELECT u.nom, COUNT(o.id) AS nb_commandes FROM utilisateurs u LEFT JOIN commandes o ON u.id = o.utilisateur_id AND o.cree_a > '2024-01-01' GROUP BY u.id, u.nom;

Attention : placer le filtre dans ON (pas dans WHERE), sinon le LEFT JOIN se comporte comme un INNER JOIN.

CROSS JOIN

-- Chaque client × chaque produit SELECT c.nom, p.nom AS produit FROM clients c CROSS JOIN produits p;

SELF JOIN

-- Paires d'employés du même département SELECT e1.nom AS emp1, e2.nom AS emp2, e1.departement FROM employes e1 INNER JOIN employes e2 ON e1.departement = e2.departement AND e1.id < e2.id;

Règle : aliaser systématiquement chaque occurrence de la table.

🔷 CTE (WITH)

CTE simple

WITH commandes_annuelles AS ( SELECT EXTRACT(YEAR FROM cree_a) AS annee, COUNT(*) AS total FROM commandes GROUP BY EXTRACT(YEAR FROM cree_a) ) SELECT annee, total FROM commandes_annuelles ORDER BY annee;

CTE récursif (arborescence)

WITH RECURSIVE arbo AS ( SELECT id, nom, parent_id, 1 AS niveau FROM categories WHERE parent_id IS NULL UNION ALL SELECT c.id, c.nom, c.parent_id, a.niveau + 1 FROM categories c INNER JOIN arbo a ON c.parent_id = a.id ) SELECT * FROM arbo ORDER BY niveau, nom;

📊 Fonctions fenêtrées

Classement

SELECT nom, departement, salaire, ROW_NUMBER() OVER (PARTITION BY departement ORDER BY salaire DESC) AS rang FROM employes;

ROW_NUMBER attribue un numéro unique sans trou. RANK() laisse des égalités, DENSE_RANK() ne creuse pas les trous.

Décalage temporel

SELECT date, valeur, LAG(valeur) OVER (ORDER BY date) AS valeur_precedente, LEAD(valeur) OVER (ORDER BY date) AS valeur_suivante, valeur - LAG(valeur) OVER (ORDER BY date) AS delta FROM mesures;

Total cumulé

SELECT date, ventes_jour, SUM(ventes_jour) OVER (ORDER BY date) AS cumul FROM ventes;

Contrairement à GROUP BY, SUM() OVER () conserve les lignes individuelles. Idéal pour les graphiques temporels.

⚙️ Opérations courantes

CASE WHEN

-- Agrégation conditionnelle SELECT departement, COUNT(*) AS total, SUM(CASE WHEN statut = 'actif' THEN 1 ELSE 0 END) AS actifs FROM employes GROUP BY departement; -- Tri conditionnel SELECT nom, role, anciennete FROM employes ORDER BY CASE role WHEN 'manager' THEN 1 WHEN 'lead' THEN 2 ELSE 3 END, anciennete DESC;

Piège : CASE WHEN évalue les WHEN dans l’ordre. Le premier match gagne. Mettre le cas par défaut (ELSE) en dernier.

Pagination LIMIT / OFFSET

-- Page 1, 25 éléments par page SELECT id, titre, cree_a FROM articles ORDER BY cree_a DESC LIMIT 25 OFFSET 0; -- Page 2 LIMIT 25 OFFSET 25; -- Page n (calcul côté client ou application) LIMIT {page_size} OFFSET {(page - 1) * page_size};

Note : l’absence d’ORDER BY rend le résultat non déterministe sur plusieurs appels avec le même LIMIT/OFFSET. Toujours ajouter un tri avant de paginer.

UPSERT (insérer ou mettre à jour)

-- PostgreSQL (recommandé) INSERT INTO inventaire (produit_id, quantite) VALUES (42, 10) ON CONFLICT (produit_id) DO UPDATE SET quantite = inventaire.quantite + 10; -- MySQL INSERT INTO inventaire (produit_id, quantite) VALUES (42, 10) ON DUPLICATE KEY UPDATE quantite = quantite + 10; -- SQLite INSERT INTO inventaire (produit_id, quantite) VALUES (42, 10) ON CONFLICT(produit_id) DO UPDATE SET quantite = excluded.quantite;

Piège : ON CONFLICT / ON DUPLICATE KEY ne correspond qu’aux colonnes soumises à une contrainte UNIQUE ou PRIMARY KEY.

Sous-requêtes corrélées

-- Clients sans commande (anti-jointure) SELECT nom FROM clients c WHERE NOT EXISTS ( SELECT 1 FROM commandes o WHERE o.client_id = c.id ); -- Dernier article par auteur (corrélation) SELECT a.titre, a.auteur FROM articles a WHERE a.cree_a = ( SELECT MAX(cree_a) FROM articles b WHERE b.auteur = a.auteur );

Piège : une sous-requête corrélée s’exécute une fois par ligne de la requête externe. Sur de gros ensembles, préférer un LEFT JOIN ... WHERE ... IS NULL ou un CTE avec ROW_NUMBER.

Agrégation multi-niveau avec GROUPING SETS

-- Ventes par produit ET par année, avec sous-total SELECT produit, YEAR(cree_a) AS annee, SUM(montant) AS total FROM ventes GROUP BY GROUPING SETS ( (produit), -- sous-total par produit (YEAR(cree_a)), -- sous-total par année () -- grand total );

Piège : les lignes de sous-total ont NULL dans les colonnes non-agrégées. Distinguer « vraie valeur NULL » de « ligne de total » avec GROUPING(produit) = 1.

Pivot table avec CASE

SELECT produit, SUM(CASE WHEN trimestre = 'T1' THEN montant END) AS t1, SUM(CASE WHEN trimestre = 'T2' THEN montant END) AS t2, SUM(CASE WHEN trimestre = 'T3' THEN montant END) AS t3, SUM(CASE WHEN trimestre = 'T4' THEN montant END) AS t4 FROM ventes GROUP BY produit;

Split de chaîne

-- PostgreSQL : dépliage en lignes SELECT unnest(string_to_array('a,b,c', ',')) AS valeur; -- PostgreSQL : split en colonnes SELECT (string_to_array('2024-01-15,Paris,500', ','))[1] AS date , (string_to_array('2024-01-15,Paris,500', ','))[2] AS ville , (string_to_array('2024-01-15,Paris,500', ','))[3] AS montant; -- MySQL : SUBSTRING_INDEX SELECT TRIM(SUBSTRING_INDEX('a,b,c', ',', 1)) AS premier;

Piège : string_to_array ne gère pas les séparateurs imbriqués. Pour un CSV avec quotes, utiliser regexp_split_to_table ou importer avec COPY / LOAD DATA.