Quand on est développeur web, on a tendance à tout faire dans le code — PHP, JavaScript, Python. Mais une seule requête SQL bien écrite peut remplacer 100 lignes de boucles, de tris et de jointures manuelles. Voici 10 patterns SQL avancés qui vont vous faire gagner du temps et de la performance, testés sur PostgreSQL 16 et MySQL 8.
1. Window Functions : classer sans GROUP BY
Le cas classique : vous voulez le top 3 des articles les plus lus par catégorie, pas globalement. En PHP, vous feriez une boucle par catégorie. En SQL, c’est une window function.
-- Top 3 articles par catégorie (en une seule requête)
SELECT * FROM (
SELECT
p.post_title,
t.name AS category,
p.view_count,
ROW_NUMBER() OVER (
PARTITION BY t.term_id
ORDER BY p.view_count DESC
) AS rank
FROM wp_posts p
JOIN wp_term_relationships tr ON p.ID = tr.object_id
JOIN wp_term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
JOIN wp_terms t ON tt.term_id = t.term_id
WHERE p.post_type = 'post' AND p.post_status = 'publish'
) ranked
WHERE rank <= 3;
Pourquoi c’est puissant : ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE() s’exécutent en une seule passe sur les données. Pas de sous-requêtes corrélées, pas de variables PHP. Le moteur SQL optimise tout.
2. CTE récursives : parcourir un arbre sans code
Les menus hiérarchiques, les catégories imbriquées, les commentaires en fils de discussion… Une CTE récursive fait tout en une requête.
-- Afficher l'arbre complet des catégories WordPress
WITH RECURSIVE category_tree AS (
-- Cas de base : racines
SELECT t.term_id, t.name, t.slug, tt.parent, 0 AS depth,
CAST(t.name AS CHAR(500)) AS path
FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
WHERE tt.taxonomy = 'category' AND tt.parent = 0
UNION ALL
-- Récursion : enfants
SELECT t.term_id, t.name, t.slug, tt.parent, ct.depth + 1,
CONCAT(ct.path, ' > ', t.name)
FROM wp_terms t
JOIN wp_term_taxonomy tt ON t.term_id = tt.term_id
JOIN category_tree ct ON tt.parent = ct.term_id
)
SELECT
CONCAT(REPEAT(' ', depth), name) AS indented_name,
depth, path
FROM category_tree
ORDER BY path;
Résultat : un menu indenté prêt à l’affichage, sans récursion PHP et sans N+1 queries.
3. JSON dans PostgreSQL : quand le NoSQL rencontre le SQL
PostgreSQL 16 traite le JSON comme un citoyen de première classe. Stockez des données semi-structurées sans perdre la puissance du SQL.
-- Stocker et requêter du JSON dans une colonne
CREATE TABLE user_preferences (
user_id INT PRIMARY KEY,
settings JSONB NOT NULL DEFAULT '{}'
);
INSERT INTO user_preferences VALUES
(1, '{"theme":"dark","notifications":{"email":true,"push":false},"lang":"fr"}'),
(2, '{"theme":"light","notifications":{"email":false,"push":true},"lang":"en"}');
-- Trouver tous les utilisateurs avec notifications email activées
SELECT user_id, settings->>'lang' AS language
FROM user_preferences
WHERE settings @> '{"notifications":{"email":true}}';
-- Mettre à jour une clé spécifique sans réécrire tout le JSON
UPDATE user_preferences
SET settings = jsonb_set(settings, '{theme}', '"auto"')
WHERE user_id = 1;
4. Index partiels : indexer 10% des données pour 90% des requêtes
Vos requêtes filtrent souvent sur status = 'active' ou deleted_at IS NULL ? Un index partiel ne couvre que les lignes qui correspondent.
-- Index partiel : 10x plus petit, 10x plus rapide
CREATE INDEX idx_active_posts
ON wp_posts (post_date DESC)
WHERE post_status = 'publish' AND post_type = 'post';
-- Cet index sera utilisé automatiquement pour :
SELECT * FROM wp_posts
WHERE post_status = 'publish' AND post_type = 'post'
ORDER BY post_date DESC LIMIT 10;
Sur une table de 100 000 posts dont 20 000 publiés, l’index partiel fait 80% de taille en moins et les recherches sont 5x plus rapides.
5. LATERAL JOIN : la sous-requête qui voit les colonnes d’avant
Besoin de prendre les 3 derniers commentaires de chaque article ? Avant, c’était N+1 requêtes. Avec LATERAL, c’est une seule.
-- Les 3 derniers commentaires de chaque article (PostgreSQL)
SELECT
p.post_title,
c.comment_content,
c.comment_date
FROM wp_posts p
CROSS JOIN LATERAL (
SELECT comment_content, comment_date
FROM wp_comments
WHERE comment_post_ID = p.ID
AND comment_approved = '1'
ORDER BY comment_date DESC
LIMIT 3
) c
WHERE p.post_type = 'post' AND p.post_status = 'publish';
6. UPSERT : INSERT ou UPDATE en une commande
Le pattern « vérifier si ça existe, sinon créer » est remplacé par ON CONFLICT.
-- PostgreSQL : ON CONFLICT DO UPDATE
INSERT INTO daily_stats (post_id, date, views)
VALUES (42, CURRENT_DATE, 1)
ON CONFLICT (post_id, date)
DO UPDATE SET views = daily_stats.views + 1;
-- MySQL : ON DUPLICATE KEY UPDATE
INSERT INTO daily_stats (post_id, date, views)
VALUES (42, CURDATE(), 1)
ON DUPLICATE KEY UPDATE views = views + 1;
7. Generated Columns : calculer une fois, lire toujours
Besoin de chercher rapidement par un champ calculé ? Les colonnes générées sont calculées à l’INSERT/UPDATE et indexables.
-- MySQL 8 : colonne générée pour le search
ALTER TABLE wp_posts
ADD COLUMN title_search VARCHAR(255)
GENERATED ALWAYS AS (LOWER(post_title)) STORED;
CREATE INDEX idx_title_search ON wp_posts (title_search);
-- Recherche insensible à la casse, sans LOWER() dans la requête
SELECT * FROM wp_posts
WHERE title_search LIKE '%wordpress%';
8. FULL TEXT Search sans Elasticsearch
Pour un site moyen, PostgreSQL ou MySQL font du full-text search très correct. Pas besoin d’Elasticsearch tout de suite.
-- PostgreSQL Full-Text Search
ALTER TABLE wp_posts ADD COLUMN search_vector tsvector;
CREATE INDEX idx_search ON wp_posts USING GIN(search_vector);
-- Mise à jour automatique
CREATE TRIGGER update_search_vector
BEFORE INSERT OR UPDATE ON wp_posts
FOR EACH ROW EXECUTE FUNCTION
tsvector_update_trigger(search_vector, 'pg_catalog.french', post_title, post_content);
-- Recherche avec ranking
SELECT post_title,
ts_rank(search_vector, query) AS rank
FROM wp_posts,
plainto_tsquery('pg_catalog.french', 'optimisation wordpress cache') AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 10;
9. MATERIALIZED VIEW : pré-calculer les données lourdes
Un dashboard qui agrège des millions de lignes ? Au lieu de calculer à chaque chargement, rafraîchissez la vue matérialisée périodiquement.
CREATE MATERIALIZED VIEW mv_article_stats AS
SELECT
p.ID,
p.post_title,
COUNT(DISTINCT c.comment_ID) AS comment_count,
COALESCE(SUM(ds.views), 0) AS total_views,
p.post_date
FROM wp_posts p
LEFT JOIN wp_comments c ON p.ID = c.comment_post_ID
LEFT JOIN daily_stats ds ON p.ID = ds.post_id
WHERE p.post_status = 'publish'
GROUP BY p.ID;
-- Rafraîchir (planifiable par cron)
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_article_stats;
10. EXPLAIN ANALYZE : ne devinez pas, mesurez
La règle d’or de l’optimisation SQL : ne devinez jamais. Utilisez EXPLAIN ANALYZE.
-- PostgreSQL : le plus détaillé
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)
SELECT * FROM wp_posts
WHERE post_status = 'publish'
ORDER BY post_date DESC LIMIT 10;
-- MySQL 8 : avec tree format
EXPLAIN ANALYZE
SELECT * FROM wp_posts
WHERE post_status = 'publish'
ORDER BY post_date DESC LIMIT 10;
Regardez le Planning Time vs Execution Time, les Buffers: shared hit (cache) vs read (disque), et surtout les Rows Removed by Filter — si ce chiffre est énorme, il vous faut un index.
Conclusion : le SQL est votre meilleur ami backend
Chaque fois que vous écrivez une boucle PHP pour filtrer, trier ou agréger des données, demandez-vous : « Est-ce que le moteur SQL peut faire ça en une requête ? » La réponse est presque toujours oui. Et la différence de performance est souvent de l’ordre de 10x à 100x.
Le secret d’un développeur full-stack efficace en 2026 : connaître son ORM, mais maîtriser son SQL.
Commentaires (0)
Laisser un commentaire
Les commentaires sont modérés. Questions WordPress, cybersécurité ou dev web bienvenues.