Use Cases (Mis à jour: 01/06/2026)

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.

Optimiser SQL avec Claude Code : guide pratique

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’usageSymptômeAmélioration principaleRisque
Listes d’administrationRecherche lente, pages profondes lentesindex composites, pagination keyset, moins de colonnestrop de filtres optionnels multiplient les index
Pages SaaS ou e-commerceAPI de liste ralentie par la croissance du traficindex alignés sur WHERE et ORDER BY, cache, agrégations asynchronesne jamais oublier les filtres de stock, tenant ou droits
Tableaux de bordGraphiques journaliers ou mensuels qui scannent de grosses tablestables 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 include dans 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.

#Claude Code #SQL #base de données #performance #optimisation
Gratuit

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.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.