Introduction
Dans un monde où les données explosent, les requêtes SQL avancées sont essentielles pour extraire des insights précieux sans alourdir la base. Ce tutoriel intermédiaire vous guide pour maîtriser les jointures complexes, les CTE (Common Table Expressions), les agrégations avec GROUP BY/HAVING, les sous-requêtes et les fonctions fenêtres.
Pourquoi c'est crucial en 2026 ? Les bases modernes comme PostgreSQL ou MySQL gèrent des téraoctets, mais une requête mal optimisée peut paralyser votre app. Imaginez analyser les ventes e-commerce : top clients, paniers moyens, tendances produits... Nous utilisons une base exemple réaliste (clients, commandes, produits).
À la fin, vous serez capable d'écrire des queries scalables, testables immédiatement. Prêt à passer de junior à pro SQL ? (128 mots)
Prérequis
- Connaissances de base en SQL (SELECT, WHERE, INSERT/UPDATE/DELETE)
- Un SGBD installé : PostgreSQL 16+ ou MySQL 8+ (recommandé : Docker pour tester)
- Outil client : pgAdmin, DBeaver ou VS Code avec extension SQL
- Temps estimé : 20 minutes
1. Créer la base de données exemple
DROP DATABASE IF EXISTS ecommerce;
CREATE DATABASE ecommerce;
\c ecommerce;
CREATE TABLE customers (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL
);
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price DECIMAL(10,2) NOT NULL
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id),
order_date DATE NOT NULL,
total DECIMAL(10,2) NOT NULL
);
CREATE TABLE order_items (
id SERIAL PRIMARY KEY,
order_id INTEGER REFERENCES orders(id),
product_id INTEGER REFERENCES products(id),
quantity INTEGER NOT NULL
);
-- Insertion de données
INSERT INTO customers (name, email) VALUES
('Alice Dupont', 'alice@ex.com'),
('Bob Martin', 'bob@ex.com'),
('Charlie Roux', 'charlie@ex.com'),
('Diana Lefevre', 'diana@ex.com');
INSERT INTO products (name, price) VALUES
('iPhone 15', 999.99),
('MacBook Pro', 2499.99),
('AirPods', 199.99),
('iPad Air', 599.99),
('Apple Watch', 399.99);
INSERT INTO orders (customer_id, order_date, total) VALUES
(1, '2026-01-15', 1199.98),
(1, '2026-02-20', 2699.98),
(2, '2026-01-10', 399.98),
(2, '2026-03-05', 999.99),
(3, '2026-02-28', 599.99),
(4, '2026-01-25', 1999.98);
INSERT INTO order_items (order_id, product_id, quantity) VALUES
(1, 1, 1),
(1, 3, 1),
(2, 2, 1),
(2, 3, 1),
(3, 1, 2),
(4, 5, 1),
(5, 4, 1),
(6, 2, 1),
(6, 1, 1);Ce script crée une base e-commerce complète avec 4 tables liées par clés étrangères. Il inclut 4 clients, 5 produits et 6 commandes avec lignes. Exécutez-le en PostgreSQL pour tester toutes les queries suivantes. Attention : adaptez DROP/CREATE si MySQL (utilisez USE au lieu de \c).
Comprendre la structure des données
Les tables forment un schéma star-like : customers et products au centre, reliés via orders et order_items. Cela simule un vrai e-commerce. Chaque requête suivante exploite ces relations pour des analyses réalistes, comme le CA par client ou les produits les plus vendus.
2. Jointures multiples pour détail des commandes
SELECT
c.name AS client,
o.order_date,
o.total,
p.name AS produit,
oi.quantity,
(p.price * oi.quantity) AS ligne_total
FROM customers c
JOIN orders o ON c.id = o.customer_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id
ORDER BY o.order_date DESC, c.name;Cette requête INNER JOIN relie 4 tables pour lister toutes les lignes de commandes avec totaux. Utilisez INNER pour exclure les NULL ; LEFT JOIN si besoin d'options. Piège : sans INDEX sur foreign keys, ça ralentit sur gros volumes – ajoutez-les en prod.
Passer aux agrégations
Les jointures posent les bases ; maintenant, agrégez pour des KPI : CA total, moyenne panier, top produits. GROUP BY segmente, HAVING filtre les groupes.
3. Agrégations avec GROUP BY et HAVING
SELECT
c.name AS client,
COUNT(o.id) AS nb_commandes,
SUM(o.total) AS ca_total,
AVG(o.total) AS panier_moyen
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
HAVING COUNT(o.id) >= 1 AND SUM(o.total) > 1000
ORDER BY ca_total DESC;Agrège par client : COUNT/SUM/AVG sur commandes. LEFT JOIN inclut clients sans orders. HAVING filtre post-group (vs WHERE pre-group). Résultat : clients VIP. Piège : oublier GROUP BY sur non-agrégés cause erreur.
4. Sous-requêtes pour top produits
SELECT
p.name AS produit,
SUM(oi.quantity) AS qte_totale,
SUM(p.price * oi.quantity) AS ca_produit
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
HAVING SUM(oi.quantity) > (
SELECT AVG(qte_totale)
FROM (
SELECT SUM(quantity) AS qte_totale
FROM order_items
GROUP BY product_id
) AS moyennes
)
ORDER BY ca_produit DESC;Sous-requête calcule moyenne quantités par produit, HAVING compare. Utile pour benchmarks dynamiques. Piège : sous-requêtes corrélées (avec outer ref) scalent mal – préférez CTE pour complexité.
Introduction aux CTE et fonctions fenêtres
CTE (WITH) rendent les queries lisibles comme du code modulaire. Fonctions fenêtres (OVER) calculent rankings/running totals sans sous-groupes, idéales pour analytics.
5. CTE pour analyse modulaire
WITH ventes_par_client AS (
SELECT
c.name,
SUM(o.total) AS ca
FROM customers c
JOIN orders o ON c.id = o.customer_id
GROUP BY c.id, c.name
),
ventes_par_produit AS (
SELECT
p.name AS produit,
SUM(p.price * oi.quantity) AS ca_prod
FROM products p
JOIN order_items oi ON p.id = oi.product_id
GROUP BY p.id, p.name
)
SELECT
vpc.name AS top_client,
vpc.ca,
vpp.produit AS top_produit,
vpp.ca_prod
FROM ventes_par_client vpc
CROSS JOIN ventes_par_produit vpp
ORDER BY vpc.ca DESC, vpp.ca_prod DESC
LIMIT 2;CTE sépare logiques : un pour clients, un pour produits ; CROSS JOIN combine tops. Réutilisable, indexable en Postgres. Piège : CTE matérialisés (Postgres) consomment RAM sur très gros sets.
6. Fonctions fenêtres pour rankings
SELECT
c.name AS client,
o.order_date,
o.total,
SUM(o.total) OVER (PARTITION BY c.id ORDER BY o.order_date) AS ca_cumulatif,
RANK() OVER (PARTITION BY DATE_TRUNC('month', o.order_date) ORDER BY o.total DESC) AS rang_mensuel,
AVG(o.total) OVER (PARTITION BY c.id) AS panier_moyen_client
FROM customers c
JOIN orders o ON c.id = o.customer_id
ORDER BY c.name, o.order_date;OVER PARTITION segmente (par client), ORDER pour cumul/rank. RANK() gère ex-aequo. Parfait pour dashboards. Piège : sans frame clause (ROWS/ RANGE), default est unbounded – précisez pour précision.
Bonnes pratiques
- Indexez toujours foreign keys et colonnes WHERE/GROUP BY (EXPLAIN ANALYZE pour vérifier).
- Utilisez CTE pour >3 sous-requêtes : lisibilité + optimisations modernes.
- Limitez résultats (LIMIT/OFFSET) et paginez en API.
- Validez types : CAST pour agrégations précises.
- Testez perf : pgBadger ou EXPLAIN pour bottlenecks.
Erreurs courantes à éviter
- Oublier GROUP BY sur colonnes non-agrégées : erreur syntaxe.
- Confondre WHERE/HAVING : WHERE filtre lignes, HAVING groupes.
- Sous-requêtes non-corrélées lentes : refactorisez en JOIN/CTE.
- Fenêtres sans PARTITION : résultats globaux inattendus.
Pour aller plus loin
Maîtrisez l'optimisation avec PostgreSQL Advanced. Explorez ClickHouse pour big data. Découvrez nos formations Learni sur les bases de données pour SQL expert et NoSQL.