Datenbankdesign mit Claude Code: praktischer Review-Workflow
Claude Code als Review-Hilfe für Datenbankdesign: Tabellen, Constraints, Indizes, Migrationen und Rollback.
Datenbankdesign bedeutet nicht nur, Tabellen zu benennen. Du entscheidest, welche Fakten zusammengehören, welche Spalten niemals leer sein dürfen, welche Werte eindeutig sein müssen, wie Tabellen miteinander verbunden sind und ob Listen, Suche und Reporting auch später noch schnell bleiben. Ein schwaches Schema sieht in der ersten Demo oft gut aus, erzeugt aber später doppelte Accounts, widersprüchliche Abos, langsame Dashboards oder Migrationen, die sich nicht sauber zurückrollen lassen.
Claude Code kann diesen Prozess stark beschleunigen, sollte aber nicht als unfehlbare Quelle behandelt werden. Nutze es als Designassistent und Reviewer. Es kann Entwürfe erstellen, Risiken aufzählen, Test-SQL schreiben und Annahmen hinterfragen. Die finale Entscheidung muss trotzdem bei Menschen liegen, die Produktregeln, echte Queries, offizielle Dokumentation und Betriebsrisiken kennen.
Als Ergänzung passen Datenbankmigrationen mit Claude Code und Prisma ORM mit Claude Code. Für technische Entscheidungen solltest du die offiziellen PostgreSQL-Seiten zu Constraints und Indexes prüfen. Sobald Logs, Suche oder Reporting sensible Daten enthalten können, gehört auch OWASP Top 10 in die Review-Perspektive.
Grundbegriffe
Eine Tabelle sammelt Datensätze einer Art, etwa users, teams oder projects. Eine Spalte ist ein Feld eines Datensatzes, etwa email, status oder created_at. Der Primary Key identifiziert eine Zeile eindeutig. Ein Foreign Key stellt sicher, dass eine Zeile auf eine vorhandene Elternzeile zeigt. Ein Unique Constraint verhindert doppelte Geschäftswerte wie E-Mail, Team-Slug oder Zahlungsanbieter-Kundennummer. Ein Index beschleunigt häufige Lesezugriffe. Eine Migration beschreibt eine Schemaänderung wiederholbar, und ein Rollback ist der Rückweg, wenn ein Release falsch läuft.
Der typische Anfängerfehler ist, diese Regeln nur in der Anwendung zu prüfen. Formularvalidierung ist gut für UX, schützt aber nicht vor parallelen Requests oder Batch-Jobs. Die Datenbank braucht die letzte Schutzschicht. Genau dort hilft Claude Code: Es soll finden, welche Schutzschicht im Schema fehlt.
claude -p "Read docs/database/schema.sql and migrations/*.sql.
Act as a database design reviewer, not as the owner of truth.
Check for:
- Missing primary keys, foreign keys, unique constraints, and NOT NULL rules
- status or role columns that are too loose
- Indexes needed for WHERE, JOIN, and ORDER BY patterns
- Destructive migrations and rollback gaps
- PII leaking into audit logs or search/reporting tables
- API shapes that may trigger N+1 queries
Return findings first. Include why it matters, how to verify it, and only then suggest a minimal patch."
Vier konkrete Use Cases
Der erste Use Case ist ein SaaS mit Users, Teams und Projects. Ein User kann in mehreren Teams sein, und jedes Team besitzt Projekte. Ein Project-Slug muss meist nur innerhalb des Teams eindeutig sein. Darum ist UNIQUE (team_id, slug) oft besser als ein globaler Slug.
Der zweite Use Case ist ein Audit Log. Du willst wissen, wer wann welches Objekt geändert hat. Trotzdem darf die Audit-Tabelle nicht zur JSON-Ablage für alles werden. E-Mails, Telefonnummern oder Adressen in metadata wirken bequem, erschweren aber Aufbewahrung, Zugriffskontrolle und Löschanfragen.
Der dritte Use Case ist Billing. Die Kunden-ID des Payment Providers darf sich nicht wiederholen, und ein Team sollte nicht zwei aktive Subscriptions haben. Gleichzeitig sollen gekündigte Historienzeilen erhalten bleiben.
Der vierte Use Case ist Suche oder Reporting. Eine read-optimierte Tabelle kann komplexe JOINs vermeiden. Dann brauchst du aber klare Regeln für Aktualisierung, Indexe und Verifikation, sonst lieferst du veraltete Suchergebnisse aus.
Ausführbares PostgreSQL-DDL
Dieses SQL erstellt nur ein eigenes Demo-Schema neu und eignet sich für eine lokale PostgreSQL-Datenbank.
DROP SCHEMA IF EXISTS claude_db_design_demo CASCADE;
CREATE SCHEMA claude_db_design_demo;
SET search_path = claude_db_design_demo;
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
display_name TEXT NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT users_email_unique UNIQUE (email),
CONSTRAINT users_email_has_at CHECK (position('@' in email) > 1)
);
CREATE TABLE teams (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT teams_slug_unique UNIQUE (slug)
);
CREATE TABLE team_members (
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (team_id, user_id)
);
CREATE TABLE projects (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
name TEXT NOT NULL,
slug TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived')),
created_by BIGINT NOT NULL REFERENCES users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT projects_team_slug_unique UNIQUE (team_id, slug)
);
CREATE TABLE audit_logs (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
actor_user_id BIGINT REFERENCES users(id) ON DELETE SET NULL,
action TEXT NOT NULL,
entity_type TEXT NOT NULL,
entity_id TEXT NOT NULL,
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT audit_action_not_blank CHECK (length(action) > 0),
CONSTRAINT audit_metadata_no_email CHECK (
metadata::text !~* '[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}'
)
);
CREATE TABLE subscriptions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
provider TEXT NOT NULL CHECK (provider IN ('stripe', 'manual')),
provider_customer_id TEXT NOT NULL,
plan_code TEXT NOT NULL CHECK (plan_code IN ('free', 'pro', 'business')),
status TEXT NOT NULL CHECK (status IN ('trialing', 'active', 'past_due', 'canceled')),
current_period_end DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CONSTRAINT subscriptions_provider_customer_unique UNIQUE (provider, provider_customer_id)
);
CREATE TABLE project_search_documents (
project_id BIGINT PRIMARY KEY REFERENCES projects(id) ON DELETE CASCADE,
team_id BIGINT NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
title TEXT NOT NULL,
search_text TEXT NOT NULL,
refreshed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX subscriptions_one_open_per_team
ON subscriptions(team_id)
WHERE status IN ('trialing', 'active', 'past_due');
CREATE INDEX idx_team_members_user_id ON team_members(user_id);
CREATE INDEX idx_projects_team_status_created ON projects(team_id, status, created_at DESC);
CREATE INDEX idx_audit_logs_team_created ON audit_logs(team_id, created_at DESC);
CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_project_search_documents_search
ON project_search_documents USING GIN (to_tsvector('simple', search_text));
Seed-Daten und Constraint-Tests
Ein gutes Schema zeigt nicht nur erfolgreiche Inserts. Es muss auch schlechte Daten ablehnen. Die DO-Blöcke fangen erwartete Fehler ab und machen sie sichtbar.
SET search_path = claude_db_design_demo;
INSERT INTO users (email, display_name, password_hash) VALUES
('[email protected]', 'Masa', '$2b$demo-hash-1'),
('[email protected]', 'Aiko', '$2b$demo-hash-2'),
('[email protected]', 'Ren', '$2b$demo-hash-3');
INSERT INTO teams (name, slug) VALUES
('ClaudeCodeLab', 'clab'),
('Northstar Apps', 'northstar');
INSERT INTO team_members (team_id, user_id, role) VALUES
(1, 1, 'owner'),
(1, 2, 'admin'),
(2, 3, 'owner');
INSERT INTO projects (team_id, name, slug, status, created_by) VALUES
(1, 'Schema Review', 'schema-review', 'active', 1),
(1, 'Billing Console', 'billing-console', 'active', 2),
(2, 'Customer Portal', 'customer-portal', 'active', 3);
INSERT INTO audit_logs (team_id, actor_user_id, action, entity_type, entity_id, metadata) VALUES
(1, 1, 'project.created', 'project', '1', '{"project_slug":"schema-review"}'::jsonb),
(1, 2, 'billing.plan_changed', 'subscription', '1', '{"from":"free","to":"pro"}'::jsonb);
INSERT INTO subscriptions (team_id, provider, provider_customer_id, plan_code, status, current_period_end) VALUES
(1, 'stripe', 'cus_demo_1', 'pro', 'active', DATE '2026-07-31'),
(2, 'manual', 'manual_northstar_1', 'free', 'canceled', NULL);
INSERT INTO project_search_documents (project_id, team_id, title, search_text)
SELECT
id,
team_id,
name,
name || ' project notes for database design, billing, audit, and reporting.'
FROM projects;
SET search_path = claude_db_design_demo;
DO $$
BEGIN
INSERT INTO users (email, display_name, password_hash)
VALUES ('[email protected]', 'Duplicate Masa', '$2b$demo-hash-x');
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'OK duplicate email blocked: %', SQLERRM;
END $$;
DO $$
BEGIN
INSERT INTO projects (team_id, name, slug, created_by)
VALUES (1, NULL, 'bad-null', 1);
EXCEPTION WHEN not_null_violation THEN
RAISE NOTICE 'OK NULL project name blocked: %', SQLERRM;
END $$;
DO $$
BEGIN
INSERT INTO team_members (team_id, user_id, role)
VALUES (999, 1, 'member');
EXCEPTION WHEN foreign_key_violation THEN
RAISE NOTICE 'OK missing team blocked: %', SQLERRM;
END $$;
DO $$
BEGIN
INSERT INTO projects (team_id, name, slug, created_by)
VALUES (1, 'Duplicate Slug', 'schema-review', 1);
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'OK duplicate team slug blocked: %', SQLERRM;
END $$;
DO $$
BEGIN
INSERT INTO audit_logs (team_id, actor_user_id, action, entity_type, entity_id, metadata)
VALUES (1, 1, 'project.invited', 'project', '1', '{"email":"[email protected]"}'::jsonb);
EXCEPTION WHEN check_violation THEN
RAISE NOTICE 'OK email in audit metadata blocked: %', SQLERRM;
END $$;
DO $$
BEGIN
INSERT INTO subscriptions (team_id, provider, provider_customer_id, plan_code, status, current_period_end)
VALUES (1, 'stripe', 'cus_demo_2', 'business', 'active', DATE '2026-08-31');
EXCEPTION WHEN unique_violation THEN
RAISE NOTICE 'OK second open subscription blocked: %', SQLERRM;
END $$;
SELECT p.id, p.name, count(a.id) AS audit_events
FROM projects p
LEFT JOIN audit_logs a
ON a.entity_type = 'project'
AND a.entity_id = p.id::text
WHERE p.team_id = 1
GROUP BY p.id, p.name
ORDER BY audit_events DESC, p.id;
SELECT project_id, title
FROM project_search_documents
WHERE to_tsvector('simple', search_text) @@ plainto_tsquery('simple', 'billing');
EXPLAIN (COSTS OFF)
SELECT *
FROM audit_logs
WHERE team_id = 1
ORDER BY created_at DESC
LIMIT 20;
Typische Fehler
Der häufigste Fehler sind fehlende Unique Constraints. E-Mail, OAuth-Konto, Payment-Customer-ID und Team-Slug gehören meist in die Datenbank abgesichert. Ein zweiter Fehler sind zu viele nullable Spalten. Wenn projects.name, team_members.role oder subscriptions.status leer sein können, geraten UI, Berechtigungen und Billing ins Raten.
Der dritte Fehler sind destruktive Migrationen. DROP COLUMN, Typänderungen, NOT NULL auf bestehenden Daten oder Defaults auf riesigen Tabellen können Produktion sperren oder Daten verlieren. Bitte Claude Code um einen Expand-and-Contract-Plan und prüfe ihn kritisch. Der vierte Fehler ist N+1: Eine Projektliste, die pro Projekt Audit-Zähler lädt, erzeugt bei 100 Projekten 101 Queries. Der fünfte Fehler ist PII in Logs, Suche oder Reporting.
Migration- und Rollback-Checklist
# Migration checklist: add projects.archived_at
## Design
- [ ] Why this column is needed is written in the issue or PR.
- [ ] Existing rows and backfill behavior are defined.
- [ ] NOT NULL, default value, and CHECK constraint choices are explained.
- [ ] Index impact was checked against real WHERE / ORDER BY queries.
## Safety
- [ ] This migration does not drop data in the first deploy.
- [ ] Large-table locks were considered.
- [ ] The application can run during the expand-and-contract period.
- [ ] Backfill can be retried safely.
## Up SQL
ALTER TABLE projects ADD COLUMN archived_at TIMESTAMPTZ;
UPDATE projects
SET archived_at = now()
WHERE status = 'archived' AND archived_at IS NULL;
ALTER TABLE projects
ADD CONSTRAINT projects_archived_at_matches_status
CHECK (
(status = 'archived' AND archived_at IS NOT NULL)
OR (status = 'active' AND archived_at IS NULL)
) NOT VALID;
ALTER TABLE projects VALIDATE CONSTRAINT projects_archived_at_matches_status;
## Down SQL
ALTER TABLE projects DROP CONSTRAINT IF EXISTS projects_archived_at_matches_status;
ALTER TABLE projects DROP COLUMN IF EXISTS archived_at;
## Verification
- [ ] Constraint test SQL was run locally.
- [ ] Read queries still use expected indexes.
- [ ] Rollback was reviewed, including data-loss notes.
- [ ] Claude Code findings were reviewed by a human owner.
Lies den Rollback besonders streng. Ein generiertes Down-Skript kann die Tabellenform wiederherstellen, aber gelöschte Daten nicht zurückholen. Wenn etwas nicht vollständig reversibel ist, schreibe es in den PR und plane Backup, gestaffelten Rollout oder Recovery separat.
ER-Diagramm für Reviews
erDiagram
users ||--o{ team_members : joins
teams ||--o{ team_members : has
teams ||--o{ projects : owns
users ||--o{ projects : creates
teams ||--o{ audit_logs : records
users ||--o{ audit_logs : acts
teams ||--o{ subscriptions : pays
projects ||--|| project_search_documents : indexes
Das Diagramm hilft beim Gespräch, ersetzt aber kein DDL, keine Migration und keine Constraint-Tests. Ein schönes Diagramm ohne Unique Constraint schützt keine Daten.
CTA und Ergebnis aus der Praxis
Wenn dein Team Claude Code für Datenbankdesign nutzt, schreibe die Regeln in CLAUDE.md: kein direkter Zugriff auf Produktionsdatenbanken, destruktive Migrationen nur gestuft, Constraint-Tests verpflichtend, keine PII in Logs ohne Freigabe. ClaudeCodeLab unterstützt Teams bei Claude-Code-Einführung, Review-Templates, PR-Checklists und Training an realen Repositories. Für bestehende Produkte starte über Claude Code Training und Beratung. Für Einzelpersonen helfen die kostenlose Cheatsheet und Produkt-Templates.
Als Masa diesen Ablauf getestet hat, war nicht das erste ER-Diagramm der größte Gewinn, sondern die Inserts, die fehlschlagen sollten: doppelte E-Mail, leerer Projektname, fehlendes Team, doppelter Slug, E-Mail im Audit-Log und zweite aktive Subscription. Claude Code beschleunigte die Review-Liste, aber Vertrauen entstand erst dadurch, dass PostgreSQL schlechte Daten wirklich ablehnte.
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.
Über den Autor
Masa
Engineer für praktische Claude-Code-Workflows und Team-Einführung.
Ähnliche Artikel
Claude Code Workflow von Obsidian zu CLAUDE.md
Obsidian-Arbeitsnotizen in CLAUDE.md-Betriebsnotizen verwandeln und Kontext nicht ständig neu erklären.
Claude Code Revenue CTA Routing: Artikel zu PDF, Gumroad und Beratung führen
Ein Claude-Code-Ablauf, der Leser nach Absicht zu Gratis-PDF, Gumroad oder Beratung führt.
Claude-Code-Team-Handoff-Regeln: Belege, Berechtigungen, Rollback und Umsatzpfade
Ein praktisches Claude-Code-Handoff für Review-Belege, Berechtigungen, Rollback, Gratis-PDF, Gumroad und Beratung.