Use Cases (Actualizado: 2/6/2026)

Diseño de bases de datos con Claude Code: guía práctica de revisión

Usa Claude Code para revisar diseño de bases de datos: tablas, restricciones, índices, migraciones y rollback.

Diseño de bases de datos con Claude Code: guía práctica de revisión

Diseñar una base de datos no consiste solo en elegir nombres de tablas. Hay que decidir qué datos pertenecen juntos, qué columnas no pueden quedar vacías, qué valores deben ser únicos, cómo se relacionan las filas y qué consultas tendrán que seguir siendo rápidas cuando el producto crezca. Un esquema débil puede funcionar en la demo inicial y fallar después con cuentas duplicadas, facturación inconsistente, paneles lentos o migraciones imposibles de revertir.

Claude Code ayuda mucho en esta fase, pero no debe tratarse como una fuente de verdad incuestionable. Úsalo como asistente de diseño y revisor: que proponga un esquema, enumere riesgos, genere SQL de prueba y cuestione supuestos. La decisión final debe venir de las reglas del producto, consultas reales, documentación oficial y revisión humana.

Para completar el flujo, lee también automatización de migraciones de base de datos y Prisma ORM con Claude Code. Para validar decisiones técnicas, consulta la documentación oficial de restricciones en PostgreSQL, índices en PostgreSQL y OWASP Top 10 cuando haya datos sensibles.

Conceptos básicos sin jerga innecesaria

Una tabla agrupa registros del mismo tipo, como users, teams o projects. Una columna es un campo de cada registro, como email, status o created_at. La clave primaria identifica una fila. La clave externa garantiza que una fila apunte a otra fila existente. Una restricción única evita duplicados de negocio, por ejemplo emails, slugs dentro de un equipo o IDs de cliente de Stripe. Un índice acelera búsquedas frecuentes. Una migración es el script repetible que cambia el esquema, y el rollback es el camino de vuelta si la release sale mal.

El error de principiante es dejar estas reglas solo en la aplicación. La validación del formulario mejora la experiencia, pero no protege contra escrituras concurrentes ni procesos batch. La base de datos debe ser la última barrera. Claude Code es útil cuando le pides que encuentre esa barrera que falta.

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."

Cuatro casos de uso concretos

El primer caso es un SaaS con usuarios, equipos y proyectos. Un usuario puede pertenecer a varios equipos, y cada equipo tiene proyectos. El slug de un proyecto debe ser único dentro del equipo, no necesariamente en todo el producto. Por eso la regla correcta suele ser UNIQUE (team_id, slug).

El segundo caso es un esquema de auditoría. Queremos registrar quién hizo qué y cuándo, pero una tabla de auditoría no debe convertirse en un basurero de JSON con emails, teléfonos o direcciones. Esos datos suelen tener requisitos de retención y acceso más estrictos.

El tercer caso es billing y suscripciones. El ID de cliente del proveedor no puede repetirse, y un equipo no debería tener dos suscripciones activas. A la vez, conviene conservar el historial cancelado.

El cuarto caso es búsqueda o reporting. A veces una tabla optimizada para lectura evita JOIN complejos en cada pantalla. El precio es que hay que diseñar refresco, índices y verificación para no mostrar datos obsoletos.

DDL de PostgreSQL ejecutable

Este ejemplo recrea un schema dedicado para que puedas pegarlo en una base local sin tocar otros schemas.

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

Datos semilla y pruebas de restricciones

Un buen diseño debe demostrar tanto casos válidos como casos que deben fallar. Los bloques DO convierten los errores esperados en avisos legibles.

SET search_path = claude_db_design_demo;

INSERT INTO users (email, display_name, password_hash) VALUES
  ('masa@example.com', 'Masa', '$2b$demo-hash-1'),
  ('aiko@example.com', 'Aiko', '$2b$demo-hash-2'),
  ('ren@example.com', '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 ('masa@example.com', '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":"customer@example.com"}'::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;

Fallos que debes buscar

La falta de restricciones únicas es el fallo más frecuente. Email, cuenta OAuth, cliente de pago y slug por equipo suelen necesitar protección en la base de datos, no solo en el backend. Otro fallo es permitir NULL sin una razón clara. Si projects.name, team_members.role o subscriptions.status quedan vacíos, la UI y las reglas de permisos empiezan a adivinar.

Las migraciones destructivas necesitan revisión humana: DROP COLUMN, cambios de tipo, NOT NULL sobre datos existentes y valores por defecto en tablas grandes pueden bloquear producción o perder información. Pide a Claude Code una estrategia expand-and-contract y valida el resultado. También revisa N+1: una lista de proyectos que consulta el conteo de auditoría uno por uno se convierte en 101 consultas para 100 proyectos.

El último riesgo es PII en logs. Las tablas de auditoría, búsqueda y reporting suelen vivir más tiempo y tener más lectores. No guardes emails, teléfonos, direcciones o tokens en metadata por comodidad.

Checklist de migración y rollback

# 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.

Lee el rollback con especial cuidado. Un down migration generado puede devolver la forma de la tabla, pero no los datos eliminados. Si el cambio no es completamente reversible, escríbelo en el PR y prepara backup, despliegue por fases o recuperación.

Compartir el diseño con un diagrama

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

El diagrama ayuda a conversar, pero no reemplaza DDL, migraciones ni SQL de prueba. Un ER bonito sin restricciones únicas sigue permitiendo datos rotos.

CTA y resultado práctico

Si tu equipo va a usar Claude Code para diseño de bases de datos, escribe las reglas en CLAUDE.md: no conectar a producción, dividir migraciones destructivas, exigir pruebas de restricciones y prohibir PII en logs sin aprobación. ClaudeCodeLab ayuda con adopción de Claude Code, plantillas de revisión, checklists de PR y formación con repositorios reales. Para aplicarlo en un producto existente, empieza por formación y consultoría de Claude Code. También puedes revisar la cheatsheet gratuita y las plantillas de productos.

Cuando Masa probó este flujo, lo más útil no fue el primer diagrama ER, sino los INSERT que debían fallar: email duplicado, nombre de proyecto nulo, equipo inexistente, slug repetido, email en auditoría y segunda suscripción activa. Claude Code aceleró la lista de revisión; la confianza vino de ver a PostgreSQL rechazar datos malos.

#claude-code #database #prisma #sql
Gratis

PDF gratis: cheatsheet de Claude Code

Introduce tu email y descarga una hoja con comandos, hábitos de revisión y flujos seguros.

Cuidamos tus datos y no enviamos spam.

Masa

Sobre el autor

Masa

Ingeniero enfocado en workflows prácticos con Claude Code.