| name | database-query-optimizer |
| description | Analyse et optimise des requêtes SQL ou NoSQL pour améliorer les performances. À utiliser quand l'utilisateur a une requête lente ou veut optimiser sa base de données. Se déclenche aussi avec "requête lente", "optimiser SQL", "EXPLAIN", "index", "performance DB", "N+1", ou toute question d'optimisation de requêtes. |
Database Query Optimizer
Étape 1 — Collecte d'informations
Demande systématiquement (ne suppose rien) :
- SGBD et version : PostgreSQL 16, MySQL 8, SQL Server 2022, MongoDB 7, SQLite…
- Requête complète (pas un résumé, le SQL brut)
- Schéma : colonnes, types, index existants (
\d table PG, SHOW CREATE TABLE MySQL)
- Volume : lignes dans les tables concernées (ordre de grandeur suffit)
- Temps mesuré : durée actuelle, cible souhaitée, outil de mesure
- Contexte d'exécution : fréquence, OLTP vs OLAP, connexions concurrentes
Étape 2 — Diagnostic avec EXPLAIN
PostgreSQL / MySQL / SQLite
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) <requête>;
EXPLAIN FORMAT=JSON <requête>;
SHOW STATUS LIKE 'Last_Query_Cost';
SET STATISTICS IO, TIME ON;
<requête>;
Signaux d'alarme dans le plan
| Signal | Signification | Priorité |
|---|
Seq Scan sur grande table | Pas d'index utilisable | Critique |
Hash Join + rows estimés très faux | Statistiques obsolètes | Élevée |
Nested Loop × millions de rows | Potentiel N+1 ou jointure cartésienne | Critique |
Sort sans Index Scan | Manque d'index sur ORDER BY | Modérée |
cost=... très élevé vs actual rows faibles | Mauvaise estimation selectivité | Élevée |
rows=1 partout mais slow | Problème réseau / lock / cache miss | Variable |
Mettre à jour les statistiques
ANALYZE table_name;
ANALYZE TABLE table_name;
UPDATE STATISTICS table_name;
Étape 3 — Optimisations concrètes
3.1 Index manquants
CREATE INDEX CONCURRENTLY idx_orders_user_status
ON orders (user_id, status)
INCLUDE (created_at, total_amount);
CREATE INDEX idx_orders_pending
ON orders (created_at)
WHERE status = 'pending';
3.2 Réécriture de requêtes
SELECT * FROM orders WHERE user_id = 42;
SELECT id, status, total_amount, created_at FROM orders WHERE user_id = 42;
SELECT id FROM users WHERE id NOT IN (SELECT user_id FROM orders);
SELECT u.id FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);
SELECT * FROM orders ORDER BY id LIMIT 20 OFFSET 100000;
SELECT * FROM orders WHERE id > :last_seen_id ORDER BY id LIMIT 20;
WHERE LOWER(email) = 'foo@bar.com'
WHERE email = 'foo@bar.com'
3.3 Problème N+1
orders = Order.objects.select_related('user').prefetch_related('items').all()
Order.includes(:user, :items)
prisma.order.findMany({ include: { user: true, items: true } })
SELECT o.id, u.name, i.product_id
FROM orders o
JOIN users u ON u.id = o.user_id
JOIN order_items i ON i.order_id = o.id
WHERE o.status = 'pending';
3.4 Agrégations et CTEs
SELECT user_id, COUNT(*) FROM orders GROUP BY user_id HAVING status = 'done';
SELECT user_id, COUNT(*) FROM orders WHERE status = 'done' GROUP BY user_id;
WITH recent AS NOT MATERIALIZED (
SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'
)
SELECT user_id, COUNT(*) FROM recent GROUP BY user_id;
Étape 4 — Stratégies avancées
Caching
Partitioning (tables > 50M lignes)
CREATE TABLE orders (created_at DATE, ...) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2025 PARTITION OF orders
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
Connection pooling
- PgBouncer (PostgreSQL), ProxySQL (MySQL) : éviter l'overhead de connexion
- Configurer
pool_mode = transaction pour OLTP
Étape 5 — Vérification et mesure
SELECT query, mean_exec_time, calls, total_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC LIMIT 20;
EXPLAIN (ANALYZE, BUFFERS) <requête_avant>;
EXPLAIN (ANALYZE, BUFFERS) <requête_après>;
Métriques à comparer :
- Temps d'exécution moyen (p50 / p95 / p99)
shared_hit vs shared_read (cache disque vs RAM)
- Nombre de rows scannées vs retournées
- Nombre de lock waits
Garde-fous / Anti-patterns / Pièges
- Ne pas créer d'index en masse : chaque index ralentit INSERT/UPDATE/DELETE. Cibler les requêtes > 100ms ou fréquence élevée.
- EXPLAIN sans ANALYZE : montre le plan estimé, pas réel. Toujours utiliser
EXPLAIN ANALYZE pour diagnostiquer.
- Optimiser sans mesurer : noter le temps baseline avant toute modification.
- Index sur colonne de faible cardinalité (booléen, statut 3 valeurs) : inefficace sauf index partiel.
- OR dans WHERE : peut empêcher l'usage d'index. Réécrire en UNION ALL si possible.
- DISTINCT comme palliatif : souvent masque un problème de jointure qui produit des doublons.
- Dénormalisation prématurée : mesurer d'abord, dénormaliser seulement si le gain est prouvé.
SELECT 1 dans EXISTS : équivalent à SELECT * côté optimiseur depuis PG 9+, mais rester explicite par convention.
- Ne jamais tuner en production sans rollback plan :
DROP INDEX CONCURRENTLY si régression.