🛡️

Plan de Migraciones — NutriTrack Pro v2.0

PostgreSQL 15 · Prisma · 2.3M rows · Zero Downtime

✓ Zero Downtime 4 Migraciones 2.3M Filas Prisma ORM
Tabla crítica
users
2,300,000 filas
SLA objetivo
99.9%
Sin ventana de mantenimiento
Duración estimada
7 días
Patrón expand-contract completo
Riesgo global
BAJO
Con checklist aplicado
🗂 Migraciones ordenadas
1
20260618_001_add_display_name_column
EXPAND: Añadir columna display_name (nullable)
DDL ~0ms lock
Paso 1 del patrón expand-contract para renombrar usernamedisplay_name. La nueva columna es nullable — sin lock, sin reescritura de filas.
ℹ️ Añadir una columna nullable en PostgreSQL 11+ es una operación metadata-only: no requiere reescribir la tabla.
 prisma/migrations/20260618_001_add_display_name_column/migration.sql
-- +migrate Up
ALTER TABLE users
  ADD COLUMN display_name TEXT;

-- La columna es nullable intencionalmente:
-- la app puede escribir en ambas columnas sin error durante la transición.
-- NO usar NOT NULL aquí todavía.

-- +migrate Down
ALTER TABLE users
  DROP COLUMN IF EXISTS display_name;
  • Columna nullable — sin bloqueo de tabla
  • Tiene DOWN migration (DROP COLUMN IF EXISTS)
  • No mezcla DDL con DML
2
20260619_002_backfill_display_name
BACKFILL: Poblar display_name desde username
DML Batch 10k
Migración de datos separada del DDL. Actualiza 2.3M filas en lotes de 10.000 con FOR UPDATE SKIP LOCKED para no bloquear lecturas concurrentes.
⚠️ Ejecutar esta migración en horario de menor tráfico (ej. 03:00 UTC). Cada iteración hace COMMIT, lo que mantiene latencia baja. Tiempo estimado: 4–8 minutos.
 prisma/migrations/20260619_002_backfill_display_name/migration.sql
-- +migrate Up (data migration — no DOWN needed)
DO $$
DECLARE
  batch_size  INT := 10000;
  rows_updated INT;
BEGIN
  LOOP
    UPDATE users
    SET    display_name = username
    WHERE  id IN (
      SELECT id FROM users
      WHERE  display_name IS NULL
      LIMIT  batch_size
      FOR UPDATE SKIP LOCKED
    );
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    RAISE NOTICE 'Backfill: % filas actualizadas', rows_updated;
    EXIT WHEN rows_updated = 0;
    COMMIT;
  END LOOP;
END $$;

-- Verificar que todos los registros tienen display_name
SELECT COUNT(*) FROM users WHERE display_name IS NULL;
-- Debe retornar 0 antes de continuar.
  • DML separado del DDL (migraciones distintas)
  • Batches de 10k — sin lock global de tabla
  • SKIP LOCKED — no bloquea transacciones concurrentes
  • COMMIT por iteración — no una transacción gigante
3
20260620_003_index_and_subscription_tier
DDL: Índice concurrente en email + columna subscription_tier
DDL CONCURRENTLY
Dos operaciones críticas combinadas en una migración: (1) índice no-bloqueante en email con CONCURRENTLY, y (2) nueva columna subscription_tier con DEFAULT para evitar reescritura de tabla.
🚨 CREATE INDEX CONCURRENTLY no puede ejecutarse dentro de una transacción. Prisma necesita --create-only con SQL manual para este caso.
 prisma/migrations/20260620_003_index_and_subscription_tier/migration.sql
-- Crear migración vacía para escribir SQL manual:
-- npx prisma migrate dev --create-only --name index_and_subscription_tier

-- ────────────────────────────────────────────────
-- (1) Índice en email — NO bloqueante (CONCURRENTLY)
-- ATENCIÓN: ejecutar FUERA de bloque de transacción
-- ────────────────────────────────────────────────
CREATE INDEX CONCURRENTLY IF NOT EXISTS
  idx_users_email
ON users (email);

-- ────────────────────────────────────────────────
-- (2) Columna subscription_tier con DEFAULT
-- PostgreSQL 11+: instant, no rewrite, no lock
-- ────────────────────────────────────────────────
CREATE TYPE subscription_tier_enum AS ENUM (
  'free', 'pro', 'enterprise'
);

ALTER TABLE users
  ADD COLUMN subscription_tier subscription_tier_enum
  NOT NULL DEFAULT 'free';

-- BAD (evitar):
-- ADD COLUMN subscription_tier TEXT NOT NULL  ← lock + full table rewrite
-- ADD COLUMN subscription_tier TEXT NOT NULL DEFAULT 'free'  ← OK en PG11+
--   pero ENUM es más seguro para validación.

-- +migrate Down
DROP INDEX CONCURRENTLY IF EXISTS idx_users_email;
ALTER TABLE users DROP COLUMN IF EXISTS subscription_tier;
DROP TYPE IF EXISTS subscription_tier_enum;
  • CONCURRENTLY — escrituras no bloqueadas durante build del índice
  • NOT NULL con DEFAULT — PostgreSQL 11+ no reescribe la tabla
  • ENUM en lugar de TEXT — validación a nivel de BD, sin coste extra
  • DOWN migration incluida
4
20260621_004_normalize_emails_and_drop_username
CONTRACT: Normalizar emails + eliminar columna username
DML + DDL Irreversible
Migración final del sprint. Requiere que la app esté desplegada sin referencias a username. Normaliza 340k emails con mayúsculas y elimina la columna antigua.
⚠️ PREREQUISITO: Verificar que ningún servicio lee o escribe users.username antes de ejecutar esta migración. Revisar logs de aplicación durante 24h previas.
 prisma/migrations/20260621_004_normalize_emails_and_drop_username/migration.sql
-- ────────────────────────────────────────────────
-- (1) Normalizar emails a lowercase en batches
--     340k filas con mayúsculas — aprox. 35 lotes de 10k
-- ────────────────────────────────────────────────
DO $$
DECLARE
  batch_size  INT := 10000;
  rows_updated INT;
BEGIN
  LOOP
    UPDATE users
    SET    email = LOWER(email)
    WHERE  id IN (
      SELECT id FROM users
      WHERE  email != LOWER(email)
      LIMIT  batch_size
      FOR UPDATE SKIP LOCKED
    );
    GET DIAGNOSTICS rows_updated = ROW_COUNT;
    RAISE NOTICE 'Normalización: % emails actualizados', rows_updated;
    EXIT WHEN rows_updated = 0;
    COMMIT;
  END LOOP;
END $$;

-- Verificar: debe ser 0 antes de continuar
SELECT COUNT(*) FROM users WHERE email != LOWER(email);

-- ────────────────────────────────────────────────
-- (2) CONTRACT: eliminar columna username
--     Solo ejecutar cuando app v2 esté 100% desplegada
--     y sin referencias al campo antiguo
-- ────────────────────────────────────────────────
ALTER TABLE users
  DROP COLUMN IF EXISTS username;

-- IRREVERSIBLE: no hay DOWN migration para el DROP COLUMN.
-- Si se necesita rollback: crear nueva migración que añada la columna
-- y ejecute backfill desde display_name.
  • DML de normalización en batches (no lock global)
  • DROP COLUMN solo tras verificar que no hay referencias en código
  • !Marcada como IRREVERSIBLE — documentado explícitamente
  • Plan de rollback documentado (nueva migración forward)
🔄 Patrón Expand-Contract (renombrado username → display_name)
Fase 1 — Expand
✦ Migración 001: ADD COLUMN display_name
✦ Deploy app que escribe en AMBAS columnas
✦ username = display_name en cada write
✦ Lee desde username (backward compat)
Fase 2 — Migrate
✦ Migración 002: Backfill de filas existentes
✦ Deploy app v2: lee desde display_name
✦ Sigue escribiendo en ambas columnas
✦ Verificar consistencia en producción
Fase 3 — Contract
✦ Deploy app v3: solo usa display_name
✦ Verificar 24h sin lecturas a username
✦ Migración 004: DROP COLUMN username
✦ Schema limpio, migración completada
✓ Este patrón garantiza rollback en cualquier punto hasta la fase 3. Si algo falla en Fase 2, simplemente se revierte el deploy de la app — la BD no cambia.
⛔ Anti-patrones detectados en el brief (evitados)
Anti-patrón Por qué falla en NutriTrack Solución aplicada
ALTER TABLE users RENAME COLUMN username TO display_name Bloqueo momentáneo + rompe la app si hay dos versiones desplegadas simultáneamente (blue-green) Expand-contract en 3 fases durante 7 días
ADD COLUMN subscription_tier TEXT NOT NULL En PG < 11 reescribe 2.3M filas con lock. En PG 11+ solo es seguro CON default explícito. NOT NULL DEFAULT 'free' — metadata-only en PG 11+
CREATE INDEX idx_users_email ON users (email) Bloquea escrituras en la tabla users durante la construcción del índice (minutos en 2.3M filas) CREATE INDEX CONCURRENTLY — escrituras no bloqueadas
UPDATE users SET email = LOWER(email) (una sola query) Transacción de 340k filas, lock prolongado, timeout probable en producción Batches de 10k con SKIP LOCKED + COMMIT por iteración
DDL + DML en una sola migración Dificulta rollback y mezcla tiempos de ejecución muy distintos Migración 001 (DDL) separada de Migración 002 (DML)
📊 Resumen del plan
4
Migraciones ordenadas
7
Días de rollout seguro
0
Segundos de downtime
5
Anti-patrones evitados