PostgreSQL 15 · Prisma · 2.3M rows · Zero Downtime
username → display_name. La nueva columna es nullable — sin lock, sin reescritura de filas.
-- +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;
FOR UPDATE SKIP LOCKED para no bloquear lecturas concurrentes.
-- +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.
CONCURRENTLY, y (2) nueva columna subscription_tier con DEFAULT para evitar reescritura de tabla.
--create-only con SQL manual para este caso.
-- 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;
username. Normaliza 340k emails con mayúsculas y elimina la columna antigua.
users.username antes de ejecutar esta migración. Revisar logs de aplicación durante 24h previas.
-- ──────────────────────────────────────────────── -- (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.
| 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) |