Optimiser SQL avec Claude Code : guide pratique
Optimisez SQL avec Claude Code : requêtes lentes, EXPLAIN, index, N+1, pagination, agrégations, tests et déploiement.
Ce que signifie vraiment optimiser SQL
Optimiser SQL consiste à ajuster les requêtes, les index et les modes d’accès aux données pour renvoyer le même résultat avec moins de lectures et moins d’attente. Ce n’est pas seulement écrire une requête plus élégante. En production, il faut regarder l’endpoint applicatif, les appels ORM, le plan d’exécution, le volume de données réel, les tests de non-régression et le monitoring après déploiement.
Claude Code est utile parce qu’il peut lire le contexte complet : route handler, modèle Prisma ou autre ORM, migrations, journaux de requêtes lentes et tests. Dans les applications réelles, la lenteur vient souvent d’une accumulation de détails : SELECT * sur les listes, boucles N+1, pagination OFFSET profonde, index composites mal alignés et agrégations de tableau de bord recalculées à chaque requête.
Ce guide se concentre sur PostgreSQL, avec des remarques applicables à MySQL. Pour interpréter les plans, gardez les références officielles à portée de main : PostgreSQL Using EXPLAIN, PostgreSQL EXPLAIN, PostgreSQL Indexes, PostgreSQL Multicolumn Indexes et MySQL 8.4 Using EXPLAIN.
Pour compléter le sujet, consultez aussi la conception de base de données, les migrations de base de données, Prisma ORM et l’optimisation des performances.
Trois cas d’usage concrets
Commencez par l’écran ou le workflow. Claude Code pourra alors proposer des changements ciblés au lieu d’index génériques.
| Cas d’usage | Symptôme | Amélioration principale | Risque |
|---|---|---|---|
| Listes d’administration | Recherche lente, pages profondes lentes | index composites, pagination keyset, moins de colonnes | trop de filtres optionnels multiplient les index |
| Pages SaaS ou e-commerce | API de liste ralentie par la croissance du trafic | index alignés sur WHERE et ORDER BY, cache, agrégations asynchrones | ne jamais oublier les filtres de stock, tenant ou droits |
| Tableaux de bord | Graphiques journaliers ou mensuels qui scannent de grosses tables | tables de synthèse, vues matérialisées, rafraîchissement planifié | la fraîcheur des données doit être décidée avant |
Définissez un budget avant de modifier : p95 sous 300 ms, cinq requêtes SQL maximum par liste, JSON de réponse inchangé.
Prompt pour Claude Code:
Optimise cette API de liste côté SQL.
Objectif: p95 sous 300 ms, maximum 5 requêtes SQL, JSON de réponse inchangé.
À lire: src/routes/admin/orders.ts, prisma/schema.prisma, migrations, slow-query.log.
À faire: identifier les SQL lents, lire EXPLAIN ANALYZE, proposer les index, supprimer N+1, ajouter des tests de régression.
Contrainte: garder un changement limité et compatible production.
Étape 1 : capturer les requêtes lentes
Ne créez pas d’index au jugé. Capturez d’abord les requêtes réellement lentes. Avec PostgreSQL, pg_stat_statements et les logs de base de données sont de bons points d’entrée. Les logs ORM côté application aident aussi, car ils relient la requête SQL à l’endpoint.
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
SELECT
query,
calls,
ROUND(total_exec_time::numeric, 2) AS total_ms,
ROUND(mean_exec_time::numeric, 2) AS mean_ms,
rows
FROM pg_stat_statements
WHERE query NOT ILIKE '%pg_stat_statements%'
ORDER BY total_exec_time DESC
LIMIT 10;
Avec Prisma, vous pouvez journaliser les requêtes lentes en développement avec les API réelles du client :
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient({
log: [
{ emit: "event", level: "query" },
{ emit: "stdout", level: "error" },
{ emit: "stdout", level: "warn" },
],
});
prisma.$on("query", (event) => {
if (event.duration > 100) {
console.log({
durationMs: event.duration,
query: event.query,
params: event.params,
});
}
});
Transmettez à Claude Code des logs nettoyés. Masquez les e-mails, tokens et identifiants clients, puis demandez un regroupement par écran, appelant, temps moyen, volume d’appels et cause probable.
Étape 2 : lire EXPLAIN et EXPLAIN ANALYZE
Une fois la requête identifiée, EXPLAIN montre le plan prévu. Dans un environnement sûr avec des données représentatives, utilisez EXPLAIN (ANALYZE, BUFFERS). Attention : ANALYZE exécute réellement la requête, donc soyez prudent avec UPDATE et DELETE.
EXPLAIN (ANALYZE, BUFFERS)
SELECT
o.id,
o.created_at,
o.status,
c.name AS customer_name
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE o.status = 'paid'
AND o.created_at >= DATE '2026-01-01'
ORDER BY o.created_at DESC, o.id DESC
LIMIT 50;
Un Seq Scan n’est pas automatiquement mauvais. Pour une petite table ou un filtre peu sélectif, il peut être le meilleur choix. Regardez plutôt l’écart entre lignes estimées et réelles, les gros tris, les lectures de buffers et l’ordre des jointures.
Prompt pour Claude Code:
Analyse cet EXPLAIN ANALYZE. Sépare la réponse en goulot d'étranglement, écart d'estimation des lignes, candidats d'index et réécritures SQL sûres. Ne change pas le résultat retourné.
Étape 3 : concevoir des index composites
Les index accélèrent la lecture, mais ils augmentent les coûts d’écriture, le stockage et le risque de migration. Partez des requêtes réelles : conditions d’égalité, conditions de plage, colonnes de tri, puis jointures.
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_status_created_id
ON orders (status, created_at DESC, id DESC);
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_customer_created
ON orders (customer_id, created_at DESC);
Dans PostgreSQL, CREATE INDEX CONCURRENTLY limite le blocage des écritures, mais ne peut pas être exécuté dans une transaction normale. Votre outil de migration peut nécessiter une option spéciale ou un script séparé.
Avant de demander des index à Claude Code, fournissez les index existants :
SELECT
indexname,
indexdef
FROM pg_indexes
WHERE schemaname = 'public'
AND tablename = 'orders'
ORDER BY indexname;
Étape 4 : supprimer le N+1
Le N+1 apparaît quand on charge une liste, puis les données liées ligne par ligne. Cela ajoute du travail côté base et de l’attente réseau.
// Mauvais exemple: une requête customer par commande
const orders = await prisma.order.findMany({
take: 100,
orderBy: { createdAt: "desc" },
});
const rows = [];
for (const order of orders) {
const customer = await prisma.customer.findUnique({
where: { id: order.customerId },
});
rows.push({ ...order, customerName: customer?.name ?? "" });
}
// Mieux: charger seulement les colonnes nécessaires à l'UI
const orders = await prisma.order.findMany({
take: 100,
orderBy: { createdAt: "desc" },
select: {
id: true,
createdAt: true,
status: true,
totalAmount: true,
customer: {
select: {
id: true,
name: true,
},
},
},
});
Demandez à Claude Code de préserver la forme de la réponse tout en réduisant le nombre de requêtes. Cela l’oblige à lire l’UI ou le serializer plutôt qu’à ajouter des include trop larges.
Étape 5 : remplacer OFFSET profond par une pagination keyset
OFFSET 10000 LIMIT 50 devient souvent lent, car la base doit parcourir les lignes ignorées. Pour des listes chronologiques ou un scroll infini, utilisez un curseur basé sur le dernier created_at et le dernier id.
SELECT id, created_at, status, total_amount
FROM orders
WHERE status = 'paid'
ORDER BY created_at DESC, id DESC
LIMIT 50;
SELECT id, created_at, status, total_amount
FROM orders
WHERE status = 'paid'
AND (created_at, id) < (TIMESTAMP '2026-05-01 10:30:00', 98765)
ORDER BY created_at DESC, id DESC
LIMIT 50;
L’index (status, created_at DESC, id DESC) correspond à cet accès. Le id stabilise l’ordre quand plusieurs lignes partagent le même horodatage.
Étape 6 : sortir les agrégations des requêtes chaudes
Les tableaux de bord deviennent lents quand chaque requête recalcule les mêmes totaux. Si le métier accepte un léger délai, utilisez une table de synthèse ou un refresh planifié.
CREATE TABLE IF NOT EXISTS daily_sales_summary (
sales_date date PRIMARY KEY,
order_count integer NOT NULL,
revenue numeric(12, 2) NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO daily_sales_summary (sales_date, order_count, revenue)
SELECT
created_at::date AS sales_date,
COUNT(*) AS order_count,
SUM(total_amount) AS revenue
FROM orders
WHERE status = 'paid'
AND created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY created_at::date
ON CONFLICT (sales_date) DO UPDATE
SET
order_count = EXCLUDED.order_count,
revenue = EXCLUDED.revenue,
updated_at = now();
Précisez à Claude Code la fraîcheur attendue : une minute, cinq minutes, une fois par jour ou temps réel strict. Sans cela, une optimisation peut changer le sens métier du tableau de bord.
Budget de requêtes et tests
Une optimisation peut régresser. Les snapshots exacts de plans sont fragiles ; commencez par tester le nombre de requêtes, la compatibilité de réponse et quelques cas représentatifs.
import { PrismaClient } from "@prisma/client";
import { expect, it } from "vitest";
it("keeps the order list within the query budget", async () => {
const queries: string[] = [];
const prisma = new PrismaClient({
log: [{ emit: "event", level: "query" }],
});
prisma.$on("query", (event) => {
queries.push(event.query);
});
await prisma.order.findMany({
take: 50,
orderBy: [{ createdAt: "desc" }, { id: "desc" }],
select: {
id: true,
createdAt: true,
status: true,
customer: { select: { id: true, name: true } },
},
});
await prisma.$disconnect();
expect(queries.length).toBeLessThanOrEqual(2);
});
Schéma de workflow
flowchart LR
A["Logs de requêtes lentes"] --> B["EXPLAIN / EXPLAIN ANALYZE"]
B --> C["Index et modifications SQL"]
C --> D["N+1, pagination, agrégation"]
D --> E["Tests de budget SQL"]
E --> F["Déploiement et monitoring"]
Pièges fréquents
- Lire de grosses colonnes avec
SELECT *sur les pages de liste. - Attendre trop d’un index seul sur une colonne peu sélective comme
status. - Essayer d’accélérer
LIKE '%keyword%'avec un index B-tree classique. - Juger le plan sur une petite base de développement.
- Lancer une migration d’index en heure de pointe sans vérifier locks, disque et réplication.
- Utiliser
includedans l’ORM et retourner tout le graphe relationnel.
Checklist de déploiement
1. L'endpoint et l'écran sont identifiés.
2. La requête lente vient des logs ou de pg_stat_statements.
3. Les EXPLAIN avant/après sont conservés.
4. Les index existants ont été vérifiés.
5. N+1, OFFSET, colonnes retournées et agrégations synchrones sont revus.
6. La compatibilité de réponse est testée.
7. Le nombre de requêtes ou la latence est couvert par un test.
8. Le risque de lock, la durée et le rollback d'index sont documentés.
9. Les métriques à suivre après release sont décidées.
L’optimisation SQL est un workflow, pas une astuce isolée. Claude Code peut relier code, migrations et tests, mais la décision finale doit venir des données réelles, des plans d’exécution et des exigences produit.
ClaudeCodeLab propose des formations et consultations basées sur ce type de revue pratique. Avec un endpoint lent, des logs et un schéma, vous pouvez construire une checklist SQL réutilisable pour votre équipe.
PDF gratuit: cheatsheet Claude Code
Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.
Nous protégeons vos données et n'envoyons pas de spam.
À propos de l'auteur
Masa
Ingénieur spécialisé dans les workflows pratiques avec Claude Code.
Articles liés
Workflow Obsidian vers CLAUDE.md avec Claude Code
Transformer des notes Obsidian en notes CLAUDE.md concises pour reprendre les sessions sans réexpliquer.
Claude Code Revenue CTA Routing : relier articles, PDF, Gumroad et consultation
Un workflow Claude Code pour orienter les lecteurs vers PDF gratuit, Gumroad ou consultation selon l'intention.
Règles de handoff Claude Code en équipe: preuves, permissions, rollback et revenus
Un format concret pour transmettre un travail Claude Code avec preuves, permissions, rollback, PDF gratuit, Gumroad et consultation.