Use Cases (Aktualisiert: 1.6.2026)

SQL-Optimierung mit Claude Code: Praxisleitfaden

SQL-Optimierung mit Claude Code: Slow Queries, EXPLAIN, Indizes, N+1, Pagination, Aggregation, Tests und Rollout.

SQL-Optimierung mit Claude Code: Praxisleitfaden

Was SQL-Optimierung wirklich bedeutet

SQL-Optimierung bedeutet, Abfragen, Indizes und Datenzugriffe so anzupassen, dass dasselbe Ergebnis mit weniger Leseaufwand und kürzerer Wartezeit zurückkommt. Es geht nicht darum, eine einzelne SQL-Zeile besonders raffiniert aussehen zu lassen. In der Praxis gehören Endpoint-Code, ORM-Aufrufe, Ausführungspläne, realistische Datenmengen, Regressionstests und Monitoring nach dem Rollout zusammen.

Claude Code hilft, weil es mehr Kontext lesen kann als nur den SQL-String: Route Handler, Prisma- oder ORM-Modelle, Migrationen, Slow-Query-Logs und Tests. In produktiven Anwendungen entsteht Latenz oft durch viele kleine Muster: SELECT * auf Listen, N+1-Schleifen, tiefe OFFSET-Pagination, falsch angeordnete Composite-Indizes und Dashboard-Aggregationen, die bei jedem Seitenaufruf synchron laufen.

Dieser Leitfaden nutzt PostgreSQL als Hauptbeispiel und erwähnt MySQL dort, wo es relevant ist. Für Ausführungspläne solltest du die offiziellen Quellen prüfen: PostgreSQL Using EXPLAIN, PostgreSQL EXPLAIN, PostgreSQL Indexes, PostgreSQL Multicolumn Indexes und MySQL 8.4 Using EXPLAIN.

Ergänzend passen Datenbankdesign, Datenbankmigrationen, Prisma ORM und Performance-Optimierung.

Drei typische Einsatzfälle

Beginne mit dem konkreten Screen oder Workflow. So bekommt Claude Code eine klare Aufgabe statt allgemeiner Index-Ideen.

EinsatzfallSymptomHauptmaßnahmeRisiko
Admin-ListenSuche und tiefe Seiten sind langsamComposite-Indizes, Keyset-Pagination, weniger Spaltenzu viele optionale Filter erzeugen Index-Wildwuchs
SaaS- oder Shop-ListenAPIs werden bei mehr Traffic langsamIndizes passend zu WHERE und ORDER BY, Cache, asynchrone AggregationMandanten-, Bestands- oder Rechtefilter dürfen nicht wegfallen
DashboardsTages- oder Monatscharts scannen große TabellenSummary-Tabellen, Materialisierung, geplante RefreshsDatenfrische muss vorher geklärt sein

Lege vor der Änderung ein Query-Budget fest: zum Beispiel p95 unter 300 ms, maximal fünf SQL-Statements pro Listenrequest und unverändertes Response-JSON.

Prompt für Claude Code:
Optimiere diese Listen-API für SQL-Performance.
Ziel: p95 unter 300 ms, maximal 5 SQL-Statements, Response-JSON bleibt kompatibel.
Prüfe: src/routes/admin/orders.ts, prisma/schema.prisma, migrations, slow-query.log.
Aufgaben: langsame SQLs finden, EXPLAIN ANALYZE lesen, Indizes vorschlagen, N+1 entfernen, Regressionstests ergänzen.
Einschränkung: Änderung klein halten und produktionskompatibel bleiben.

Schritt 1: Slow Queries erfassen

Baue keine Indizes nach Gefühl. Erfasse zuerst die wirklich langsamen SQLs. In PostgreSQL helfen pg_stat_statements oder Datenbanklogs. ORM-Logs auf Anwendungsebene sind ebenfalls nützlich, weil sie die SQLs dem aufrufenden Endpoint zuordnen.

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;

Mit Prisma kannst du in der Entwicklung echte Query-Events ausgeben:

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,
    });
  }
});

Gib Claude Code nur bereinigte Logs. E-Mail-Adressen, Tokens und Kundennummern gehören maskiert. Bitte es dann, gleiche SQL-Muster nach Screen, Aufrufer, Durchschnittszeit, Anzahl der Aufrufe und vermuteter Ursache zu gruppieren.

Schritt 2: EXPLAIN und EXPLAIN ANALYZE lesen

Wenn eine Kandidatenabfrage feststeht, zeigt EXPLAIN den geplanten Ablauf. In einer sicheren Umgebung mit repräsentativen Daten nutzt du EXPLAIN (ANALYZE, BUFFERS). Beachte: ANALYZE führt die Abfrage tatsächlich aus, also nicht leichtfertig bei UPDATE oder DELETE verwenden.

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;

Ein Seq Scan ist nicht automatisch ein Fehler. Bei kleinen Tabellen oder wenig selektiven Bedingungen kann er richtig sein. Achte auf Abweichungen zwischen geschätzten und tatsächlichen Zeilen, große Sorts, hohe Buffer Reads und unerwartete Join-Reihenfolgen.

Prompt für Claude Code:
Bewerte dieses EXPLAIN ANALYZE. Trenne die Antwort in Bottleneck, Schätzfehler bei Zeilen, Indexkandidaten und sichere SQL-Umschreibungen. Das Resultset darf sich nicht ändern.

Schritt 3: Composite-Indizes entwerfen

Indizes beschleunigen Lesezugriffe, erhöhen aber Schreibkosten, Speicherbedarf und Migrationsrisiko. Entwirf sie aus realen Abfragen: Gleichheitsbedingungen zuerst, dann Bereichsbedingungen, Sortierung und Join-Bedarf.

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);

In PostgreSQL reduziert CREATE INDEX CONCURRENTLY Schreibblockaden, läuft aber nicht in einem normalen Transaktionsblock. Dein Migrationstool braucht eventuell eine Sonderbehandlung. Dokumentiere das im PR.

Gib Claude Code vor Indexvorschlägen die bestehenden Indizes:

SELECT
  indexname,
  indexdef
FROM pg_indexes
WHERE schemaname = 'public'
  AND tablename = 'orders'
ORDER BY indexname;

Schritt 4: N+1-Abfragen entfernen

N+1 bedeutet: erst eine Liste laden, danach pro Zeile verwandte Daten einzeln nachladen. Das erhöht Datenbankarbeit und Netzwerkwartezeit.

// Schlecht: eine Customer-Abfrage pro Order
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 ?? "" });
}
// Besser: nur benötigte Spalten gemeinsam laden
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,
      },
    },
  },
});

Bitte Claude Code, das Response-Format zu erhalten und die Query-Anzahl zu senken. So liest es eher UI und Serializer, statt blind große include-Bäume zu erzeugen.

Schritt 5: Tiefe OFFSET-Pagination ersetzen

OFFSET 10000 LIMIT 50 wird oft langsam, weil die Datenbank die übersprungenen Zeilen trotzdem durchlaufen muss. Für chronologische Listen und Infinite Scroll passt Keyset-Pagination mit dem zuletzt gesehenen created_at und 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;

Der Index (status, created_at DESC, id DESC) unterstützt dieses Muster. id stabilisiert die Reihenfolge, wenn mehrere Zeilen denselben Zeitstempel haben.

Schritt 6: Aggregation aus heißen Requests herausziehen

Dashboards werden oft langsam, weil jeder Request dieselben Summen neu berechnet. Wenn fachlich eine Verzögerung erlaubt ist, nutze Summary-Tabellen oder geplante Refreshs.

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();

Sage Claude Code ausdrücklich, wie frisch die Daten sein müssen: eine Minute, fünf Minuten, täglich oder wirklich live. Ohne diese Angabe kann eine Optimierung die Bedeutung des Dashboards ändern.

Query-Budget und Regressionstests

Performance braucht Leitplanken. Exakte Plan-Snapshots sind oft brüchig; beginne mit Query-Anzahl, Response-Kompatibilität und repräsentativen Fällen.

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);
});

Ablaufbild

flowchart LR
  A["Slow-Query-Log"] --> B["EXPLAIN / EXPLAIN ANALYZE"]
  B --> C["Index- und SQL-Änderungen"]
  C --> D["N+1, Pagination, Aggregation"]
  D --> E["Query-Budget-Tests"]
  E --> F["Rollout und Monitoring"]

Häufige Fallstricke

  • Listen mit SELECT * lesen große Spalten unnötig mit.
  • Ein einzelner Index auf eine Spalte mit wenigen Werten wie status löst selten das Problem.
  • LIKE '%keyword%' wird nicht durch einen normalen B-tree-Index schnell.
  • Kleine Entwicklungsdaten führen zu falschen Planbewertungen.
  • Indexmigrationen laufen zur Spitzenzeit ohne Prüfung von Locks, Replikationslag und Speicher.
  • ORM-include gibt ganze Beziehungsgraphen zurück.

Rollout-Checkliste

1. Endpoint und betroffener Screen sind benannt.
2. Slow SQL wurde aus Logs oder pg_stat_statements ermittelt.
3. Vorher/Nachher-EXPLAIN ist gespeichert.
4. Bestehende Indizes wurden geprüft.
5. N+1, OFFSET, ausgewählte Spalten und synchrone Aggregation sind geprüft.
6. Response-Kompatibilität ist getestet.
7. Query-Anzahl oder Latenz wird regressionsgetestet.
8. Lock-Risiko, Laufzeit und Rollback der Indexänderung sind dokumentiert.
9. Metriken für die Zeit nach dem Release sind festgelegt.

SQL-Optimierung ist ein Arbeitsablauf, kein einzelner Trick. Claude Code kann Code, Migrationen und Tests verbinden, aber die Entscheidung muss auf echten Daten, Ausführungsplänen und Produktanforderungen beruhen.

ClaudeCodeLab bietet Training und Beratung auf Basis solcher praktischen Reviews. Mit einem langsamen Endpoint, Logs und Schema lässt sich daraus eine wiederholbare SQL-Optimierungs-Checkliste für dein Team bauen.

#Claude Code #SQL #Datenbank #Performance #Optimierung
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.