📌 Ce qu'il faut retenir
- Technique 1 : Éviter SELECT * — listez uniquement les colonnes nécessaires
- Technique 2 : Utiliser EXPLAIN/EXPLAIN ANALYZE pour lire le plan d'exécution
- Technique 3 : Créer les bons index (simple, composé, couvrant)
- Technique 4 : Éviter les fonctions sur les colonnes indexées (YEAR, LOWER, CAST)
- Technique 5 : Optimiser les JOINs — indexer les colonnes de jointure, réduire avant de joindre
- Technique 6 : Préférer EXISTS à IN pour les grosses sous-requêtes
- Technique 7 : Keyset pagination plutôt que OFFSET élevé
- Technique 8 : Batch processing pour les UPDATE/DELETE massifs
- Technique 9 : Partitionnement de table pour les très grandes tables
- Technique 10 : Vues matérialisées pour les calculs fréquents
L'optimisation SQL est un sujet qui revient de plus en plus en entretien technique, même pour des postes de data analyst. Les recruteurs veulent vérifier que vous ne vous contentez pas d'écrire des requêtes qui « marchent » — mais que vous comprenez pourquoi une requête est lente et comment la rendre rapide.
Thomas Leroy vous présente 10 techniques concrètes, de la plus basique à la plus avancée, avec des exemples mesurables et des explications claires.
Technique 1 : Arrêtez d'utiliser SELECT *
C'est la règle la plus simple et la plus souvent ignorée.
-- Mauvais : charge toutes les colonnes
SELECT * FROM commandes WHERE statut = 'livré';
-- Bon : charge uniquement ce dont vous avez besoin
SELECT id, client_id, montant, date_livraison
FROM commandes
WHERE statut = 'livré';
Pourquoi c'est important :
- **Moins de données transférées** entre le disque, la mémoire et le réseau
- **Les index couvrants** deviennent possibles (le moteur peut répondre sans accéder à la table)
- **Clarté** : vous et le recruteur savez exactement quelles colonnes sont utilisées
Sur une table avec 50 colonnes et des millions de lignes, la différence peut être d'un facteur 10x.
### Exemple concret avec mesure de performance
Prenez l'exemple de Sophie, développeuse chez une plateforme e-commerce. Elle optimise une requête sur la table `produits` qui contient 2 millions d'articles avec 45 colonnes (descriptions, métadonnées, images, etc.) :
```sql
-- Avant (SELECT *) : 15 secondes
SELECT * FROM produits WHERE categorie = 'électronique';
-- Après (colonnes spécifiques) : 1.2 secondes
SELECT id, nom, prix, stock
FROM produits
WHERE categorie = 'électronique';
L'amélioration de 12.5x provient de la réduction de 45 colonnes à 4, permettant à l'index couvrant de répondre sans accéder au stockage principal.
<div class="callout callout-tip">
<p class="callout-title">💡 Bon à savoir</p>
<p>Mentionnez systématiquement EXPLAIN en entretien lorsqu'on vous demande comment optimiser une requête. Même si vous ne pouvez pas l'exécuter, indiquer que votre première étape serait de lire le plan d'exécution montre une approche rigoureuse de diagnostic.</p>
</div>
## Technique 2 : Comprendre et utiliser EXPLAIN
`EXPLAIN` (ou `EXPLAIN ANALYZE` en PostgreSQL) affiche le plan d'exécution de la requête — la façon dont le moteur SQL va chercher les données.
```sql
EXPLAIN ANALYZE
SELECT * FROM commandes WHERE client_id = 42;
Ce qu'il faut lire dans le plan d'exécution :
| Opération | Signification | Performance |
|---|---|---|
| Seq Scan | Parcours séquentiel de toute la table | Lent sur grosses tables |
| Index Scan | Utilisation d'un index | Rapide |
| Index Only Scan | Réponse uniquement via l'index | Très rapide |
| Nested Loop | Boucle imbriquée pour les JOINs | OK pour petites tables |
| Hash Join | Table de hachage pour le JOIN | Bon pour grosses tables |
| Sort | Tri en mémoire ou sur disque | Coûteux si sur disque |
En entretien, le fait de mentionner EXPLAIN et de savoir lire un plan d'exécution vous distingue immédiatement.
Cas pratique : diagnostic d'une requête lente
Marco, analyste data chez un courtier en assurance, fait face à une requête qui prenait 45 secondes :
-- Requête problématique
SELECT COUNT(*) FROM polices p
JOIN clients c ON p.client_id = c.id
WHERE c.age BETWEEN 25 AND 35;
-- EXPLAIN révèle un Seq Scan sur 5M de lignes
-- Après ajout d'index composé : 2.1 secondes
CREATE INDEX idx_clients_age ON clients(age);
Le plan d'exécution EXPLAIN a révélé que la jointure se faisait après un Seq Scan complet sur clients, au lieu d'utiliser un index pour filtrer d'abord par âge.
💡 Bon à savoir
Pour progresser dans votre maîtrise des optimisations, consultez nos guides complémentaires sur les fonctions fenêtre SQL et les GROUP BY et HAVING, qui interviennent aussi dans les plans d'exécution complexes.
Technique 3 : Créer les bons index
Un index est comme l'index d'un livre : il permet de trouver une information sans parcourir toutes les pages.
Index simple
-- Créer un index sur la colonne la plus filtrée
CREATE INDEX idx_commandes_client_id ON commandes(client_id);
Après cet index, WHERE client_id = 42 passe de Seq Scan à Index Scan.
Index composé
L'ordre des colonnes dans un index composé est crucial :
-- Index composé (client_id d'abord, puis date)
CREATE INDEX idx_commandes_client_date
ON commandes(client_id, date_commande);
Cet index fonctionne pour :
WHERE client_id = 42(utilise la première colonne)WHERE client_id = 42 AND date_commande > '2026-01-01'(utilise les deux colonnes)- Mais PAS pour
WHERE date_commande > '2026-01-01'seul (la première colonne manque)
Règle du « leftmost prefix » : un index composé est utilisable si la requête filtre sur un préfixe gauche des colonnes de l'index.
Index couvrant
Un index qui contient toutes les colonnes nécessaires à la requête :
-- Index couvrant
CREATE INDEX idx_commandes_couv
ON commandes(client_id, date_commande, montant);
-- Cette requête n'accède même pas à la table (Index Only Scan)
SELECT date_commande, montant
FROM commandes
WHERE client_id = 42;
Spécificités par SGBD
PostgreSQL : Les index partiels permettent d'indexer seulement un sous-ensemble :
CREATE INDEX idx_commandes_actives
ON commandes(client_id) WHERE statut = 'active';
MySQL : Les préfixes d'index économisent l'espace sur les colonnes texte longues :
CREATE INDEX idx_description ON produits(description(50));
SQL Server : Les index avec colonnes incluses évitent les key lookups :
CREATE INDEX idx_commandes_include
ON commandes(client_id) INCLUDE (montant, date_commande);
Quand ne PAS créer d'index
- Tables très petites (< 1000 lignes) : le Seq Scan est plus rapide
- Colonnes avec très peu de valeurs distinctives (booléens, statuts à 3 valeurs)
- Tables avec beaucoup d'INSERT/UPDATE : chaque index ralentit les écritures
Erreurs courantes sur l'indexation
Erreur #1 : Créer trop d'index similaires
-- Redondant
CREATE INDEX idx_orders_customer_id ON orders(customer_id);
CREATE INDEX idx_orders_customer_date ON orders(customer_id, order_date);
-- Le premier index est inutile
Erreur #2 : Oublier l'ordre dans les index composés
-- Mauvais ordre pour des requêtes sur date_commande seule
CREATE INDEX idx_orders_bad ON orders(customer_id, order_date);
-- Meilleur ordre selon le pattern de requêtes
CREATE INDEX idx_orders_good ON orders(order_date, customer_id);
Erreur #3 : Index sur colonnes avec faible cardinalité
-- Inefficace : seulement 2 valeurs possibles
CREATE INDEX idx_users_gender ON users(gender);
⚠️ Attention
Appliquer une fonction sur une colonne indexée (YEAR, LOWER, CAST) désactive l'index. Le moteur doit alors faire un Seq Scan complet. Reformulez toujours les conditions pour opérer sur la valeur brute, pas sur une transformation de la colonne.
Technique 4 : Éviter les fonctions sur les colonnes indexées
Appliquer une fonction sur une colonne empêche l'utilisation de l'index :
-- Mauvais : l'index sur date_commande n'est PAS utilisé
SELECT * FROM commandes
WHERE YEAR(date_commande) = 2026;
-- Bon : reformuler comme une plage
SELECT * FROM commandes
WHERE date_commande >= '2026-01-01'
AND date_commande < '2027-01-01';
-- Mauvais : l'index sur email n'est PAS utilisé
SELECT * FROM utilisateurs
WHERE LOWER(email) = 'alice@example.com';
-- Bon : utiliser un index fonctionnel (PostgreSQL)
CREATE INDEX idx_users_email_lower ON utilisateurs(LOWER(email));
Même principe avec les casts implicites — si la colonne est VARCHAR et que vous comparez avec un INT, le moteur peut caster la colonne et ne pas utiliser l'index.
Exemples de reformulation courantes
| Mauvaise pratique | Bonne pratique | Gain |
|---|---|---|
YEAR(date) = 2026 | date >= '2026-01-01' AND date < '2027-01-01' | Index utilisé |
SUBSTRING(code, 1, 3) = 'ABC' | code LIKE 'ABC%' | Index utilisé |
UPPER(nom) = 'MARTIN' | Index fonctionnel ou collation insensible | Index utilisé |
prix * tva > 100 | prix > 100 / tva | Index utilisé |
💡 Bon à savoir
Pour un index composé, respectez la règle du "leftmost prefix" : l'index (client_id, date_commande) est utilisé pour WHERE client_id = 42, mais pas pour WHERE date_commande > '2026-01-01' seul. Placez toujours la colonne la plus filtrée en premier dans l'index composé.
Technique 5 : Optimiser les JOINs
Indexer les colonnes de jointure
-- Assurez-vous que les deux côtés du JOIN sont indexés
CREATE INDEX idx_commandes_client_id ON commandes(client_id);
-- clients.id est probablement déjà indexé (clé primaire)
Réduire les données avant le JOIN
-- Moins performant : joindre puis filtrer
SELECT c.nom, SUM(co.montant)
FROM clients c
INNER JOIN commandes co ON c.id = co.client_id
WHERE co.date_commande >= '2026-01-01'
GROUP BY c.nom;
-- Plus performant : filtrer puis joindre
WITH commandes_recentes AS (
SELECT client_id, montant
FROM commandes
WHERE date_commande >= '2026-01-01'
)
SELECT c.nom, SUM(cr.montant)
FROM clients c
INNER JOIN commandes_recentes cr ON c.id = cr.client_id
GROUP BY c.nom;
En théorie, l'optimiseur devrait produire le même plan dans les deux cas. En pratique, sur des requêtes complexes avec plusieurs JOINs, l'aider avec des CTE (Common Table Expressions) peut faire une vraie différence.
Choisir le bon type de JOIN
- INNER JOIN est généralement plus performant que LEFT JOIN (moins de lignes à traiter)
- Utilisez LEFT JOIN uniquement quand vous en avez besoin
Pour approffondir les techniques de jointure, consultez notre guide complet des JOINs SQL.
Optimisation des JOINs multiples
Exemple d'optimisation progressive sur une requête à 4 tables :
-- Requête initiale : 12 secondes
SELECT u.nom, COUNT(c.id) as nb_commandes, SUM(p.prix) as total
FROM utilisateurs u
LEFT JOIN commandes c ON u.id = c.user_id
LEFT JOIN ligne_commande lc ON c.id = lc.commande_id
LEFT JOIN produits p ON lc.produit_id = p.id
WHERE u.date_inscription >= '2025-01-01'
GROUP BY u.id, u.nom;
-- Optimisé avec pré-agrégation : 2.3 secondes
WITH stats_commandes AS (
SELECT c.user_id,
COUNT(c.id) as nb_commandes,
SUM(p.prix) as total
FROM commandes c
JOIN ligne_commande lc ON c.id = lc.commande_id
JOIN produits p ON lc.produit_id = p.id
GROUP BY c.user_id
)
SELECT u.nom,
COALESCE(sc.nb_commandes, 0) as nb_commandes,
COALESCE(sc.total, 0) as total
FROM utilisateurs u
LEFT JOIN stats_commandes sc ON u.id = sc.user_id
WHERE u.date_inscription >= '2025-01-01';
Technique 6 : Utiliser EXISTS au lieu de IN pour les grosses sous-requêtes
-- Potentiellement lent : IN matérialise toute la sous-requête
SELECT nom FROM clients
WHERE id IN (SELECT client_id FROM commandes WHERE montant > 1000);
-- Souvent plus rapide : EXISTS s'arrête au premier match
SELECT c.nom FROM clients c
WHERE EXISTS (
SELECT 1 FROM commandes co
WHERE co.client_id = c.id AND co.montant > 1000
);
EXISTS peut s'arrêter dès qu'il trouve une correspondance, tandis que IN doit collecter toutes les valeurs d'abord.
Comparaison détaillée IN vs EXISTS vs JOIN
| Méthode | Performance | Cas d'usage optimal | Attention |
|---|---|---|---|
| IN | Rapide si petite liste | Sous-requête retournant < 1000 valeurs | Lent si gros volume |
| EXISTS | Excellent | Test d'existence, gros volumes | Syntaxe plus complexe |
| INNER JOIN | Très rapide | Quand vous avez besoin des colonnes des 2 tables | Peut dupliquer les lignes |
| LEFT JOIN + IS NOT NULL | Rapide | Alternative à EXISTS | Moins lisible |
Erreurs courantes avec les sous-requêtes
Erreur #1 : Utiliser IN avec des valeurs NULL
-- Peut ne pas retourner les résultats attendus si client_id contient NULL
SELECT * FROM clients WHERE id IN (SELECT client_id FROM commandes);
-- Plus sûr
SELECT c.* FROM clients c
WHERE EXISTS (SELECT 1 FROM commandes co WHERE co.client_id = c.id);
Erreur #2 : Sous-requêtes corrélées non optimisées
-- Lent : exécute la sous-requête pour chaque ligne
SELECT * FROM clients c
WHERE (SELECT COUNT(*) FROM commandes co WHERE co.client_id = c.id) > 5;
-- Plus rapide : window function
SELECT DISTINCT c.*
FROM clients c
JOIN (
SELECT client_id,
COUNT(*) OVER (PARTITION BY client_id) as nb_commandes
FROM commandes
) co ON c.id = co.client_id
WHERE co.nb_commandes > 5;
Pour approfondir, consultez notre article sur les sous-requêtes SQL.
Technique 7 : LIMIT et pagination efficace
Le problème du OFFSET
-- Lent pour les grandes valeurs de OFFSET
SELECT * FROM articles
ORDER BY date_creation DESC
LIMIT 20 OFFSET 100000;
Le moteur doit lire 100020 lignes, les trier, puis en jeter 100000. Plus le OFFSET est grand, plus c'est lent.
La solution : keyset pagination
-- Rapide : utilise l'index directement
SELECT * FROM articles
WHERE date_creation < '2026-03-15 10:30:00'
ORDER BY date_creation DESC
LIMIT 20;
Au lieu de dire « saute 100000 lignes », on dit « donne-moi les lignes après ce curseur ». L'index est utilisé directement.
Implémentation pratique de la keyset pagination
-- Page 1 : récupérer les 20 premiers articles
SELECT id, titre, date_creation, auteur_id
FROM articles
ORDER BY date_creation DESC, id DESC
LIMIT 20;
-- Page suivante : utiliser les valeurs de la dernière ligne
SELECT id, titre, date_creation, auteur_id
FROM articles
WHERE (date_creation, id) < ('2026-03-15 10:30:00', 12547)
ORDER BY date_creation DESC, id DESC
LIMIT 20;
Comparaison des performances
Exemple sur une table de 10 millions d'articles :
- OFFSET 100000 : 8.5 secondes
- Keyset pagination : 0.12 secondes (70x plus rapide)
L'amélioration devient exponentielle avec la taille de l'OFFSET.
Gestion de la pagination bidirectionnelle
Pour permettre la navigation dans les deux sens :
-- Page suivante (plus récents)
SELECT id, titre, date_creation
FROM articles
WHERE date_creation > '2026-03-15 10:30:00'
ORDER BY date_creation ASC, id ASC
LIMIT 20;
-- Page précédente (plus anciens)
SELECT id, titre, date_creation
FROM articles
WHERE date_creation < '2026-03-15 09:15:00'
ORDER BY date_creation DESC, id DESC
LIMIT 20;
Technique 8 : Batch processing pour les UPDATE/DELETE massifs
-- Mauvais : verrouille toute la table
DELETE FROM logs WHERE date_log < '2025-01-01';
-- Bon : traiter par lots
DELETE FROM logs
WHERE id IN (
SELECT id FROM logs
WHERE date_log < '2025-01-01'
LIMIT 10000
);
-- Répéter jusqu'à ce que 0 lignes soient supprimées
Avantages du traitement par lots :
- Pas de verrouillage prolongé
- Les autres requêtes continuent de fonctionner
- Les logs de transaction restent gérables
Script complet pour le batch processing
-- PostgreSQL : suppression par lots avec boucle
DO $$
DECLARE
rows_deleted INTEGER := 1;
BEGIN
WHILE rows_deleted > 0 LOOP
DELETE FROM logs
WHERE id IN (
SELECT id FROM logs
WHERE date_log < '2025-01-01'
LIMIT 10000
);
GET DIAGNOSTICS rows_deleted = ROW_COUNT;
RAISE NOTICE 'Supprimées: % lignes', rows_deleted;
-- Pause pour laisser respirer la base
PERFORM pg_sleep(1);
END LOOP;
END$$;
Stratégies d'UPDATE massifs
Pour les modifications importantes, la stratégie table temporaire peut être plus efficace :
-- Au lieu d'un UPDATE sur 10M lignes
-- Créer une nouvelle table avec les bonnes données
CREATE TABLE commandes_new AS
SELECT id, client_id, montant * 1.1 as montant, -- augmentation prix
date_commande, statut
FROM commandes;
-- Swapper atomiquement
BEGIN;
DROP TABLE commandes;
ALTER TABLE commandes_new RENAME TO commandes;
-- Recréer index et contraintes
COMMIT;
Erreurs fréquentes dans le batch processing
Erreur #1 : Lots trop petits ou trop gros
-- Trop petit : overhead excessif
LIMIT 100; -- 1000 itérations pour 100K lignes
-- Trop gros : verrous prolongés
LIMIT 1000000; -- Risque de timeout
-- Optimal : entre 1K et 10K selon la complexité
LIMIT 5000;
Erreur #2 : Oublier les conditions d'arrêt
-- Risque de boucle infinie si la condition WHERE ne correspond à rien
WHILE rows_affected > 0 LOOP
UPDATE table SET col = value WHERE impossible_condition;
END LOOP;
Erreur #3 : Ne pas monitorer la progression
-- Ajoutez toujours des logs pour suivre l'avancement
RAISE NOTICE 'Lot %, lignes affectées: %', iteration, rows_affected;
Technique 9 : Partitionnement de table
Le partitionnement divise physiquement une grande table en segments plus petits :
-- PostgreSQL : partitionnement par range
CREATE TABLE commandes (
id SERIAL,
date_commande DATE,
montant DECIMAL(10,2)
) PARTITION BY RANGE (date_commande);
CREATE TABLE commandes_2025 PARTITION OF commandes
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE commandes_2026 PARTITION OF commandes
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
Quand vous filtrez par date, le moteur ne parcourt que la partition concernée (partition pruning). Sur des tables de centaines de millions de lignes, le gain est considérable.
Types de partitionnement
Partitionnement par range (le plus courant) :
-- Par date
CREATE TABLE ventes_2026_q1 PARTITION OF ventes
FOR VALUES FROM ('2026-01-01') TO ('2026-04-01');
-- Par valeur numérique
CREATE TABLE commandes_petites PARTITION OF commandes
FOR VALUES FROM (0) TO (1000);
Partitionnement par hash (distribution équilibrée) :
CREATE TABLE utilisateurs_hash PARTITION OF utilisateurs
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
Partitionnement par liste (valeurs discrètes) :
CREATE TABLE commandes_france PARTITION OF commandes
FOR VALUES IN ('FR', 'MC', 'AD');
Bénéfices du partitionnement
- Partition pruning : requêtes 10-100x plus rapides
- Maintenance parallèle : VACUUM, REINDEX par partition
- Archivage simple : DROP d'une partition = suppression instantanée
- Requêtes parallèles : travail distribué sur plusieurs partitions
Cas d'usage optimal
Clara, DBA chez une fintech, gère une table transactions de 500M de lignes. Après partitionnement mensuel :
- Requêtes sur le mois courant : de 45s à 1.2s
- Archivage des données anciennes : de 6h à 30 secondes (DROP partition)
- VACUUM mensuel au lieu d'hebdomadaire
Stratégies de partitionnement par secteur
E-commerce : Partitionnement par date de commande
-- Permet l'archivage automatique des anciennes commandes
CREATE TABLE commandes_2026_01 PARTITION OF commandes
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
Finance : Partitionnement par type de transaction
CREATE TABLE transactions_payments PARTITION OF transactions
FOR VALUES IN ('payment', 'refund', 'chargeback');
SaaS/Analytics : Partitionnement par tenant (client)
CREATE TABLE events_client_1001 PARTITION OF events
FOR VALUES FROM (1001) TO (1002);
Technique 10 : Matérialiser les calculs fréquents
Vues matérialisées
-- Créer une vue matérialisée pour un rapport fréquent
CREATE MATERIALIZED VIEW mv_stats_vendeurs AS
SELECT
vendeur,
DATE_TRUNC('month', date_vente) AS mois,
COUNT(*) AS nb_ventes,
SUM(montant) AS total,
AVG(montant) AS moyenne
FROM ventes
GROUP BY vendeur, DATE_TRUNC('month', date_vente);
-- Requêter la vue matérialisée (instantané)
SELECT * FROM mv_stats_vendeurs WHERE vendeur = 'Alice';
-- Rafraîchir quand les données changent
REFRESH MATERIALIZED VIEW mv_stats_vendeurs;
Colonnes calculées / Generated columns
-- PostgreSQL 12+
ALTER TABLE commandes
ADD COLUMN mois_commande DATE GENERATED ALWAYS AS
(DATE_TRUNC('month', date_commande)) STORED;
CREATE INDEX idx_commandes_mois ON commandes(mois_commande);
Stratégies de rafraîchissement
Rafraîchissement complet (relit toutes les données) :
REFRESH MATERIALIZED VIEW mv_stats_vendeurs;
Rafraîchissement concurrent (les requêtes continuent) :
-- PostgreSQL uniquement
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_stats_vendeurs;
Rafraîchissement incrémental (pour les trop grosses vues) :
-- Recréer seulement le mois courant
INSERT INTO mv_stats_vendeurs_incremental
SELECT vendeur, DATE_TRUNC('month', NOW()),
COUNT(*), SUM(montant), AVG(montant)
FROM ventes
WHERE DATE_TRUNC('month', date_vente) = DATE_TRUNC('month', NOW())
GROUP BY vendeur;
Cas pratique : agrégations commerciales
Yannick, analyste chez un retailer multicanal, doit afficher des tableaux de bord avec 50+ métriques à chaque actualisation. Les requêtes prenaient 8-12 secondes. Après matérialisation :
-- Vue matérialisée combinant 7 tables
CREATE MATERIALIZED VIEW vw_dashboard_kpis AS
WITH daily_sales AS (
SELECT DATE_TRUNC('day', o.date_commande) as date,
p.categorie,
COUNT(*) as nb_commandes,
SUM(ol.quantite) as nb_articles,
SUM(ol.prix_unitaire * ol.quantite) as total_ht
FROM orders o
JOIN order_lines ol ON o.id = ol.order_id
JOIN products p ON ol.product_id = p.id
WHERE o.status = 'completed'
GROUP BY 1, 2
)
SELECT * FROM daily_sales;
-- Rafraîchissement nightly (05:00 UTC)
-- INSERT INTO admin_logs VALUES (NOW(), 'Dashboard refreshed')
Résultat : requêtes de tableau de bord < 0.3 secondes au lieu de 10+ secondes.
Erreurs fréquentes avec les vues matérialisées
Erreur #1 : Oublier de rafraîchir
-- Données obsolètes sans notification
-- Les utilisateurs voient des chiffres d'hier
-- Solution : automatiser le rafraîchissement
Erreur #2 : Matérialiser des vues rarement consultées
-- Gaspille de l'espace disque
-- Utilisez des vues normales (virtuelles) si consultées < 1x/jour
Erreur #3 : Accumulation excessive de vues matérialisées
-- 100+ vues matérialisées = maintenance complexe
-- Limiter à 10-20 vues pour les principales métriques
Questions fréquentes
Quelle est la différence entre un index simple et un index composé ?
Un index simple (une colonne) est rapide pour filtrer sur cette seule colonne. Un index composé (plusieurs colonnes) est plus flexible : il peut servir pour des filtres utilisant le préfixe gauche des colonnes. Par exemple, l'index (client_id, date_commande) fonctionne pour WHERE client_id = 42 ET pour WHERE client_id = 42 AND date = '2026-01-01', mais pas pour WHERE date = '2026-01-01' seul. Toujours placer la colonne la plus filtrée en premier.
Dois-je toujours utiliser EXISTS au lieu de IN ?
Non. IN est plus lisible et généralement efficace pour les sous-requêtes retournant moins de 1000 valeurs. EXISTS est meilleur pour les gros volumes ou les sous-requêtes corrélées complexes. Sur les SGBD modernes (PostgreSQL 13+, MySQL 8.0+), l'optimiseur transforme souvent IN en EXISTS automatiquement. Préférez la lisibilité sauf sur des requêtes très lentes.
Comment choisir entre OFFSET/LIMIT et keyset pagination ?
Utilisez OFFSET/LIMIT pour les petites pages (< 10 pages consultées en général). Keyset pagination est obligatoire pour les grands volumes de données ou la pagination infinie. Keyset nécessite un tri stable et un curseur, mais offre une performance indépendante de la position dans la liste.
Combien de vues matérialisées devrais-je créer ?
Limitez-vous à 10-20 vues pour les métriques vraiment fréquentes. Chaque vue matérialisée consomme du disque et nécessite des rafraîchissements. Mesurez l'impact : si une requête prend 1s, créer une vue matérialisée rarement consultée est du gaspillage. Utilisez des vues normales pour le reste.
Pourquoi mon index n'est-il pas utilisé alors que j'ai créé la bonne colonne ?
Causes courantes : (1) Fonction appliquée sur la colonne (YEAR, LOWER, etc.) ; (2) Cast implicite de type ; (3) Condition avec OR impliquant plusieurs colonnes ; (4) Statistiques obsolètes (ANALYZE requis) ; (5) Selectivité trop faible (booléens, énumérations). Utilisez EXPLAIN ANALYZE pour diagnostiquer.
Le partitionnement ralentit-il les requêtes sans filtrage de partition ?
Oui, légèrement. Une requête sans filtre sur la clé de partitionnement doit unionner tous les partitions, ce qui ajoute un peu d'overhead. Le partitionnement vaut la peine seulement si (1) la table > 10GB, (2) vous filtrez souvent sur la clé de partitionnement, ou (3) vous avez besoin d'archivage rapide.
Comment optimiser une requête sans pouvoir ajouter d'index (droits limités) ?
Réécrivez la requête : (1) Séparez en requêtes plus simples ; (2) Utilisez des CTE pour pré-filtrer ; (3) Éliminez les sous-requêtes corrélées ; (4) Reformulez les conditions (YEAR → plages de dates) ; (5) Demandez à votre DBA les index nécessaires avec données de production.
