El Blueprint del Cambio de Esquema
Una migración de esquema parece un problema de escritura SQL, pero el SQL rara vez es donde reside el riesgo.
Busca en todas las páginas de la documentación
Una migración de esquema parece un problema de escritura SQL, pero el SQL rara vez es donde reside el riesgo.
El riesgo reside en el bloqueo: qué bloqueo toma una sentencia, cuánto tiempo espera para adquirir ese bloqueo y qué más en una tabla de producción ocupada se bloquea mientras espera.
ALTER TABLE que se ejecuta instantáneamente en una tabla vacía puede hacer cola detrás de cada otra consulta en una ocupada y luego bloquear a todas ellas a su vez.NOT VALID, CONCURRENTLY, expandir/contraer.La mayoría de las sentencias DDL de PostgreSQL son transaccionales, lo que significa que un CREATE TABLE o ALTER TABLE dentro de BEGIN ... COMMIT se revierte limpiamente si algo más tarde en esa transacción falla.
Esa es una ventaja genuina sobre las bases de datos donde el DDL se confirma automáticamente sentencia por sentencia, ya que una migración fallida de PostgreSQL puede dejar el esquema exactamente como estaba antes del intento.
Un pequeño conjunto de operaciones son la excepción deliberada: CREATE INDEX CONCURRENTLY, VACUUM, y algunas otras no pueden ejecutarse dentro de un bloque de transacciones en absoluto, porque su propósito es evitar mantener el tipo de bloqueo que una sentencia DDL envuelta en una transacción necesitaría.
La otra idea fundamental es la fuerza de bloqueo: cada sentencia DDL adquiere un bloqueo sobre la tabla que toca, y diferentes sentencias necesitan bloqueos de diferente fuerza, desde los que simplemente bloquean otro DDL hasta los que bloquean todas las lecturas y escrituras en la tabla.
Una analogía útil es un puente de un solo carril: un bloqueo ligero es como un estrechamiento temporal del carril por el que el tráfico aún puede pasar lentamente, mientras que un bloqueo pesado es un cierre completo del puente, y el peligro no es el trabajo de construcción en sí, sino cuánto dura el cierre mientras el tráfico se acumula detrás de él.
ALTER TABLE ... ADD COLUMN sin un valor predeterminado, por ejemplo, toma un bloqueo fuerte pero lo mantiene solo brevemente, porque solo actualiza metadatos del catálogo; la misma sentencia con un valor predeterminado volátil históricamente requería reescribir cada fila mientras mantenía ese bloqueo.
El peligro práctico en una migración rara vez es la fuerza del bloqueo por sí sola, sino la fuerza combinada con cuánto tiempo la sentencia espera para adquirirlo.
Una solicitud de bloqueo fuerte se pone en cola detrás de cualquier transacción que ya esté tocando esa tabla, y cada consulta que llega después de la solicitud de bloqueo de la migración también se pone en cola detrás de ella, incluso las lecturas simples que de otro modo se ejecutarían instantáneamente.
Ese efecto de cola es lo que convierte un ALTER TABLE de dos segundos en un incidente de producción: la sentencia en sí es rápida, pero si tiene que esperar diez minutos a que termine una transacción de larga duración, todas las demás consultas en esa tabla esperan con ella.
-- Barato: cambio solo de catálogo, bloqueo fuerte breve
ALTER TABLE orders ADD COLUMN priority int;
-- Más seguro para una tabla ocupada: construye el índice sin bloquear escrituras
CREATE INDEX CONCURRENTLY idx_orders_priority ON orders (priority);
-- Añade una restricción sin un bloqueo inicial de validación de tabla completa
ALTER TABLE orders ADD CONSTRAINT priority_positive
CHECK (priority > 0) NOT VALID;
ALTER TABLE orders VALIDATE CONSTRAINT priority_positive;NOT VALID es el patrón general detrás de la mayoría del trabajo de restricciones de cero tiempo de inactividad: agrega la restricción inmediatamente bajo un bloqueo breve, luego un paso separado VALIDATE CONSTRAINT verifica las filas existentes bajo un bloqueo mucho más ligero que no bloquea escrituras concurrentes.
Establecer lock_timeout en las sesiones de migración convierte una espera indefinida en un fallo rápido y visible en lugar de un atasco de tráfico silencioso, que es exactamente la diferencia entre una migración que falla ruidosamente en un registro de despliegue y una que detiene silenciosamente la producción.
Expandir/contraer es el patrón que generaliza todo esto en un plan de juego repetible para cambios que no se pueden hacer de forma segura en un solo paso: agregue la nueva forma de forma aditiva, migre lecturas y escrituras a ella gradualmente, luego elimine la forma antigua una vez que nada dependa de ella.
Renombrar una columna es el ejemplo más claro, porque un RENAME COLUMN directo rompe todas las instancias de aplicación desplegadas que aún esperan el nombre antiguo en el momento en que se confirma, mientras que una versión de expandir/contraer agrega la nueva columna, la rellena, cambia el tráfico de la aplicación y solo entonces elimina la columna antigua en un despliegue posterior y separado.
A escala, el "relleno" en sí mismo se convierte en un problema adyacente a la migración, porque actualizar cada fila de una tabla grande en una sola sentencia toma el mismo tipo de bloqueo de larga duración que un cambio de esquema, razón por la cual los rellenos grandes se dividen típicamente en lotes pequeños con pausas entre ellos.
Las herramientas de migración (Flyway, Liquibase, o una tabla de historial de esquemas hecha a mano) resuelven un problema genuinamente diferente al de la seguridad del bloqueo: resuelven el orden y la idempotencia, garantizando que cada entorno aplique las mismas sentencias en el mismo orden exactamente una vez, pero no tienen opinión sobre si una sentencia individual es segura de ejecutar contra una tabla en vivo.
Esa distinción es importante porque una herramienta de migración bien ejecutada aún puede aplicar fielmente una sentencia insegura; la corrección de la secuenciación y la seguridad del bloqueo son dos disciplinas separadas que deben ser verdaderas al mismo tiempo.
| Enfoque | Fuerza | Debilidad | Mejor Ajuste |
|---|---|---|---|
| DDL directo en una transacción | Más simple de escribir y revisar | Bloqueos de larga duración en tablas ocupadas; sin implementación parcial | Tablas pequeñas, tablas de bajo tráfico, ventanas de mantenimiento |
NOT VALID + VALIDATE separado | Agrega restricciones sin un bloqueo de validación de tabla completa | Proceso de dos pasos; la restricción no se aplica entre pasos | Agregar restricciones a tablas grandes y escritas activamente |
| Expandir/contraer completo entre lanzamientos | Genuinamente sin tiempo de inactividad; cada paso es independiente y seguro | Más coordinación, más despliegues, línea de tiempo total más larga | Renombres, cambios de tipo y cualquier cambio que rompa una tabla en vivo |
CREATE INDEX CONCURRENTLY y VACUUM, no pueden ejecutarse dentro de un bloque de transacciones en absoluto y deben emitirse por separado.ADD CONSTRAINT ... NOT VALID seguido de un VALIDATE CONSTRAINT separado divide eso en un bloqueo breve más un pase de validación no bloqueante.La mayor parte lo es, lo que significa que una transacción fallida revierte los cambios de esquema limpiamente, pero un pequeño conjunto de operaciones —entre ellas CREATE INDEX CONCURRENTLY y VACUUM— no pueden ejecutarse dentro de un bloque de transacciones en absoluto.
Porque el tiempo de ejecución de la sentencia en sí rara vez es el problema; el tiempo que pasa en cola esperando adquirir su bloqueo, y cada otra consulta que se pone en cola detrás de ella, es lo que convierte milisegundos en minutos.
Agrega la restricción inmediatamente bajo un bloqueo breve sin escanear las filas existentes, posponiendo la validación de tabla completa a un paso VALIDATE CONSTRAINT separado que toma un bloqueo mucho más ligero.
Es dividir un cambio de esquema arriesgado en un paso aditivo (expandir), un período de migración/corte y un paso de limpieza (contraer), de modo que ningún despliegue individual agregue y elimine algo de lo que dependen las aplicaciones.
Porque un renombre directo rompe cualquier instancia de aplicación ya desplegada que todavía use el nombre antiguo en el instante en que se confirma, sin una ventana para una implementación gradual.
No, garantiza que tus migraciones se ejecuten en el orden correcto exactamente una vez en todos los entornos, pero no tiene conocimiento de si el comportamiento de bloqueo de una sentencia individual es seguro para una tabla en vivo.
Sin él, una sentencia que espera un bloqueo puede esperar indefinidamente y poner en cola silenciosamente todas las demás consultas detrás de ella; lock_timeout convierte eso en un fallo rápido y visible.
Es la opción correcta siempre que la tabla reciba escrituras en vivo, ya que evita bloquearlas, pero es más lento y no se puede ejecutar dentro de un bloque de transacciones, por lo que es una sobrecarga innecesaria en tablas que no están bajo carga de escritura activa.
Un relleno que actualiza cada fila en una sola sentencia toma un bloqueo de larga duración de manera similar a un cambio de esquema, razón por la cual los rellenos grandes se dividen típicamente en lotes pequeños y pausables en lugar de una única y gigantesca UPDATE.
La fuerza del bloqueo determina qué otra actividad bloquea el bloqueo una vez adquirido; el tiempo de espera determina cuánto tiempo la sentencia se pone en cola antes de adquirirlo: un bloqueo fuerte mantenido brevemente puede ser más seguro que un bloqueo débil que espera detrás de una transacción larga.
Para tablas pequeñas o de bajo tráfico, o durante una ventana de mantenimiento real, donde la simplicidad de un cambio directo supera el costo de coordinación de una implementación completa de expandir/contraer.
Porque una migración que es segura hacia adelante no es automáticamente segura para revertir; los equipos que solo avanzan tratan un despliegue fallido como una nueva migración hacia adelante en lugar de asumir que existe una reversión simétrica.
CONCURRENTLY, NOT VALID y cortes por fases en detallepg_repackVersiones de Stack: Esta página es conceptual y describe el comportamiento de bloqueo DDL y transacciones consistente en las versiones principales actuales de PostgreSQL, incluyendo PostgreSQL 18.4 (estable 18, línea de mantenimiento 17).
Revisado por Chris St. John·Última actualización: 15 jul 2026