Checklist de formato SQL antes de desplegar una migración
Una lista de verificación práctica para recorrer antes de que una migración de esquema o de datos pase a revisión y a producción. Cada punto es breve, explica por qué importa y la mayoría se puede verificar en un minuto. Al final, un ejemplo completo muestra una migración antes y después del formateo.
La lista de verificación de un vistazo
- El archivo está formateado con las opciones documentadas del equipo.
- El archivo contiene un solo cambio lógico.
- Cada
INSERTlista sus columnas. - Las restricciones y los índices tienen nombres explícitos.
- Los comentarios explican el porqué, no el qué.
- Cada
UPDATEyDELETEtiene una cláusulaWHERErevisada. - Los límites de las transacciones son deliberados.
- La migración se puede volver a ejecutar sin riesgo, o tiene un rollback.
- El revisor leyó el diff formateado, no solo la descripción.
Las migraciones merecen más cuidado que las consultas comunes porque se ejecutan una sola vez, sobre datos de producción, muchas veces con presión de tiempo, y muchos de sus efectos no se pueden deshacer con un simple revert. La legibilidad es parte de la seguridad: un revisor solo puede detectar lo que puede leer.
1. Formato consistente con opciones documentadas
Formatea todas las migraciones con la misma configuración y deja esa configuración por escrito donde el equipo la pueda encontrar, por ejemplo en la guía de contribución del repositorio. En SQL Format eso significa cinco valores: Espacios de sangría, Cantidad máxima de columnas, Comas, Palabras clave y Paréntesis en nueva línea. Cuando todos usan los mismos valores, un diff muestra solo lo que cambió en la lógica.
Formatea antes de hacer commit del archivo, no después de que empiece la revisión. Reformatear un archivo que ya está en revisión entierra los comentarios reales bajo el ruido del diseño. La guía sobre formato, revisión de código y control de versiones describe este flujo de trabajo con más detalle.
2. Un solo cambio lógico por migración
Una migración debe hacer una sola cosa que se pueda describir en una frase: "agregar last_reminded_at a las facturas y completar sus valores". Una columna, el índice que la respalda y el backfill de datos van juntos. Una restricción no relacionada en otra tabla, no. Los archivos pequeños son más fáciles de revisar y de revertir, y cuando algo falla a mitad de camino, el mensaje de error apunta a un cambio fácil de entender.
3. Listas de columnas explícitas en los INSERT
Un INSERT sin lista de columnas depende del orden actual de las columnas de la tabla. Cuando una migración posterior agrega una columna, la sentencia falla; cuando se reordenan columnas del mismo tipo, puede ejecutarse sin error y poner los valores en el lugar equivocado.
-- Breaks, or silently swaps values, when a column is added or reordered
INSERT INTO job_settings VALUES ('dunning', 'min_days_between_reminders', '7');
-- Survives schema changes and documents itself
INSERT INTO job_settings (job_name, setting_key, setting_value)
VALUES ('dunning', 'min_days_between_reminders', '7');Después del formateo, cada columna y cada valor quedan en su propia línea, así que el revisor puede verificarlos en pares.
4. Nombres explícitos para restricciones e índices
Si no le das nombre a una restricción, la base de datos genera uno, y ese nombre generado depende de la base de datos y, a veces, del entorno. Las migraciones posteriores que necesiten eliminarla o modificarla tendrán que buscar ese nombre, y los mensajes de error como "violates check constraint" se vuelven menos útiles.
-- Unnamed: the database invents a name such as invoices_total_check
-- or SYS_C0012345, and it differs between environments
ALTER TABLE invoices ADD CHECK (total >= 0);
-- Named: predictable in error messages, monitoring and later migrations
ALTER TABLE invoices
ADD CONSTRAINT ck_invoices_total_non_negative CHECK (total >= 0);
ALTER TABLE invoice_reminders
ADD CONSTRAINT fk_invoice_reminders_invoice
FOREIGN KEY (invoice_id) REFERENCES invoices (id);Elige un patrón y mantenlo, por ejemplo los prefijos pk_, fk_, uq_, ck_ e ix_ seguidos de la tabla y de las columnas o la regla. Verifica la longitud máxima de identificadores de tu base de datos, que en algunos sistemas es menor de lo que podrías esperar.
5. Comentarios que explican el porqué
El SQL ya dice qué hace la migración. Un comentario al inicio debe decir por qué: el problema que resuelve, el ticket y cómo deshacerla. Dentro del archivo, comenta todo aquello sobre lo que un revisor tendría que preguntar, como por qué un backfill omite algunas filas o por qué se eligió un valor predeterminado. SQL Format conserva los comentarios -- y /* */ en su lugar, así que sobreviven al formateo.
6. Una cláusula WHERE revisada en cada UPDATE y DELETE
Una corrección de datos sin cláusula WHERE modifica todas las filas, y una cláusula WHERE con un OR mal ubicado puede hacer casi lo mismo. Antes del despliegue, ejecuta un SELECT COUNT(*) con exactamente la misma condición y compara el número con lo que espera el ticket:
-- 1. Count what the WHERE clause matches, and compare with the ticket
SELECT COUNT(*) FROM sessions WHERE expires_at < DATE '2026-01-01';
-- 2. Run the change with the same WHERE clause, copied, not retyped
DELETE FROM sessions WHERE expires_at < DATE '2026-01-01';El formateo ayuda aquí: con una condición por línea y los grupos entre paréntesis sangrados, la lógica que mezcla AND/OR es mucho más fácil de verificar. La misma trampa se describe en errores comunes de legibilidad en SQL. En tablas grandes, considera también eliminar o actualizar por lotes para mantener pequeños los bloqueos y los logs de transacciones.
7. Límites de transacción deliberados
Que una migración fallida deje la base de datos a medio cambiar depende de la base de datos:
- PostgreSQL ejecuta la mayor parte del DDL dentro de transacciones, así que una migración envuelta en una transacción se aplica por completo o no se aplica. Algunos comandos, como
CREATE INDEX CONCURRENTLY, no pueden ejecutarse dentro de un bloque de transacción y necesitan su propia migración. - MySQL y MariaDB hacen un commit implícito antes y después de la mayoría de las sentencias DDL. Una migración que falla en su tercera sentencia deja aplicadas las dos primeras, así que planifica el orden y los pasos de recuperación.
- Oracle también hace commit implícito alrededor del DDL, con la misma consecuencia.
Muchas herramientas de migración ya envuelven cada archivo en una transacción en las bases de datos que soportan DDL transaccional. Verifica qué hace la tuya antes de agregar BEGIN y COMMIT a mano, y nunca mezcles en la misma transacción un backfill de datos largo y un DDL que toma bloqueos fuertes sin considerar cuánto tiempo se mantendrán esos bloqueos.
8. Idempotente o reversible
Los despliegues se reintentan. Una migración debe poder ejecutarse dos veces sin riesgo o venir acompañada de una forma probada de volver atrás. Las protecciones como IF NOT EXISTS (donde tu base de datos las soporte) y las cláusulas WHERE que excluyen las filas ya corregidas hacen que una segunda ejecución sea inofensiva:
-- PostgreSQL: safe to re-run
ALTER TABLE invoices ADD COLUMN IF NOT EXISTS last_reminded_at timestamptz;
CREATE INDEX IF NOT EXISTS ix_invoices_last_reminded_at ON invoices (last_reminded_at);
-- Data fix: the WHERE clause makes a second run a no-op
UPDATE invoices
SET status = 'void'
WHERE status = 'draft'
AND created_at < DATE '2025-01-01';Para los cambios que no se pueden volver a ejecutar, escribe el script de rollback al mismo tiempo que la migración y menciónalo en el comentario del encabezado. Eliminar una columna o una tabla solo es reversible si antes se copiaron los datos a otro lugar.
9. Revisa el diff formateado
Por último, lee el diff que va a ver el revisor. Como el archivo se formateó con las opciones acordadas, cada línea modificada debe corresponder a un cambio real. Busca los puntos de arriba: restricciones sin nombre, listas de columnas faltantes, condiciones sin paréntesis y comentarios que ya no coinciden con el código.
Ejemplo completo
Un encabezado de migración que responde las primeras preguntas del revisor antes de que las haga:
-- V42__invoices_last_reminded_at.sql
-- Why: the dunning job resends reminders because it cannot tell when
-- the last one went out. Ticket: BILL-318.
-- Rollback: V42_1__drop_last_reminded_at.sql (drops the index and column).
-- Run by hand with psql; a migration tool may add its own transaction.
BEGIN;
-- formatted statements go here, in the order they must run
COMMIT;Y las sentencias, tal como se escribieron al principio y después de formatearlas con las opciones predeterminadas. Los comentarios quedan donde estaban, ALTER TABLE se queda en una sola línea, CREATE INDEX pasa su cláusula ON a la línea siguiente, la tabla derivada del backfill se sangra dentro de sus paréntesis e INSERT INTO job_settings ( se queda en una sola línea, con las columnas y los valores listados uno por línea debajo. Las palabras de DDL como INDEX y los nombres de tipos como TIMESTAMPTZ siguen la configuración de mayúsculas y minúsculas como cualquier otra palabra clave.
-- V42: track when an invoice was last reminded, so the dunning job stops resending the same reminder
alter table invoices add column last_reminded_at timestamptz;
create index ix_invoices_last_reminded_at on invoices (last_reminded_at);
-- backfill from the reminder log; invoices never reminded stay null
update invoices set last_reminded_at = r.sent_at from (select invoice_id, max(sent_at) as sent_at from invoice_reminders group by invoice_id) r where r.invoice_id = invoices.id and invoices.status = 'open';
insert into job_settings (job_name, setting_key, setting_value) values ('dunning', 'min_days_between_reminders', '7');-- V42: track when an invoice was last reminded, so the dunning job stops resending the same reminder
ALTER TABLE invoices ADD COLUMN last_reminded_at TIMESTAMPTZ;
CREATE INDEX ix_invoices_last_reminded_at
ON invoices(last_reminded_at);
-- backfill from the reminder log; invoices never reminded stay null
UPDATE invoices
SET last_reminded_at = r.sent_at
FROM (
SELECT invoice_id,
MAX(sent_at) AS sent_at
FROM invoice_reminders
GROUP BY invoice_id
) r
WHERE r.invoice_id = invoices.id
AND invoices.status = 'open';
INSERT INTO job_settings (
job_name,
setting_key,
setting_value
)
VALUES (
'dunning',
'min_days_between_reminders',
'7'
);Observa cómo la cláusula WHERE del backfill ahora muestra sus dos condiciones en líneas separadas, y cómo el INSERT deja claro qué valor va en cada columna. Esas son exactamente las líneas en las que un revisor debe invertir su tiempo.
Preguntas frecuentes
¿Debo reformatear migraciones antiguas que ya se aplicaron?
Normalmente no. Muchas herramientas de migración guardan un checksum de cada archivo aplicado y se niegan a ejecutarse, o muestran una advertencia, si cambia. Aplica las reglas de formato solo a las migraciones nuevas.
¿El DDL es transaccional en todas las bases de datos?
No. PostgreSQL puede hacer rollback de la mayor parte del DDL dentro de una transacción, mientras que MySQL, MariaDB y Oracle hacen commit implícito alrededor de las sentencias DDL. Verifica el comportamiento de tu base de datos antes de contar con un rollback.
¿También tengo que darles nombre a los índices, además de a las restricciones?
Sí. Los nombres de los índices aparecen en los planes de ejecución, en el monitoreo y en migraciones posteriores que los eliminan o los reconstruyen, así que un nombre predecible le ahorra tiempo a todos los que los leen.