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) etTO_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 dansWHERE), 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 lesWHENdans 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 BYrend le résultat non déterministe sur plusieurs appels avec le mêmeLIMIT/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 KEYne 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 NULLou un CTE avecROW_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
NULLdans les colonnes non-agrégées. Distinguer « vraie valeur NULL » de « ligne de total » avecGROUPING(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_arrayne gère pas les séparateurs imbriqués. Pour un CSV avec quotes, utiliserregexp_split_to_tableou importer avecCOPY/LOAD DATA.