8 min

Cambios de esquema sin tiempo de inactividad con el patrón expandir/contraer

Planifica y publica cambios de esquema sin tiempo de inactividad con el patrón expandir/contraer, rellenos seguros, versiones compatibles, verificación y reversión.

Cambios de esquema sin tiempo de inactividad con el patrón expandir/contraer

Por qué los cambios de esquema causan interrupciones

Los cambios de esquema causan interrupciones cuando las versiones de la aplicación, los workers en segundo plano y la base de datos dejan de coincidir sobre qué estructuras y valores son válidos. El fallo puede ser evidente, por ejemplo, que todas las solicitudes devuelvan un error, o gradual, como un aumento de la latencia de consultas, escrituras fallidas, retraso de réplicas y una cola de trabajos que habrá que repetir.

Un despliegue en producción rara vez cambia todos los procesos a la vez. Los lanzamientos graduales dejan instancias antiguas y nuevas de la aplicación ejecutándose juntas. Los workers de larga duración pueden conservar una compilación anterior durante horas, los clientes móviles pueden permanecer activos durante meses, y los trabajos de informes o integración pueden usar tablas sin pasar por la aplicación principal. Todos comparten una misma base de datos.

Los fallos habituales incluyen:

  • El código nuevo escribe en una columna antes de que termine la migración que la crea.
  • El código antiguo lee una tabla o columna que una versión posterior renombró o eliminó.
  • Una reescritura de tabla, un relleno de datos o la creación de un índice consume suficiente E/S y CPU como para ralentizar el tráfico normal.
  • Un comando de esquema espera un bloqueo mientras las solicitudes se acumulan detrás de él.
  • Una restricción nueva rechaza escrituras de un proceso que aún no se ha actualizado.

La parte peligrosa suele ser adquirir el bloqueo, no el tiempo nominal de ejecución. Un ALTER TABLE rápido puede esperar detrás de una transacción larga. Mientras espera, las consultas posteriores pueden ponerse en cola detrás del bloqueo de esquema pendiente y convertir una migración pequeña en una parada para toda la aplicación.

Cero tiempo de inactividad exige que cada estado intermedio de la base de datos siga siendo utilizable por todas las versiones de la aplicación que aún puedan ejecutarse. Añade primero estructuras compatibles, mueve el tráfico y los datos de forma controlada, y elimina la ruta antigua solo cuando haya desaparecido su último consumidor.

Este trabajo está justificado en sistemas con tráfico activo, despliegues graduales, objetivos estrictos de disponibilidad o procedimientos de recuperación costosos. Una pequeña herramienta interna con una base de datos poco activa puede beneficiarse más de una ventana de mantenimiento probada. La decisión debe reflejar el coste de una interrupción y la complejidad operativa de la migración.

Expandir/contraer en palabras sencillas

El patrón expandir/contraer convierte un cambio incompatible en una secuencia de versiones compatibles. La base de datos admite temporalmente dos representaciones mientras el código y los datos pasan de la anterior a la nueva.

La secuencia tiene tres partes:

  • Expansión: añadir columnas, tablas, índices o restricciones sin eliminar nada que necesite el código actual.
  • Transición: desplegar código compatible, mover datos históricos y dirigir las lecturas y escrituras a la nueva representación.
  • Contracción: eliminar el código antiguo y los objetos de base de datos después de verificar que no se usan.

Supongamos que una tabla de PostgreSQL almacena el nombre de una persona en full_name, y la aplicación necesita los campos separados first_name y last_name. La expansión añade columnas que admiten NULL y conserva full_name. Una versión compatible escribe las representaciones necesarias durante la transición. Un relleno separa los valores existentes, con una política explícita para los nombres que no puedan dividirse de forma fiable. Las lecturas se trasladan solo cuando los nuevos campos estén suficientemente completos. Más adelante, la contracción elimina full_name.

Este orden encaja con los despliegues graduales porque la compilación antigua sigue encontrando full_name y la nueva encuentra las tres columnas. También conserva una vía para revertir la aplicación. Si la nueva versión se comporta mal, puede ejecutarse la compilación anterior porque no se han eliminado sus dependencias de esquema.

Revertir la base de datos es distinto de revertir la aplicación. Invertir una migración después de transformar datos puede descartar información o restaurar un valor obsoleto. Durante la transición, es preferible devolver el tráfico de la aplicación a la representación conocida y mantener los objetos aditivos de base de datos. Corrige la migración hacia adelante cuando el incidente esté estable.

El patrón no implica que todos los cambios necesiten escritura dual. Añadir una columna opcional que solo usa código nuevo puede requerir una migración aditiva y un despliegue. Los cambios de nombre, de representación, las divisiones de tablas y los cambios en campos obligatorios suelen necesitar más fases porque dos versiones de la aplicación no pueden compartir el esquema de forma segura de otro modo.

Clasifica el cambio antes de elegir los pasos

El plan de migración debe ajustarse a los riesgos reales de bloqueo, reescritura, compatibilidad y conversión de datos de la operación. Tratar cada ALTER TABLE como equivalente genera ceremonias innecesarias o lanzamientos inseguros.

Los cambios aditivos suelen ser los más sencillos. Una columna que admite NULL, una tabla independiente o un índice creado con un método en línea pueden introducirse antes de que el código de la aplicación los use. El comando sigue necesitando un bloqueo, así que prueba su comportamiento con una tabla y una carga de transacciones parecidas a las de producción.

Los cambios destructivos incluyen eliminar o renombrar columnas, restringir tipos, sustituir tablas y añadir restricciones más estrictas. Estos cambios invalidan una suposición del código existente. Colócalos en la fase de contracción, después de eliminar las referencias de código y los consumidores externos.

Las operaciones que cambian datos merecen una evaluación propia. Convertir marcas de tiempo, normalizar números de teléfono, combinar registros o dividir texto libre puede perder información. Define cómo se tratarán los valores no válidos y ambiguos antes de iniciar el relleno. Si una transformación no puede invertirse, conserva la fuente hasta que el resultado supere las comprobaciones de negocio.

Una revisión previa útil cubre cinco preguntas:

  • ¿Qué bloqueo solicita cada sentencia y cuánto tiempo puede esperar o mantenerlo?
  • ¿La operación reescribirá la tabla, generará mucho WAL o aumentará el retraso de las réplicas?
  • ¿Qué aplicaciones, trabajos, informes y consumidores de captura de cambios usan los objetos afectados?
  • ¿Pueden ejecutarse las versiones actuales y propuestas con cada estado transitorio?
  • ¿Qué señal pausa la operación y qué estado exacto queda después de detenerla?

Ejecuta la migración exacta sobre datos con un volumen y una distribución realistas. Una tabla de prueba con mil filas ordenadas dice poco de una tabla de producción con cientos de millones de filas, tuplas anchas, filas muertas, valores sesgados y transacciones de larga duración.

Expande con seguridad en PostgreSQL

Una expansión segura de PostgreSQL usa cambios breves de metadatos, esperas de bloqueo limitadas y operaciones en línea independientes cuando la base de datos lo exige. Añade la nueva estructura antes de desplegar código que dependa de ella.

Añadir una columna que admite NULL y no tiene valor predeterminado suele ser una operación breve de metadatos:

BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

El tiempo de espera evita que la versión espere indefinidamente detrás de una transacción abierta. Si el bloqueo no puede adquirirse pronto, deja que la migración falle, inspecciona qué la bloquea y vuelve a intentarlo en un momento más seguro. No reintentes automáticamente en un bucle cerrado porque las solicitudes repetidas de bloqueo pueden seguir alterando el tráfico de producción.

Las versiones modernas de PostgreSQL pueden añadir una columna con un valor predeterminado constante sin escribir de inmediato ese valor en cada fila existente. Esa optimización no hace que todos los valores predeterminados sean inocuos. Una expresión volátil puede requerir una reescritura, y ALTER TABLE sigue necesitando un breve bloqueo ACCESS EXCLUSIVE. Confirma el comportamiento de la versión de PostgreSQL desplegada y de la expresión exacta, en vez de confiar en una regla general.

Un CREATE INDEX normal puede bloquear las escrituras. Usa la creación concurrente si la tabla debe seguir admitiendo escrituras:

CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);

CREATE INDEX CONCURRENTLY no puede ejecutarse dentro de un bloque de transacción. Tarda más, realiza trabajo adicional y puede esperar transacciones antiguas, pero las inserciones, actualizaciones y eliminaciones normales pueden continuar. Sigue consumiendo CPU, E/S y WAL, así que supervisa la latencia de la base de datos y las réplicas mientras se ejecuta.

Una creación concurrente fallida puede dejar un índice no válido. Inspecciona el estado del índice antes de volver a intentarlo y después elimina o reconstruye el objeto no válido deliberadamente. Las herramientas de migración que envuelven cada archivo en una transacción necesitan un modo no transaccional compatible para las operaciones de índices concurrentes.

Las tablas nuevas suelen ser más fáciles de introducir que las transformaciones en el mismo lugar. Para una relación de uno a muchos o de muchos a muchos, añade la tabla de destino y sus índices mientras conservas la columna de origen. Retrasa la eliminación de la fuente hasta que se hayan trasladado las escrituras nuevas, los datos históricos, las lecturas y los consumidores posteriores.

Los cambios de tipo exigen especial cuidado. Algunos solo cambian metadatos, mientras que otros reescriben todas las filas o adquieren un bloqueo restrictivo durante demasiado tiempo. Para una conversión arriesgada, añade una columna con el tipo de destino, rellénala por lotes, cambia el acceso de la aplicación y elimina la original más tarde. Así el equipo también tiene dónde registrar los fallos de conversión en vez de hacer que un gran ALTER COLUMN TYPE tenga que completarse o fallar como una unidad.

Despliega código que siga siendo compatible

El código compatible de la aplicación tolera valores transitorios ausentes y nunca requiere una migración destructiva durante el mismo despliegue. La expansión de la base de datos debe terminar antes de que la primera instancia de la aplicación empiece a usar el objeto nuevo.

La escritura dual sirve cuando ambas representaciones deben mantenerse actualizadas. Realiza las dos escrituras en la misma transacción de base de datos siempre que sea posible. Una segunda escritura asíncrona puede fallar después de que tenga éxito la primera y crear una divergencia que las lecturas posteriores puedan mostrar.

La lógica de escritura dual también necesita una única autoridad. Si phone_e164 se deriva de phone, define qué entrada prevalece cuando se proporcionan ambas y aplica la misma normalización en controladores de API, workers, importaciones y herramientas administrativas. De lo contrario, dos rutas de código que parezcan correctas pueden guardar resultados distintos.

Las lecturas deben trasladarse después de las escrituras. Mantén las lecturas en el campo consolidado mientras las nuevas escrituras rellenan ambas formas y el relleno gestiona las filas históricas. Tras la verificación, despliega una ruta de lectura que prefiera el nuevo campo y utilice el valor antiguo solo según una regla de respaldo definida. Mide el uso del respaldo. Un respaldo silencioso puede ocultar para siempre datos incompletos.

Una secuencia de versiones habitual es:

  • La versión 1 añade los nuevos objetos de base de datos sin cambiar el comportamiento de la aplicación.
  • La versión 2 escribe las representaciones transitorias y mantiene las lecturas consolidadas.
  • La versión 3 cambia las lecturas después de que superen el relleno y las comprobaciones de coherencia.
  • La versión 4 deja de mantener la representación antigua cuando expiran los criterios de reversión.
  • La versión 5 elimina las referencias de código antiguo, seguida más tarde por la limpieza de la base de datos.

Mantén separados los contratos de API públicos y los cambios de esquema físico. Renombrar una columna de base de datos no obliga a renombrar de inmediato un campo en respuestas web, móviles o de integración. Cambia esos contratos siguiendo su propia política de compatibilidad, especialmente cuando los clientes no pueden actualizarse junto con el servidor.

Haz inventario de todos los escritores. Los controladores HTTP son solo una fuente de cambios. Los consumidores de colas, trabajos programados, scripts de importación, herramientas de reparación de datos, triggers de base de datos y operaciones administrativas directas pueden seguir produciendo filas con la forma antigua. Cuando sea práctico, etiqueta las conexiones de base de datos con un nombre de aplicación y registra el uso de las rutas transitorias para que se vea cualquier proceso pasado por alto.

Los procesos de larga duración pueden conservar suposiciones obsoletas mediante sentencias preparadas, metadatos en caché o una capa de mapeo objeto-relacional. Prueba los reinicios graduales y el comportamiento de los pools de conexiones antes de la contracción. Un proceso que no ha recibido tráfico recientemente puede seguir fallando la primera vez que se ejecute un trabajo poco frecuente.

Rellena datos sin sobrecargar la base de datos

Rellena datos en lotes pequeños
Crea un trabajo sencillo de relleno de datos y ajusta los lotes sin frenar a tu equipo.

Un relleno seguro actualiza lotes pequeños que se pueden reanudar y reduce la velocidad cuando empeora el estado de producción. Empieza solo después de que los escritores activos puedan mantener la nueva representación.

Elige los lotes según el tiempo transcurrido y el impacto en la base de datos, no por un número universal de filas. Mil filas estrechas pueden terminar en milisegundos, mientras que mil filas con valores grandes o transformaciones costosas pueden producir E/S significativa. Empieza con prudencia y busca transacciones que terminen en segundos. Confirma entre lotes para que los bloqueos y las versiones antiguas de filas no se acumulen en una sola transacción.

PostgreSQL no permite usar ORDER BY y LIMIT directamente en un UPDATE simple. Selecciona un lote en una expresión de tabla común y actualiza después esas filas:

WITH batch AS (
    SELECT id
    FROM my_table
    WHERE id > $1
      AND new_col IS NULL
    ORDER BY id
    LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
  AND target.new_col IS NULL
RETURNING target.id;

La aplicación registra el mayor id completado como cursor. La actualización condicional hace que las repeticiones sean idempotentes, por lo que un fallo después de confirmar no corrompe filas ya procesadas. Guarda el progreso con suficiente cuidado para que el cursor no avance más allá de un lote sin confirmar.

Un cursor de id creciente evita explorar repetidamente el inicio de la tabla, pero no detecta correcciones tardías ni filas insertadas por debajo del cursor. Termina con una pasada de recuperación sobre todos los valores NULL restantes. Si los identificadores no están ordenados o las filas pueden cambiar entre estados de elegibilidad, usa una tabla de trabajo u otro punto de control explícito en vez de asumir que un único recorrido hacia adelante basta.

Varios workers pueden reclamar filas con FOR UPDATE SKIP LOCKED, pero el paralelismo aumenta la presión de escritura y complica el seguimiento del progreso. No combines filas omitidas con un cursor que avance permanentemente más allá de ellas. Una cola de identificadores reclamados o exploraciones repetidas de elegibilidad son más seguras para workers paralelos.

Limita la velocidad con mediciones de producción como la latencia de consultas, conexiones activas, esperas de bloqueo, generación de WAL, retraso de reproducción de réplicas y crecimiento de filas muertas. Pausa al superar un umbral y reanuda desde el punto de control. Las pausas fijas son sencillas, pero la retroalimentación de la base de datos responde mejor a los cambios de tráfico.

Evita cambiar todas las filas cuando solo algunas necesitan trabajo. Filtra por el campo nuevo, el estado de origen o una marca de migración. Si la transformación es costosa, calcúlala fuera de la transacción de actualización cuando la coherencia lo permita y después realiza una escritura condicional breve. Conserva un recuento y una muestra de los valores rechazados en vez de inventar datos silenciosamente.

Autovacuum y las réplicas deben absorber el trabajo después de cada actualización. Un relleno puede terminar correctamente en el primario mientras las réplicas se retrasan mucho o el crecimiento de la tabla degrada consultas posteriores. Los límites de velocidad deben tener en cuenta ese coste retrasado, no solo el tiempo de ejecución inmediato del lote.

Verifica los datos y el tráfico de producción

Una migración está lista para la contracción solo cuando las comprobaciones de datos, la telemetría de la aplicación y la evidencia de dependencias coinciden en que la nueva ruta es la autoridad. Un contador de trabajos completados por sí solo no demuestra que sea correcta.

Empieza por la completitud y la coherencia. IS DISTINCT FROM de PostgreSQL compara valores y maneja NULL de forma explícita, a diferencia de <>, que produce un resultado desconocido cuando cualquiera de los lados es NULL:

SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;

No ejecutes repetidamente un recuento de tabla completa sin índice en una tabla muy grande y ocupada. Usa una validación controlada única, rangos de identificadores acotados, muestras o un proceso de verificación temporal que avance por la tabla. El método adecuado depende del coste de equivocarse y del margen disponible en la base de datos.

La verificación debe cubrir:

  • No quedan valores ausentes inesperados en filas que requieran el nuevo campo.
  • El nuevo valor coincide con la transformación acordada, incluso con entradas malformadas y vacías.
  • Las filas y actualizaciones nuevas siguen siendo coherentes después de terminar la pasada histórica.
  • El uso de lecturas de respaldo ha alcanzado el umbral previsto, normalmente cero para tráfico controlado por el servidor.
  • Las tasas de errores, la latencia de consultas, los bloqueos y el retraso de réplicas siguen dentro de los límites de la versión.

Compara resultados de negocio además de columnas. Si una migración cambia precios, permisos, estado de cuentas o identificadores, valida los totales y las invariantes de las que dependen los usuarios. Dos columnas pueden coincidir mecánicamente y aun así codificar ambas una regla de negocio incorrecta.

Observa un ciclo operativo completo antes de limpiar. El intervalo correcto se basa en el comportamiento real del sistema, no en una regla fija de una semana. Puede necesitar incluir el procesamiento de fin de mes, un trabajo de facturación poco frecuente, reintentos tardíos de cola o la vida útil máxima de un cliente móvil antiguo. Registra la evidencia de que cada consumidor se ha trasladado.

Haz un canario del cambio de lecturas cuando la arquitectura de la aplicación lo permita. Envía una pequeña parte del tráfico a la nueva ruta de lectura, compara los resultados y amplíalo gradualmente. Mantén sencilla la acción de reversión: redirige las lecturas a la representación consolidada sin invertir el relleno.

Añade restricciones cuando los datos estén listos

Las restricciones deben hacerse estrictas solo después de que todos los escritores cumplan y los datos existentes se hayan validado. Aplicar NOT NULL, una comprobación o una clave foránea durante la expansión puede bloquear tráfico o rechazar escrituras de un proceso antiguo.

PostgreSQL puede añadir una restricción CHECK como NOT VALID, que aplica la regla a las filas nuevas o modificadas sin explorar de inmediato todas las filas históricas. Valídala por separado después del relleno:

ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;

ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;

Cuando la validación tiene éxito, las versiones compatibles de PostgreSQL pueden usar esa prueba al establecer la columna como NOT NULL y evitar otra exploración completa de la tabla. La modificación final sigue necesitando un bloqueo fuerte de tabla, así que usa un tiempo de espera de bloqueo limitado y un plan de reintento:

ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;

ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;

La comprobación temporal puede mantenerse si aporta valor, pero conservar restricciones equivalentes añade desorden al catálogo sin cambiar la regla.

Las claves foráneas pueden seguir una secuencia parecida con NOT VALID y VALIDATE CONSTRAINT. Las escrituras nuevas se comprueban tras crear la restricción, mientras que la validación histórica se realiza más tarde. Añade intencionadamente el índice de soporte cuando el comportamiento de eliminación o actualización de la relación referenciada pudiera causar exploraciones costosas.

La validación de la aplicación debe preceder a la aplicación de reglas en la base de datos, pero no la sustituye. El código ofrece errores más claros para el usuario, mientras que la base de datos protege los datos escritos por cualquier ruta. Durante el despliegue, vigila las infracciones de restricciones para identificar un escritor que la auditoría de dependencias no detectó.

Contrae la ruta antigua con seguridad

Haz que las migraciones sean repetibles
Prepara un runbook repetible para las fases de expansión, verificación y contracción.

La fase de contracción debe eliminar las dependencias de la aplicación antes de eliminar objetos de base de datos. Cuando la telemetría y la verificación establecen que la nueva ruta es la autoridad, la limpieza puede avanzar mediante versiones independientes.

Primero deja de leer el campo antiguo y elimina la lógica de respaldo. Después desactiva sus escrituras y observa producción el tiempo suficiente para detectar rutas poco frecuentes. Elimina flags de funcionalidad, triggers, vistas de compatibilidad, scripts de reparación y trabajos programados que mencionen la representación antigua. Busca en el código fuente exportado y en el código de migraciones, pero inspecciona también informes, consultas de integración y configuraciones de captura de cambios fuera del repositorio principal.

Un orden de limpieza seguro es:

  • Eliminar las lecturas de respaldo y confirmar que ya no aparecen en la telemetría.
  • Detener las escrituras antiguas y borrar el código de sincronización.
  • Eliminar las referencias de aplicación de todas las versiones desplegables.
  • Eliminar índices y restricciones obsoletos con el método en línea adecuado.
  • Eliminar la columna o tabla antigua en una versión posterior de la base de datos.

Eliminar una columna de PostgreSQL es principalmente un cambio de catálogo, pero sigue necesitando un bloqueo ACCESS EXCLUSIVE. Por ello, una sentencia corta puede esperar detrás de una transacción larga y bloquear trabajo posterior. Aplica un tiempo de espera de bloqueo, inspecciona antes las transacciones de larga duración y programa el intento en un periodo de menor riesgo.

Usa DROP INDEX CONCURRENTLY para un índice obsoleto cuando bloquear escrituras sea inaceptable. Como la creación concurrente, no puede ejecutarse dentro de un bloque de transacción y tiene restricciones que las herramientas de migración deben gestionar.

No combines la limpieza de código y la eliminación física en una misma versión. La separación permite que la aplicación limpia se ejecute contra una base de datos que aún contiene el objeto sin usar. Si aparece un problema en la aplicación, sigue siendo posible revertir sin recrear el esquema ni reconstruir datos.

Antes de eliminar una tabla, comprueba la propiedad de secuencias, vistas, funciones, permisos, triggers, publicaciones de replicación y consultas externas. Evita usar CASCADE como atajo en una migración de producción porque puede eliminar dependencias que no formaban parte del cambio previsto.

Gestiona las reversiones y los pasos fallidos

La planificación de reversiones debe definir una acción segura para cada fase en vez de depender de una única migración descendente genérica. Los objetos aditivos, el movimiento de datos, los cambios de lectura y la eliminación tienen propiedades de recuperación distintas.

Si la expansión no consigue adquirir el bloqueo, deja la aplicación sin cambios y vuelve a intentarlo después de resolver la transacción que lo bloquea. Si falla una creación concurrente de índice, inspecciona si dejó un índice no válido y limpia ese objeto concreto antes de otro intento.

Si un relleno genera carga, páusalo. Los lotes idempotentes ya confirmados pueden quedarse donde están. Reduce el tamaño o la velocidad de los lotes, corrige la transformación costosa y reanuda desde el punto de control. Revertir millones de actualizaciones correctas suele añadir riesgo sin ayudar a que producción se recupere.

Si una nueva ruta de lectura devuelve resultados incorrectos, dirige las lecturas de vuelta a la representación antigua y conserva los datos nuevos para el diagnóstico. Mantén la escritura dual solo si se sabe que es correcta. Cuando el propio escritor falla, desactívalo o revierte la aplicación antes de reparar las filas afectadas.

Después de la contracción, la recuperación puede requerir restaurar datos en vez de simplemente desplegar una compilación anterior. Define explícitamente el punto de no retorno. Toma la copia de seguridad o snapshot que requiera la política de recuperación del sistema, prueba la restauración antes de la versión y conserva el objeto antiguo durante el intervalo de retención acordado cuando el coste de almacenamiento lo permita.

Los comandos de esquema pueden ser transaccionales, pero los efectos externos no siempre quedan cubiertos. Las operaciones de índices concurrentes, los mensajes de cola, los cambios de caché y los despliegues de aplicaciones no comparten una única transacción atómica. El runbook debe describir el estado observable tras cada fallo parcial y el comando que permite continuar desde él con seguridad.

Evita trampas habituales de migración

La mayoría de migraciones sin tiempo de inactividad que fallan aplican el nuevo estado demasiado pronto u olvidan un consumidor del estado antiguo. Las siguientes trampas merecen una revisión explícita antes de aprobarlas.

  • Añadir NOT NULL mientras una instancia antigua de la aplicación todavía puede omitir el campo.
  • Ejecutar un gran relleno en una transacción, reteniendo bloqueos y versiones de filas demasiado tiempo.
  • Renombrar una columna como si fuera aditivo aunque el código antiguo siga usando su nombre original.
  • Cambiar las lecturas antes de que todas las rutas de escritura y filas históricas rellenen la nueva representación.
  • Tratar un despliegue correcto como prueba de que informes, workers, réplicas e integraciones son compatibles.

Otro fallo sutil procede de la sincronización bidireccional. Un trigger copia old_col a new_col, mientras que el código de la aplicación copia new_col de vuelta a old_col. Las diferencias de normalización o de orden de triggers pueden crear bucles, sobrescribir valores intencionados o dejar poco clara la propiedad. Prefiere una sola dirección y documenta qué representación es la autoridad en cada versión.

Los valores predeterminados pueden ocultar actualizaciones ausentes de los escritores. Si una nueva columna obligatoria recibe un valor predeterminado vacío o genérico, el código antiguo parece compatible mientras guarda datos semánticamente no válidos. Usa una transición que admita NULL cuando la ausencia aporte información útil para el diagnóstico y aplica la regla real después de que cada escritor proporcione un valor significativo.

Un flag de funcionalidad no vuelve seguro por sí solo un comando de esquema incompatible. Una ruta de código desactivada puede seguir cargada, preparada o ejecutarse en un proceso antiguo. El objeto de base de datos debe permanecer hasta que ninguna versión activa o desplegable haga referencia a él.

La propiedad de la migración también importa. Asigna a una persona o equipo la transición hasta la contracción, incluidas las fechas de verificación y eliminación. De lo contrario, columnas temporales, flags y trabajos de sincronización pueden permanecer durante meses y aumentar el coste de cada cambio posterior.

Sustituye una columna de teléfono sin interrupciones

Exporta el código generado
Conserva el control total exportando el código fuente cuando tengas listo el flujo de migración.

Sustituir customers.phone por customers.phone_e164 normalizado requiere una columna aditiva, una política de conversión definida, código compatible, un relleno limitado, un cambio de lecturas y una limpieza retrasada. La política de conversión debe ir antes que SQL porque no todos los valores almacenados pueden normalizarse automáticamente.

Empieza clasificando los valores existentes. Los números válidos pueden convertirse cuando se conoce el contexto de país necesario. Los valores vacíos pueden convertirse en NULL. Los números ambiguos o malformados deben entrar en un informe de excepciones en vez de adivinarse. Decide si el producto exige que cada cliente tenga un número de teléfono, ya que eso determina si NOT NULL será apropiado más adelante.

Añade la columna con un tiempo de espera de bloqueo corto:

BEGIN;
SET LOCAL lock_timeout = '2s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

Despliega código que normalice las entradas nuevas y escriba phone y phone_e164 en una transacción. Al principio, mantén las lecturas en phone. Actualiza todos los escritores, incluidas las importaciones de cuentas, herramientas de soporte, trabajos de workers y pruebas que crean datos de clientes.

Rellena las filas elegibles en transacciones cortas. Registra el último identificador procesado, el número convertido, el número omitido y el motivo de cada categoría de fallo. Limita la velocidad del trabajo según la latencia de producción y el retraso de réplicas. Cuando termine la pasada hacia adelante, vuelve a explorar los valores NULL elegibles para detectar inserciones simultáneas o filas omitidas tras un reinicio.

Ejecuta comprobaciones de coherencia con las mismas reglas de normalización que la aplicación y después toma muestras manuales de prefijos internacionales, extensiones, valores vacíos, registros de contacto duplicados y datos importados antiguos. Un recuento de filas demuestra cobertura, no que el número de teléfono sea correcto.

Despliega una ruta de lectura que devuelva phone_e164 cuando esté presente y use phone solo para una excepción registrada. Supervisa el uso del respaldo y los errores de normalización. Resuelve las excepciones restantes en vez de permitir que el respaldo se convierta en comportamiento permanente.

Cuando el nuevo campo sea la autoridad, elimina el respaldo y deja de escribir phone. Observa los trabajos poco frecuentes y el tráfico de integración durante un ciclo operativo adecuado. Añade la restricción validada solo si la regla de producto lo exige.

Por último, elimina las referencias de código a phone. Elimina sus índices o restricciones por separado y después elimina la columna en una migración posterior con una espera de bloqueo limitada. Si el cambio de lectura falla en cualquier momento antes de esa eliminación, revierte el comportamiento de la aplicación mientras ambas columnas sigan disponibles.

Este ejemplo también deja ver un problema de dominio que la mecánica del esquema no puede resolver: dividir o normalizar datos introducidos por personas no siempre evita pérdidas. El plan de migración debe conservar las excepciones y dar a una persona responsable una forma de resolverlas.

Comprueba cada versión antes de publicarla

Una lista de comprobación de versión debe demostrar compatibilidad, limitar el impacto en producción e indicar la acción de recuperación de la fase actual. Conserva la evidencia junto con el cambio para que un operador no tenga que reconstruir la intención durante un incidente.

Antes del despliegue, confirma:

  • La versión de la aplicación funciona con el estado de la base de datos antes y después de esta versión.
  • Se han configurado tiempos de espera de bloqueo y de sentencia para comandos de esquema que podrían esperar detrás del tráfico.
  • El trabajo de relleno o validación tiene controles de progreso, pausa, reanudación y límite de velocidad.
  • Los paneles cubren errores, latencia, bloqueos, carga de base de datos, WAL y retraso de réplicas.
  • Se ha probado la acción de reversión sin depender de un objeto que ya se eliminó.

Registra condiciones explícitas de finalización. Algunos ejemplos son cero nuevos fallos de coherencia durante un ciclo completo de trabajo, cero lecturas de respaldo de tráfico controlado por el servidor, todos los consumidores conocidos actualizados y una consulta de validación controlada satisfactoria. El porcentaje completado sirve durante un relleno, pero procesar el 100 por ciento no equivale a que el 100 por ciento sea correcto.

Revisa el orden de migración por separado de la revisión de código. Una colección correcta de SQL y cambios de aplicación puede fallar si el despliegue los ejecuta en la secuencia equivocada. Indica qué paso puede comenzar solo después de que haya terminado otro.

Las condiciones de detención deben ser numéricas cuando sea posible. Define latencia de consultas, espera de bloqueo, retraso de réplicas, tasa de errores y duración de lote aceptables. Al cruzar un umbral, el operador debe saber si pausar un trabajo, cancelar una sentencia en espera o redirigir lecturas sin buscar una nueva aprobación durante el incidente.

La migración termina solo cuando la nueva representación gestiona lecturas y escrituras, los datos históricos han superado la verificación, el objeto antiguo se ha eliminado y la maquinaria operativa temporal ha desaparecido.

Haz que el proceso sea repetible

Un runbook de migración reutilizable convierte expandir/contraer en trabajo normal de lanzamiento con responsables definidos y controles medibles. Debe ser lo bastante breve para seguirlo durante un despliegue activo y lo bastante específico para describir estados de fallo parcial.

Usa cinco secciones en el runbook:

  • Expansión: operaciones exactas de esquema, bloqueos esperados, tiempos de espera y requisitos de transacción.
  • Compatibilidad: código afectado, escritores, lectores, flags, clientes y orden de despliegue.
  • Relleno: política de transformación, lotes, puntos de control, limitación de velocidad y gestión de excepciones.
  • Verificación: comprobaciones SQL, invariantes de negocio, telemetría y umbrales de finalización.
  • Contracción: eliminación de dependencias, periodo de observación, limpieza física y límites de recuperación.

Asigna un responsable y una fecha prevista de finalización a cada objeto transitorio. Controla columnas, índices, flags, triggers y trabajos en el mismo lugar. La limpieza forma parte de la migración, no es mantenimiento opcional.

Para los equipos que desarrollan con Koder.ai, el modo Planning ayuda a detallar estas fases y puntos de control antes de empezar los cambios de producción. La exportación del código fuente también permite que el SQL de migración y la lógica de compatibilidad reciban la misma revisión que el resto del código de la aplicación. Koder.ai admite despliegue, alojamiento, snapshots y reversión, pero no debe suponerse que revertir una aplicación invierte una transformación de datos ya confirmada. Conserva la compatibilidad de esquema hasta que el plan de recuperación de la base de datos ya no dependa de la representación antigua.

Programa el trabajo con muchas escrituras en horas de menor tráfico cuando sea posible, pero no uses el horario como único control de seguridad. Las transacciones limitadas, la limitación de velocidad basada en retroalimentación, el progreso observable y una acción de pausa probada son lo que mantiene manejable una migración en línea cuando el tráfico o los datos se comportan de forma distinta a la esperada.

Preguntas frecuentes

¿Por qué un cambio de esquema puede causar una interrupción?

Los cambios de esquema rompen producción cuando las versiones antiguas y nuevas de la aplicación esperan estructuras de base de datos distintas. Durante un despliegue gradual pueden ejecutarse ambas versiones a la vez, por lo que eliminar o renombrar una columna demasiado pronto puede provocar fallos de lectura o escritura.

¿Qué es el patrón de migración expandir/contraer?

Expandir/contraer divide un cambio incompatible en etapas seguras. Primero se añade la nueva estructura, después se trasladan el código y los datos, y por último se elimina la estructura antigua cuando ningún consumidor la utiliza.

¿Cómo renombro o sustituyo una columna de base de datos sin interrupciones?

Añade primero la nueva columna y conserva la anterior. Despliega código que pueda trabajar con ambos campos, rellena las filas existentes en lotes pequeños, cambia las lecturas después de validarlas y elimina la columna antigua en una versión posterior.

¿Puedo añadir una columna de PostgreSQL sin bloquear el tráfico?

Normalmente sí. Una columna que admite NULL y no tiene valor predeterminado suele ser un cambio breve de metadatos en PostgreSQL, aunque sigue necesitando un bloqueo de tabla. Configura un tiempo de espera de bloqueo corto para que la migración falle en vez de esperar a una transacción larga.

¿Cómo creo un índice sin bloquear las escrituras?

Usa CREATE INDEX CONCURRENTLY cuando la tabla deba seguir admitiendo escrituras. Tarda más y añade carga a la base de datos, y no puede ejecutarse dentro de un bloque de transacción, así que supervisa la latencia, WAL y el retraso de las réplicas mientras se ejecuta.

¿Cuándo debería una aplicación escribir tanto el campo antiguo como el nuevo?

Escribe ambos valores en la misma transacción de base de datos cuando las dos representaciones deban mantenerse al día. Define qué campo prevalece si no coinciden y utiliza las mismas reglas de normalización en API, workers, importaciones y herramientas de soporte.

¿Cómo relleno de forma segura una tabla grande de PostgreSQL?

Procesa lotes cortos que se puedan reanudar y confirma cada lote. Guarda un punto de control, actualiza solo las filas que aún necesiten trabajo y reduce la velocidad o pausa el trabajo cuando aumenten la latencia de consultas, las esperas de bloqueo, el volumen de WAL o el retraso de réplicas.

¿Cómo sé que un relleno de datos está completo y es correcto?

No cambies las lecturas solo porque haya terminado el relleno. Comprueba que existan los valores necesarios, compara las representaciones antigua y nueva, supervisa las lecturas de respaldo y confirma que las nuevas escrituras sigan siendo coherentes después de procesar los datos históricos.

¿Cuándo debo añadir NOT NULL, restricciones CHECK o claves foráneas?

Añade restricciones estrictas después de validar los datos existentes y de que todos los escritores activos proporcionen valores válidos. PostgreSQL permite añadir algunas restricciones como NOT VALID, aplicarlas a las filas nuevas y validar por separado las filas históricas.

¿Cuándo es seguro eliminar la ruta de esquema antigua?

Elimina primero las lecturas de respaldo, después deja de hacer escrituras antiguas y observa el sistema durante un ciclo operativo completo. Cuando ninguna aplicación, trabajo, informe, integración o cliente haga referencia al objeto antiguo, elimina el código relacionado y borra la columna o tabla de base de datos en una versión posterior.

Related posts