SQL Pratique
Optimisation des requêtes SQL : 10 techniques essentielles
20 min de lecture

Optimisation des requêtes SQL : 10 techniques essentielles

Apprenez à optimiser vos requêtes SQL : index, EXPLAIN, SELECT *, sous-requêtes, partitionnement. 10 techniques concrètes pour l'entretien technique.

Avatar de Thomas LeroyThomas Leroy

📌 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érationSignificationPerformance
Seq ScanParcours séquentiel de toute la tableLent sur grosses tables
Index ScanUtilisation d'un indexRapide
Index Only ScanRéponse uniquement via l'indexTrès rapide
Nested LoopBoucle imbriquée pour les JOINsOK pour petites tables
Hash JoinTable de hachage pour le JOINBon pour grosses tables
SortTri en mémoire ou sur disqueCoû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 pratiqueBonne pratiqueGain
YEAR(date) = 2026date >= '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 insensibleIndex utilisé
prix * tva > 100prix > 100 / tvaIndex 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éthodePerformanceCas d'usage optimalAttention
INRapide si petite listeSous-requête retournant < 1000 valeursLent si gros volume
EXISTSExcellentTest d'existence, gros volumesSyntaxe plus complexe
INNER JOINTrès rapideQuand vous avez besoin des colonnes des 2 tablesPeut dupliquer les lignes
LEFT JOIN + IS NOT NULLRapideAlternative à EXISTSMoins 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.

Prêt à vous entraîner ?

50 exercices SQL interactifs avec éditeur en ligne, chronomètre et feedback IA.

Voir les exercices