{"schemaVersion":1,"id":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication","title":"PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales","description":"Análisis práctico de las funcionalidades más relevantes de PostgreSQL 17: backup incremental con pg_basebackup, failover de replicación lógica, nuevas funciones JSON, pg_stat_io, y estrategias de migración desde PG 15/16.","language":"es","datePublished":"2025-10-14","dateModified":null,"contentFormat":"text/markdown","content":"Databases\n\n# PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales\n\n2025-10-1411 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/postgresql-17-incremental-backup-logical-replication>)\n\n## Backup Incremental: Una Feature Largamente Esperada\n\nAntes de PG 17, ` pg_basebackup ` siempre tomaba un backup full. Siempre. Una base de 500GB significaba copiar 500GB, ya sea que cambiaras 10 filas o 10 millones desde el último backup. Las alternativas eran herramientas de terceros como pgBackRest (excelente, pero otra cosa más para operar) o snapshots a nivel de filesystem (que tienen sus propios problemas).\n\nPG 17 agrega soporte nativo de backup incremental. Así funciona:\n\n```bash\n# Paso 1: Habilitar summarización de WAL (requerido, hacé esto primero!)\n# postgresql.conf\nsummarize_wal = on\n\n# Reiniciar PostgreSQL para que tome efecto\nsudo systemctl restart postgresql@17-main\n\n# Paso 2: Tomar el primer backup full (este es el baseline)\npg_basebackup -D /backups/full_20260114 \\\n --checkpoint=fast \\\n --wal-method=stream \\\n --manifest-checksums=SHA256 \\\n -U replication_user\n\n# Paso 3: Tomar backups incrementales (solo bloques cambiados)\npg_basebackup -D /backups/incr_20260115 \\\n --incremental=/backups/full_20260114/backup_manifest \\\n --checkpoint=fast \\\n --wal-method=stream \\\n -U replication_user\n```\n\nEl flag ` --incremental ` apunta al archivo de manifiesto del backup previo. PostgreSQL compara los resúmenes de WAL para determinar qué bloques de 8KB cambiaron desde que se creó ese manifiesto, y solo transfiere ese es bloques.\n\n**Números del mundo real de una base de datos de producción representativa (480GB):**\n\n| Tipo de backup | Tamaño | Duración | Notas |\n| --- | --- | --- | --- |\n| Backup full | 480GB | 47 min | Baseline |\n| Día 1 incremental | 8.2GB | 4.2 min | ~1.7% tasa de cambio diario |\n| Día 2 incremental | 9.1GB | 4.8 min | Día un poco más movido |\n| Día 7 incremental | 7.6GB | 3.9 min | Finde, menos actividad |\n| Full sintético semanal | 480GB | 12 min | Reconstruido de la cadena |\n\nEsa última fila es importante. Puede reconstruir un backup full a partir de una cadena incremental usando ` pg_combinebackup `:\n\n```bash\n# Combinar full + incrementales en un backup full sintético\npg_combinebackup \\\n /backups/full_20260114 \\\n /backups/incr_20260115 \\\n /backups/incr_20260116 \\\n /backups/incr_20260117 \\\n -o /backups/synthetic_full_20260117\n\n# El full sintético se puede usar como nuevo baseline\n# para futuras cadenas incrementales\n```\n\nLa reconstrucción toma ~12 minutos para una base de 480GB porque es mayormente lecturas y escrituras secuenciales, no se necesita transferencia de red si todos los backups están en el mismo storage.\n\n**El gotcha de la summarización de WAL:** El setting ` summarize_wal ` crea archivos de resumen en ` pg_wal/summaries/ `. Son chicos (unos pocos KB cada uno), pero solo se crean para WAL que se genera *después* de que se habilita el setting. Si lo se habilita e inmediatamente se intenta un backup incremental, va a fallar porque no hay resumen cubriendo el período entre tu backup full anterior y ahora. Habilitalo primero, tomá un backup full fresco, después iniciar tu cadena incremental.\n\n## Failover de Replicación Lógica: Finalmente\n\nPG 17 agrega ` failover = true ` en subscriptions y el parámetro ` sync ` en slots de replicación. Esto significa que los slots de replicación lógica pueden sobrevivir un failover a un standby. Antes de esto, si tu primary se caía y promovías un standby, todos los subscribers de replicación lógica tenían que ser reinicializados desde cero. En una base de 500GB, eso son horas de downtime.\n\nAcá está el setup:\n\n```sql\n-- En el primary: crear una publication\nCREATE PUBLICATION my_pub FOR ALL TABLES;\n\n-- En el subscriber: crear subscription con soporte de failover\nCREATE SUBSCRIPTION my_sub\n CONNECTION 'host=primary-host port=5432 dbname=mydb'\n PUBLICATION my_pub\n WITH (failover = true);\n```\n\nEn el primary, se va a ver que el slot de replicación tiene ` failover = true `:\n\n```sql\nSELECT slot_name, plugin, failover, synced\nFROM pg_replication_slots\nWHERE slot_name = 'my_sub';\n\n-- slot_name | plugin | failover | synced\n-- -----------+---------+----------+--------\n-- my_sub | pgoutput| t | f\n```\n\nLa columna ` synced ` pasa a ` true ` una vez que el standby sincronizó la posición del slot. Cuando pasa el failover, el nuevo primary tiene la posición LSN exacta del subscriber, así que la replicación se reanuda sin pérdida de datos ni re-sync.\n\nLa configuración en el standby que habilita esto:\n\n```\n# postgresql.conf en standby\nsync_replication_slots = on\nhot_standby_feedback = on\n```\n\nProbando con un failover simulado (promoviendo el standby vía ` pg_ctl promote `), el subscriber se reconecta y reanuda la replicación en 3 segundos. Antes de PG 17, el mismo escenario requería dropear y recrear la subscription, lo que significaba un sync completo de tabla, 2 horas para un dataset comparable.\n\n## JSON\\_TABLE: SQL/JSON Hecho Bien\n\nEscribir ` jsonb_array_elements ` con lateral joins siempre se sintió como un workaround. PG 17 implementa ` JSON_TABLE() ` del estándar SQL/JSON, y es sustancialmente más limpio:\n\n```sql\n-- La forma vieja (PG 16 y anteriores)\nSELECT\n e->>'id' AS id,\n e->>'name' AS name,\n e->>'email' AS email\nFROM orders o,\n LATERAL jsonb_array_elements(o.metadata->'contacts') AS e\nWHERE o.created_at > '2026-01-01';\n\n-- La forma nueva (PG 17)\nSELECT t.*\nFROM orders o,\n JSON_TABLE(\n o.metadata,\n '$.contacts[*]'\n COLUMNS (\n id INTEGER PATH '$.id',\n name TEXT PATH '$.name',\n email TEXT PATH '$.email'\n )\n ) AS t\nWHERE o.created_at > '2026-01-01';\n```\n\nPero el poder real está en los paths anidados y el manejo de errores:\n\n```sql\n-- JSON_TABLE anidado con manejo de errores\nSELECT t.*\nFROM api_responses r,\n JSON_TABLE(\n r.body,\n '$.data.users[*]'\n COLUMNS (\n user_id INTEGER PATH '$.id',\n username TEXT PATH '$.profile.name'\n DEFAULT 'unknown' ON EMPTY\n DEFAULT 'error' ON ERROR,\n NESTED PATH '$.orders[*]' COLUMNS (\n order_id INTEGER PATH '$.id',\n total NUMERIC(10,2) PATH '$.total',\n items_count INTEGER PATH '$.items.size()'\n )\n )\n ) AS t;\n```\n\nLas cláusulas ` ON EMPTY ` y ` ON ERROR ` son un game-changer para lidiar con estructuras JSON inconsistentes. Se acabaron las cadenas de ` COALESCE(jsonb_extract_path_text(...), 'default') `.\n\nEn cuanto a rendimiento, ` JSON_TABLE ` no es magia, sigue haciendo el mismo trabajo por debajo. Pero el query planner puede razonar mejor porque ve una sola función que retorna tabla en vez de una cadena de lateral joins. En un query que toca 50,000 filas con un promedio de 8 elementos JSON en array cada una, los benchmarks muestran una mejora del 22% en tiempo de ejecución versus el approach equivalente con ` jsonb_array_elements `.\n\n## pg\\_stat\\_io: Entendiendo Tus Patrones de I/O\n\nEsta vista se agregó en PG 16, pero PG 17 la expandió significativamente con mejor categorización de backend types y estadísticas de extend. Se convierte en una herramienta de diagnóstico esencial.\n\n```sql\n-- Top consumidores de I/O por tipo de backend\nSELECT\n backend_type,\n io_object,\n io_context,\n reads,\n read_bytes / (1024*1024*1024.0) AS read_gb,\n writes,\n write_bytes / (1024*1024*1024.0) AS write_gb,\n extends,\n fsyncs\nFROM pg_stat_io\nWHERE reads > 0 OR writes > 0\nORDER BY reads + writes DESC\nLIMIT 20;\n```\n\nLo que esto revela en un server de producción representativo:\n\n```\n backend_type | io_object | io_context | reads | read_gb | writes | write_gb\n-------------------+-----------+------------+----------+---------+----------+---------\n autovacuum worker | relation | normal | 14822041 | 112.8 | 2841023 | 21.6\n client backend | relation | normal | 4201882 | 32.0 | 891204 | 6.8\n client backend | relation | bulkread | 3102441 | 23.6 | 0 | 0.0\n background writer | relation | normal | 0 | 0.0 | 1204812 | 9.2\n checkpointer | relation | normal | 0 | 0.0 | 982104 | 7.5\n```\n\nAutovacuum estaba haciendo 3.5x más lecturas que todos los queries de clientes combinados. Eso no es necesariamente malo, autovacuum está haciendo su trabajo, pero indica que el ` autovacuum_vacuum_cost_delay ` era demasiado agresivo y vacuum estaba escaneando tablas que no lo necesitaban.\n\nDespués del tuning:\n\n```sql\n-- Settings de autovacuum por tabla para tablas grandes, mayormente estáticas\nALTER TABLE historical_events SET (\n autovacuum_vacuum_threshold = 10000,\n autovacuum_vacuum_scale_factor = 0.01,\n autovacuum_analyze_threshold = 5000\n);\n```\n\nLas lecturas de autovacuum cayeron un 60%, y el rendimiento de I/O de queries de clientes mejoró mediblemente porque había menos contención en el buffer pool.\n\n## Mejoras de VACUUM: 8x Menos Lecturas de Páginas\n\nPG 17 cambió fundamentalmente cómo VACUUM maneja las páginas. El comportamiento anterior era leer cada página de una tabla para checkear dead tuples, incluso si la página ya estaba completamente frozen (cada tuple visible para todas las transacciones). PG 17 trackea el estado de páginas frozen en el visibility map y las saltea completamente.\n\nPara una tabla grande con una ratio alta de páginas frozen sobre total (común para tablas append-heavy como logs de eventos), esto es transformador:\n\n```sql\n-- Verificar la ratio de páginas frozen de tu tabla\nSELECT\n relname,\n pg_size_pretty(pg_relation_size(oid)) AS table_size,\n n_tup_ins AS inserts_since_vacuum,\n n_tup_upd AS updates_since_vacuum,\n n_dead_tup AS dead_tuples,\n last_autovacuum\nFROM pg_stat_user_tables\nWHERE relname = 'events'\nORDER BY pg_relation_size(oid) DESC;\n```\n\nEn una tabla ` events ` representativa (89GB, 95% páginas frozen), el tiempo de VACUUM cayó de 18 minutos a 2.2 minutos. La reducción de I/O fue incluso más dramática, de 89GB de lecturas a menos de 5GB.\n\n## El Camino de Migración: PG 15/16 → PG 17\n\nEsta migración se realizó múltiples veces en diferentes entornos. Acá está el playbook que funciona:\n\n### Opción A: pg\\_upgrade (más simple, breve downtime)\n\n```bash\n# 1. Instalar PG 17 junto a la versión existente\nsudo apt install postgresql-17\n\n# 2. Parar ambos clusters\nsudo systemctl stop postgresql@16-main\nsudo systemctl stop postgresql@17-main\n\n# 3. Correr pg_upgrade en modo check primero\nsudo -u postgres pg_upgrade \\\n --old-datadir=/var/lib/postgresql/16/main \\\n --new-datadir=/var/lib/postgresql/17/main \\\n --old-bindir=/usr/lib/postgresql/16/bin \\\n --new-bindir=/usr/lib/postgresql/17/bin \\\n --check\n\n# 4. Si el check pasa, correr el upgrade real\nsudo -u postgres pg_upgrade \\\n --old-datadir=/var/lib/postgresql/16/main \\\n --new-datadir=/var/lib/postgresql/17/main \\\n --old-bindir=/usr/lib/postgresql/16/bin \\\n --new-bindir=/usr/lib/postgresql/17/bin \\\n --link # Usar hardlinks, 10x más rápido\n\n# 5. Arrancar PG 17 y correr el script post-upgrade\nsudo systemctl start postgresql@17-main\nsudo -u postgres /var/lib/postgresql/17/main/analyze_new_cluster.sh\n```\n\nLa opción ` --link ` es crítica, crea hardlinks en vez de copiar archivos de datos, reduciendo el tiempo de upgrade de horas a minutos para bases grandes. Los archivos de datos del cluster viejo se vuelven inválidos después de esto (comparten páginas con el cluster nuevo), así que mantener un backup apropiado.\n\nPara una base de 480GB, tiempos típicos de upgrade:\n\n- Con ` --link `: 4 minutos (mayormente operaciones de catálogo)\n- Sin ` --link `: 3 horas 20 minutos (copiando 480GB)\n\n### Opción B: Replicación lógica (zero downtime)\n\n```bash\n# 1. Levantar PG 17 como instancia fresca\n# 2. Crear el schema en PG 17 (pg_dump --schema-only)\npg_dump -h pg16-host -d mydb --schema-only | psql -h pg17-host -d mydb\n\n# 3. Crear publication en PG 16\npsql -h pg16-host -d mydb -c \"CREATE PUBLICATION upgrade_pub FOR ALL TABLES;\"\n\n# 4. Crear subscription en PG 17\npsql -h pg17-host -d mydb -c \"\n CREATE SUBSCRIPTION upgrade_sub\n CONNECTION 'host=pg16-host port=5432 dbname=mydb'\n PUBLICATION upgrade_pub;\"\n\n# 5. Esperar al sync inicial (checkear pg_stat_subscription)\n# 6. Cuando esté al día, apuntar la aplicación a PG 17\n# 7. Dropear subscription y publication\n```\n\nEste approach tiene zero downtime pero requiere más cuidado operacional, teners que manejar sequences, large objects, y cualquier cambio DDL durante la ventana de migración.\n\n### El gotcha específico de PG 17\n\nSi se está usando replicación lógica en PG 16 y upgradeando a PG 17, tener en cuenta que PG 17 cambió cómo funciona el tracking de LSN de slots de replicación con la nueva feature de failover. Si tus slots de replicación fueron creados sin ` failover = true ` (el default), van a funcionar bien después del upgrade. Pero si se intenta agregar ` failover = true ` a slots existentes post-upgrade, se necesita asegurarte de que ` sync_replication_slots = on ` en todos los standbys primero, o el slot va a parecer sano pero no va a estar realmente sincronizado.\n\n## Limitaciones y Advertencias\n\nNo todo en PG 17 es una ganancia para cada workload:\n\n- **Las mejoras de MERGE** están buenas pero siguen por detrás del INSERT ON DUPLICATE KEY UPDATE de MySQL en rendimiento crudo para patrones de upsert con alta concurrencia.\n- **El lenguaje de path SQL/JSON** es poderoso pero los mensajes de error cuando una expresión de path está mal son crípticos. Debuggear queries de ` JSON_TABLE ` requiere paciencia.\n- **La nueva opción DEFAULT de COPY** es útil pero tiene una interacción sorprendente con columnas GENERATED ALWAYS, se necesita especificar listas de columnas explícitamente para evitar errores.\n\nEstas son quejas menores en el contexto de un release genuinamente sólido. PG 17 es el tipo de upgrade que no requiere que cambies nada pero hace que todo funcione un poco mejor. Y la feature de backup incremental sola justifica el upgrade para cualquiera manejando bases de datos de más de 100GB.\n\nLa recomendación: si se está en PG 15, iniciar a planear la migración ahora. Si se está en PG 16, el upgrade es directo y las mejoras operacionales valen la pena. Si seguís en PG 14 o menos, realmente se necesita moverte, PG 14 llega a end-of-life en noviembre 2026.\n\n## Preguntas frecuentes\n\n### ¿Qué tan significativas son las mejoras de rendimiento en PostgreSQL 17?\n\nPara la mayoría de los workloads, esperá un 5-15% de mejora en throughput de queries, principalmente por optimización de VACUUM (lee 8x menos páginas para tablas mayormente frozen), mejores algoritmos de sort para queries con LIMIT, y mejor manejo de hash joins. Las ganancias más grandes son operacionales, el backup incremental reduce el almacenamiento de backup en 80-90% y el tiempo proporcionalmente.\n\n### ¿Es seguro actualizar de PostgreSQL 15 o 16 a 17?\n\nSí, con advertencias. pg\\_upgrade funciona confiablemente para 16→17 basado en experiencia de producción. Para 15→17, probar exhaustivamente, algunos cambios en el comportamiento de slots de replicación lógica pueden romper setups de replicación existentes. El approach de pg\\_createsubscriber para upgrades basados en replicación lógica es el camino más seguro para migraciones sin downtime.\n\n### ¿Cuál es la mejora operacional más grande en PG 17?\n\nBackup incremental, por lejos. Para una base de datos de 500GB que cambia 2% diario, el backup diario pasó de 500GB (full) a ~12GB (incremental). Combinado con archivado de WAL, esto hace que el point-in-time recovery sea práctico incluso para bases de datos multi-terabyte en almacenamiento cloud.\n\n## Tips\n\n1. **El backup incremental requiere summarización de WAL habilitada:** Configurar ` summarize_wal = on ` en postgresql.conf ANTES de tomar tu primer backup full. Sin esto, ` pg_basebackup --incremental ` falla silenciosamente y toma un backup full. Verificar la vista ` pg_wal_summary_stats ` para verificar que se estén generando los resúmenes.\n2. **Utilizar ` pg_createsubscriber ` para upgrades PG16→17 sin downtime:** En vez de pg\\_dump/restore, convertí un standby físico a subscriber lógico con ` pg_createsubscriber --pgdata /var/lib/postgresql/17/main -d 'host=pg16-primary' `. Esto te da una réplica viva que es posible promover cuando estés listo.\n3. **` pg_stat_io ` revela patrones de I/O ocultos:** Consultá ` SELECT backend_type, io_object, io_context, reads, writes, extends FROM pg_stat_io WHERE reads > 0 ORDER BY reads DESC ` para encontrar qué procese es backend generan más I/O. Esto puede revelar que autovacuum hace 3x más lecturas que los queries de clientes, una mina de oro para tuning.\n4. **El nuevo ` JSON_TABLE() ` elimina la mayoría de las gimnasias JSON-a-filas:** En vez de ` jsonb_array_elements ` con lateral joins, utilizar ` SELECT * FROM JSON_TABLE(data, '$.items[*]' COLUMNS (id INT PATH '$.id', name TEXT PATH '$.name')) `. Es estándar SQL, y el query planner lo maneja mejor que las funciones jsonb equivalentes.\n\n- postgresql\n- bases-de-datos\n- backup\n- replicación\n- migración\n- pg17\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)","headings":[{"level":1,"text":"PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#title"},{"level":2,"text":"Backup Incremental: Una Feature Largamente Esperada","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#backup-incremental-una-feature-largamente-esperada"},{"level":2,"text":"Failover de Replicación Lógica: Finalmente","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#failover-de-replicacin-lgica-finalmente"},{"level":2,"text":"JSON_TABLE: SQL/JSON Hecho Bien","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#jsontable-sqljson-hecho-bien"},{"level":2,"text":"pg_stat_io: Entendiendo Tus Patrones de I/O","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#pgstatio-entendiendo-tus-patrones-de-io"},{"level":2,"text":"Mejoras de VACUUM: 8x Menos Lecturas de Páginas","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#mejoras-de-vacuum-8x-menos-lecturas-de-pginas"},{"level":2,"text":"El Camino de Migración: PG 15/16 → PG 17","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#el-camino-de-migracin-pg-1516-pg-17"},{"level":3,"text":"Opción A: pg_upgrade (más simple, breve downtime)","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#opcin-a-pgupgrade-ms-simple-breve-downtime"},{"level":3,"text":"Opción B: Replicación lógica (zero downtime)","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#opcin-b-replicacin-lgica-zero-downtime"},{"level":3,"text":"El gotcha específico de PG 17","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#el-gotcha-especfico-de-pg-17"},{"level":2,"text":"Limitaciones y Advertencias","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#limitaciones-y-advertencias"},{"level":2,"text":"Preguntas frecuentes","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#questions"},{"level":3,"text":"¿Qué tan significativas son las mejoras de rendimiento en PostgreSQL 17?","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#question-1"},{"level":3,"text":"¿Es seguro actualizar de PostgreSQL 15 o 16 a 17?","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#question-2"},{"level":3,"text":"¿Cuál es la mejora operacional más grande en PG 17?","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#article-tips"}],"sections":[{"heading":"PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales","level":1,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication","content":"Databases"},{"heading":"PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales","level":1,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#title","content":"2025-10-1411 min lectura\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nTambién disponible en: [English](<https://lucioduran.com/blog/postgresql-17-incremental-backup-logical-replication>)"},{"heading":"Backup Incremental: Una Feature Largamente Esperada","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#backup-incremental-una-feature-largamente-esperada","content":"Antes de PG 17, ` pg_basebackup ` siempre tomaba un backup full. Siempre. Una base de 500GB significaba copiar 500GB, ya sea que cambiaras 10 filas o 10 millones desde el último backup. Las alternativas eran herramientas de terceros como pgBackRest (excelente, pero otra cosa más para operar) o snapshots a nivel de filesystem (que tienen sus propios problemas).\n\nPG 17 agrega soporte nativo de backup incremental. Así funciona:\n\n```bash\n# Paso 1: Habilitar summarización de WAL (requerido, hacé esto primero!)\n# postgresql.conf\nsummarize_wal = on\n\n# Reiniciar PostgreSQL para que tome efecto\nsudo systemctl restart postgresql@17-main\n\n# Paso 2: Tomar el primer backup full (este es el baseline)\npg_basebackup -D /backups/full_20260114 \\\n --checkpoint=fast \\\n --wal-method=stream \\\n --manifest-checksums=SHA256 \\\n -U replication_user\n\n# Paso 3: Tomar backups incrementales (solo bloques cambiados)\npg_basebackup -D /backups/incr_20260115 \\\n --incremental=/backups/full_20260114/backup_manifest \\\n --checkpoint=fast \\\n --wal-method=stream \\\n -U replication_user\n```\n\nEl flag ` --incremental ` apunta al archivo de manifiesto del backup previo. PostgreSQL compara los resúmenes de WAL para determinar qué bloques de 8KB cambiaron desde que se creó ese manifiesto, y solo transfiere ese es bloques.\n\n**Números del mundo real de una base de datos de producción representativa (480GB):**\n\n| Tipo de backup | Tamaño | Duración | Notas |\n| --- | --- | --- | --- |\n| Backup full | 480GB | 47 min | Baseline |\n| Día 1 incremental | 8.2GB | 4.2 min | ~1.7% tasa de cambio diario |\n| Día 2 incremental | 9.1GB | 4.8 min | Día un poco más movido |\n| Día 7 incremental | 7.6GB | 3.9 min | Finde, menos actividad |\n| Full sintético semanal | 480GB | 12 min | Reconstruido de la cadena |\n\nEsa última fila es importante. Puede reconstruir un backup full a partir de una cadena incremental usando ` pg_combinebackup `:\n\n```bash\n# Combinar full + incrementales en un backup full sintético\npg_combinebackup \\\n /backups/full_20260114 \\\n /backups/incr_20260115 \\\n /backups/incr_20260116 \\\n /backups/incr_20260117 \\\n -o /backups/synthetic_full_20260117\n\n# El full sintético se puede usar como nuevo baseline\n# para futuras cadenas incrementales\n```\n\nLa reconstrucción toma ~12 minutos para una base de 480GB porque es mayormente lecturas y escrituras secuenciales, no se necesita transferencia de red si todos los backups están en el mismo storage.\n\n**El gotcha de la summarización de WAL:** El setting ` summarize_wal ` crea archivos de resumen en ` pg_wal/summaries/ `. Son chicos (unos pocos KB cada uno), pero solo se crean para WAL que se genera *después* de que se habilita el setting. Si lo se habilita e inmediatamente se intenta un backup incremental, va a fallar porque no hay resumen cubriendo el período entre tu backup full anterior y ahora. Habilitalo primero, tomá un backup full fresco, después iniciar tu cadena incremental."},{"heading":"Failover de Replicación Lógica: Finalmente","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#failover-de-replicacin-lgica-finalmente","content":"PG 17 agrega ` failover = true ` en subscriptions y el parámetro ` sync ` en slots de replicación. Esto significa que los slots de replicación lógica pueden sobrevivir un failover a un standby. Antes de esto, si tu primary se caía y promovías un standby, todos los subscribers de replicación lógica tenían que ser reinicializados desde cero. En una base de 500GB, eso son horas de downtime.\n\nAcá está el setup:\n\n```sql\n-- En el primary: crear una publication\nCREATE PUBLICATION my_pub FOR ALL TABLES;\n\n-- En el subscriber: crear subscription con soporte de failover\nCREATE SUBSCRIPTION my_sub\n CONNECTION 'host=primary-host port=5432 dbname=mydb'\n PUBLICATION my_pub\n WITH (failover = true);\n```\n\nEn el primary, se va a ver que el slot de replicación tiene ` failover = true `:\n\n```sql\nSELECT slot_name, plugin, failover, synced\nFROM pg_replication_slots\nWHERE slot_name = 'my_sub';\n\n-- slot_name | plugin | failover | synced\n-- -----------+---------+----------+--------\n-- my_sub | pgoutput| t | f\n```\n\nLa columna ` synced ` pasa a ` true ` una vez que el standby sincronizó la posición del slot. Cuando pasa el failover, el nuevo primary tiene la posición LSN exacta del subscriber, así que la replicación se reanuda sin pérdida de datos ni re-sync.\n\nLa configuración en el standby que habilita esto:\n\n```\n# postgresql.conf en standby\nsync_replication_slots = on\nhot_standby_feedback = on\n```\n\nProbando con un failover simulado (promoviendo el standby vía ` pg_ctl promote `), el subscriber se reconecta y reanuda la replicación en 3 segundos. Antes de PG 17, el mismo escenario requería dropear y recrear la subscription, lo que significaba un sync completo de tabla, 2 horas para un dataset comparable."},{"heading":"JSON_TABLE: SQL/JSON Hecho Bien","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#jsontable-sqljson-hecho-bien","content":"Escribir ` jsonb_array_elements ` con lateral joins siempre se sintió como un workaround. PG 17 implementa ` JSON_TABLE() ` del estándar SQL/JSON, y es sustancialmente más limpio:\n\n```sql\n-- La forma vieja (PG 16 y anteriores)\nSELECT\n e->>'id' AS id,\n e->>'name' AS name,\n e->>'email' AS email\nFROM orders o,\n LATERAL jsonb_array_elements(o.metadata->'contacts') AS e\nWHERE o.created_at > '2026-01-01';\n\n-- La forma nueva (PG 17)\nSELECT t.*\nFROM orders o,\n JSON_TABLE(\n o.metadata,\n '$.contacts[*]'\n COLUMNS (\n id INTEGER PATH '$.id',\n name TEXT PATH '$.name',\n email TEXT PATH '$.email'\n )\n ) AS t\nWHERE o.created_at > '2026-01-01';\n```\n\nPero el poder real está en los paths anidados y el manejo de errores:\n\n```sql\n-- JSON_TABLE anidado con manejo de errores\nSELECT t.*\nFROM api_responses r,\n JSON_TABLE(\n r.body,\n '$.data.users[*]'\n COLUMNS (\n user_id INTEGER PATH '$.id',\n username TEXT PATH '$.profile.name'\n DEFAULT 'unknown' ON EMPTY\n DEFAULT 'error' ON ERROR,\n NESTED PATH '$.orders[*]' COLUMNS (\n order_id INTEGER PATH '$.id',\n total NUMERIC(10,2) PATH '$.total',\n items_count INTEGER PATH '$.items.size()'\n )\n )\n ) AS t;\n```\n\nLas cláusulas ` ON EMPTY ` y ` ON ERROR ` son un game-changer para lidiar con estructuras JSON inconsistentes. Se acabaron las cadenas de ` COALESCE(jsonb_extract_path_text(...), 'default') `.\n\nEn cuanto a rendimiento, ` JSON_TABLE ` no es magia, sigue haciendo el mismo trabajo por debajo. Pero el query planner puede razonar mejor porque ve una sola función que retorna tabla en vez de una cadena de lateral joins. En un query que toca 50,000 filas con un promedio de 8 elementos JSON en array cada una, los benchmarks muestran una mejora del 22% en tiempo de ejecución versus el approach equivalente con ` jsonb_array_elements `."},{"heading":"pg_stat_io: Entendiendo Tus Patrones de I/O","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#pgstatio-entendiendo-tus-patrones-de-io","content":"Esta vista se agregó en PG 16, pero PG 17 la expandió significativamente con mejor categorización de backend types y estadísticas de extend. Se convierte en una herramienta de diagnóstico esencial.\n\n```sql\n-- Top consumidores de I/O por tipo de backend\nSELECT\n backend_type,\n io_object,\n io_context,\n reads,\n read_bytes / (1024*1024*1024.0) AS read_gb,\n writes,\n write_bytes / (1024*1024*1024.0) AS write_gb,\n extends,\n fsyncs\nFROM pg_stat_io\nWHERE reads > 0 OR writes > 0\nORDER BY reads + writes DESC\nLIMIT 20;\n```\n\nLo que esto revela en un server de producción representativo:\n\n```\n backend_type | io_object | io_context | reads | read_gb | writes | write_gb\n-------------------+-----------+------------+----------+---------+----------+---------\n autovacuum worker | relation | normal | 14822041 | 112.8 | 2841023 | 21.6\n client backend | relation | normal | 4201882 | 32.0 | 891204 | 6.8\n client backend | relation | bulkread | 3102441 | 23.6 | 0 | 0.0\n background writer | relation | normal | 0 | 0.0 | 1204812 | 9.2\n checkpointer | relation | normal | 0 | 0.0 | 982104 | 7.5\n```\n\nAutovacuum estaba haciendo 3.5x más lecturas que todos los queries de clientes combinados. Eso no es necesariamente malo, autovacuum está haciendo su trabajo, pero indica que el ` autovacuum_vacuum_cost_delay ` era demasiado agresivo y vacuum estaba escaneando tablas que no lo necesitaban.\n\nDespués del tuning:\n\n```sql\n-- Settings de autovacuum por tabla para tablas grandes, mayormente estáticas\nALTER TABLE historical_events SET (\n autovacuum_vacuum_threshold = 10000,\n autovacuum_vacuum_scale_factor = 0.01,\n autovacuum_analyze_threshold = 5000\n);\n```\n\nLas lecturas de autovacuum cayeron un 60%, y el rendimiento de I/O de queries de clientes mejoró mediblemente porque había menos contención en el buffer pool."},{"heading":"Mejoras de VACUUM: 8x Menos Lecturas de Páginas","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#mejoras-de-vacuum-8x-menos-lecturas-de-pginas","content":"PG 17 cambió fundamentalmente cómo VACUUM maneja las páginas. El comportamiento anterior era leer cada página de una tabla para checkear dead tuples, incluso si la página ya estaba completamente frozen (cada tuple visible para todas las transacciones). PG 17 trackea el estado de páginas frozen en el visibility map y las saltea completamente.\n\nPara una tabla grande con una ratio alta de páginas frozen sobre total (común para tablas append-heavy como logs de eventos), esto es transformador:\n\n```sql\n-- Verificar la ratio de páginas frozen de tu tabla\nSELECT\n relname,\n pg_size_pretty(pg_relation_size(oid)) AS table_size,\n n_tup_ins AS inserts_since_vacuum,\n n_tup_upd AS updates_since_vacuum,\n n_dead_tup AS dead_tuples,\n last_autovacuum\nFROM pg_stat_user_tables\nWHERE relname = 'events'\nORDER BY pg_relation_size(oid) DESC;\n```\n\nEn una tabla ` events ` representativa (89GB, 95% páginas frozen), el tiempo de VACUUM cayó de 18 minutos a 2.2 minutos. La reducción de I/O fue incluso más dramática, de 89GB de lecturas a menos de 5GB."},{"heading":"El Camino de Migración: PG 15/16 → PG 17","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#el-camino-de-migracin-pg-1516-pg-17","content":"Esta migración se realizó múltiples veces en diferentes entornos. Acá está el playbook que funciona:"},{"heading":"Opción A: pg_upgrade (más simple, breve downtime)","level":3,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#opcin-a-pgupgrade-ms-simple-breve-downtime","content":"```bash\n# 1. Instalar PG 17 junto a la versión existente\nsudo apt install postgresql-17\n\n# 2. Parar ambos clusters\nsudo systemctl stop postgresql@16-main\nsudo systemctl stop postgresql@17-main\n\n# 3. Correr pg_upgrade en modo check primero\nsudo -u postgres pg_upgrade \\\n --old-datadir=/var/lib/postgresql/16/main \\\n --new-datadir=/var/lib/postgresql/17/main \\\n --old-bindir=/usr/lib/postgresql/16/bin \\\n --new-bindir=/usr/lib/postgresql/17/bin \\\n --check\n\n# 4. Si el check pasa, correr el upgrade real\nsudo -u postgres pg_upgrade \\\n --old-datadir=/var/lib/postgresql/16/main \\\n --new-datadir=/var/lib/postgresql/17/main \\\n --old-bindir=/usr/lib/postgresql/16/bin \\\n --new-bindir=/usr/lib/postgresql/17/bin \\\n --link # Usar hardlinks, 10x más rápido\n\n# 5. Arrancar PG 17 y correr el script post-upgrade\nsudo systemctl start postgresql@17-main\nsudo -u postgres /var/lib/postgresql/17/main/analyze_new_cluster.sh\n```\n\nLa opción ` --link ` es crítica, crea hardlinks en vez de copiar archivos de datos, reduciendo el tiempo de upgrade de horas a minutos para bases grandes. Los archivos de datos del cluster viejo se vuelven inválidos después de esto (comparten páginas con el cluster nuevo), así que mantener un backup apropiado.\n\nPara una base de 480GB, tiempos típicos de upgrade:\n\n- Con ` --link `: 4 minutos (mayormente operaciones de catálogo)\n- Sin ` --link `: 3 horas 20 minutos (copiando 480GB)"},{"heading":"Opción B: Replicación lógica (zero downtime)","level":3,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#opcin-b-replicacin-lgica-zero-downtime","content":"```bash\n# 1. Levantar PG 17 como instancia fresca\n# 2. Crear el schema en PG 17 (pg_dump --schema-only)\npg_dump -h pg16-host -d mydb --schema-only | psql -h pg17-host -d mydb\n\n# 3. Crear publication en PG 16\npsql -h pg16-host -d mydb -c \"CREATE PUBLICATION upgrade_pub FOR ALL TABLES;\"\n\n# 4. Crear subscription en PG 17\npsql -h pg17-host -d mydb -c \"\n CREATE SUBSCRIPTION upgrade_sub\n CONNECTION 'host=pg16-host port=5432 dbname=mydb'\n PUBLICATION upgrade_pub;\"\n\n# 5. Esperar al sync inicial (checkear pg_stat_subscription)\n# 6. Cuando esté al día, apuntar la aplicación a PG 17\n# 7. Dropear subscription y publication\n```\n\nEste approach tiene zero downtime pero requiere más cuidado operacional, teners que manejar sequences, large objects, y cualquier cambio DDL durante la ventana de migración."},{"heading":"El gotcha específico de PG 17","level":3,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#el-gotcha-especfico-de-pg-17","content":"Si se está usando replicación lógica en PG 16 y upgradeando a PG 17, tener en cuenta que PG 17 cambió cómo funciona el tracking de LSN de slots de replicación con la nueva feature de failover. Si tus slots de replicación fueron creados sin ` failover = true ` (el default), van a funcionar bien después del upgrade. Pero si se intenta agregar ` failover = true ` a slots existentes post-upgrade, se necesita asegurarte de que ` sync_replication_slots = on ` en todos los standbys primero, o el slot va a parecer sano pero no va a estar realmente sincronizado."},{"heading":"Limitaciones y Advertencias","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#limitaciones-y-advertencias","content":"No todo en PG 17 es una ganancia para cada workload:\n\n- **Las mejoras de MERGE** están buenas pero siguen por detrás del INSERT ON DUPLICATE KEY UPDATE de MySQL en rendimiento crudo para patrones de upsert con alta concurrencia.\n- **El lenguaje de path SQL/JSON** es poderoso pero los mensajes de error cuando una expresión de path está mal son crípticos. Debuggear queries de ` JSON_TABLE ` requiere paciencia.\n- **La nueva opción DEFAULT de COPY** es útil pero tiene una interacción sorprendente con columnas GENERATED ALWAYS, se necesita especificar listas de columnas explícitamente para evitar errores.\n\nEstas son quejas menores en el contexto de un release genuinamente sólido. PG 17 es el tipo de upgrade que no requiere que cambies nada pero hace que todo funcione un poco mejor. Y la feature de backup incremental sola justifica el upgrade para cualquiera manejando bases de datos de más de 100GB.\n\nLa recomendación: si se está en PG 15, iniciar a planear la migración ahora. Si se está en PG 16, el upgrade es directo y las mejoras operacionales valen la pena. Si seguís en PG 14 o menos, realmente se necesita moverte, PG 14 llega a end-of-life en noviembre 2026."},{"heading":"Preguntas frecuentes","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#questions","content":""},{"heading":"¿Qué tan significativas son las mejoras de rendimiento en PostgreSQL 17?","level":3,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#question-1","content":"Para la mayoría de los workloads, esperá un 5-15% de mejora en throughput de queries, principalmente por optimización de VACUUM (lee 8x menos páginas para tablas mayormente frozen), mejores algoritmos de sort para queries con LIMIT, y mejor manejo de hash joins. Las ganancias más grandes son operacionales, el backup incremental reduce el almacenamiento de backup en 80-90% y el tiempo proporcionalmente."},{"heading":"¿Es seguro actualizar de PostgreSQL 15 o 16 a 17?","level":3,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#question-2","content":"Sí, con advertencias. pg\\_upgrade funciona confiablemente para 16→17 basado en experiencia de producción. Para 15→17, probar exhaustivamente, algunos cambios en el comportamiento de slots de replicación lógica pueden romper setups de replicación existentes. El approach de pg\\_createsubscriber para upgrades basados en replicación lógica es el camino más seguro para migraciones sin downtime."},{"heading":"¿Cuál es la mejora operacional más grande en PG 17?","level":3,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#question-3","content":"Backup incremental, por lejos. Para una base de datos de 500GB que cambia 2% diario, el backup diario pasó de 500GB (full) a ~12GB (incremental). Combinado con archivado de WAL, esto hace que el point-in-time recovery sea práctico incluso para bases de datos multi-terabyte en almacenamiento cloud."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#article-tips","content":"1. **El backup incremental requiere summarización de WAL habilitada:** Configurar ` summarize_wal = on ` en postgresql.conf ANTES de tomar tu primer backup full. Sin esto, ` pg_basebackup --incremental ` falla silenciosamente y toma un backup full. Verificar la vista ` pg_wal_summary_stats ` para verificar que se estén generando los resúmenes.\n2. **Utilizar ` pg_createsubscriber ` para upgrades PG16→17 sin downtime:** En vez de pg\\_dump/restore, convertí un standby físico a subscriber lógico con ` pg_createsubscriber --pgdata /var/lib/postgresql/17/main -d 'host=pg16-primary' `. Esto te da una réplica viva que es posible promover cuando estés listo.\n3. **` pg_stat_io ` revela patrones de I/O ocultos:** Consultá ` SELECT backend_type, io_object, io_context, reads, writes, extends FROM pg_stat_io WHERE reads > 0 ORDER BY reads DESC ` para encontrar qué procese es backend generan más I/O. Esto puede revelar que autovacuum hace 3x más lecturas que los queries de clientes, una mina de oro para tuning.\n4. **El nuevo ` JSON_TABLE() ` elimina la mayoría de las gimnasias JSON-a-filas:** En vez de ` jsonb_array_elements ` con lateral joins, utilizar ` SELECT * FROM JSON_TABLE(data, '$.items[*]' COLUMNS (id INT PATH '$.id', name TEXT PATH '$.name')) `. Es estándar SQL, y el query planner lo maneja mejor que las funciones jsonb equivalentes.\n\n- postgresql\n- bases-de-datos\n- backup\n- replicación\n- migración\n- pg17\n\n[Volver al Blog](<https://lucioduran.com/es/blog>)"}],"links":[{"text":"English","url":"https://lucioduran.com/blog/postgresql-17-incremental-backup-logical-replication"},{"text":"Volver al Blog","url":"https://lucioduran.com/es/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication"},"headline":"PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales","description":"Análisis práctico de las funcionalidades más relevantes de PostgreSQL 17: backup incremental con pg_basebackup, failover de replicación lógica, nuevas funciones JSON, pg_stat_io, y estrategias de migración desde PG 15/16.","datePublished":"2025-10-14","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication","inLanguage":"es","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Databases","keywords":"postgresql, bases-de-datos, backup, replicación, migración, pg17","wordCount":1995,"timeRequired":"PT11M","workTranslation":[{"@type":"BlogPosting","inLanguage":"en","url":"https://lucioduran.com/blog/postgresql-17-incremental-backup-logical-replication"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Inicio","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/es/blog"},{"@type":"ListItem","position":3,"name":"PostgreSQL 17: Backup Incremental, Failover de Replicación Lógica y Mejoras Operacionales","item":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"¿Qué tan significativas son las mejoras de rendimiento en PostgreSQL 17?","acceptedAnswer":{"@type":"Answer","text":"Para la mayoría de los workloads, esperá un 5-15% de mejora en throughput de queries, principalmente por optimización de VACUUM (lee 8x menos páginas para tablas mayormente frozen), mejores algoritmos de sort para queries con LIMIT, y mejor manejo de hash joins. Las ganancias más grandes son operacionales, el backup incremental reduce el almacenamiento de backup en 80-90% y el tiempo proporcionalmente."}},{"@type":"Question","name":"¿Es seguro actualizar de PostgreSQL 15 o 16 a 17?","acceptedAnswer":{"@type":"Answer","text":"Sí, con advertencias. pg_upgrade funciona confiablemente para 16→17 basado en experiencia de producción. Para 15→17, probar exhaustivamente, algunos cambios en el comportamiento de slots de replicación lógica pueden romper setups de replicación existentes. El approach de pg_createsubscriber para upgrades basados en replicación lógica es el camino más seguro para migraciones sin downtime."}},{"@type":"Question","name":"¿Cuál es la mejora operacional más grande en PG 17?","acceptedAnswer":{"@type":"Answer","text":"Backup incremental, por lejos. Para una base de datos de 500GB que cambia 2% diario, el backup diario pasó de 500GB (full) a ~12GB (incremental). Combinado con archivado de WAL, esto hace que el point-in-time recovery sea práctico incluso para bases de datos multi-terabyte en almacenamiento cloud."}}]}]}],"contentHash":"8d7a78ba86e8801a11f8080bcb913468edd3aece5419eceee0cdb264307bf1ec","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/es/blog/postgresql-17-incremental-backup-logical-replication","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":75451,"contentBytes":17033}}
