Volver al Blog
Databases & Backend25 de octubre de 202510 min de lectura

La caja de herramientas oculta de Postgres: features que tu stack probablemente ya está reemplazando

DR

Doug Ribeiro

10 min de lectura

El post de la escalera de adopción cubrió las decisiones macro: sobre qué features de Postgres construir tu stack y dónde frenar. Este trata las decisiones micro. Herramientas dentro de Postgres que reemplazan, cada una, un pedacito de tu infraestructura, y que nadie mete en el tour de features habitual porque no son lo bastante espectaculares para abrir una charla de conferencia.

La mayoría de los seniors que conozco ha adoptado dos o tres de estas y no tiene ni idea de que existen las demás. Pagan un servicio de scheduling porque nunca se enteraron de pg_cron. Levantan un Redis porque nadie les habló de los advisory locks. No son features del estilo 'Postgres puede hacerlo todo'. Son features del estilo 'Postgres ya lo hacía y tú no lo sabías'.

LISTEN/NOTIFY, usado con cabeza

El post de la escalera metió a LISTEN/NOTIFY en el Nivel 3 porque los equipos tratan de convertirlo en el sistema nervioso de un producto de alto tráfico y se queman. Ese veredicto sigue en pie. Pero tiene un caso de uso legítimo que merece respeto: coordinación de baja frecuencia entre tu aplicación y tu base.

Ejemplos que funcionan bien: un dashboard de admin en Angular que debe auto-refrescarse cuando un agente de soporte actualiza un ticket. Una capa de caché que necesita invalidar una clave específica cuando cambia una fila. Un worker que se despierta cuando aparecen jobs nuevos (combinado con SKIP LOCKED, esto te da colas event-driven sin polling).

Como guía: por debajo de unos 100 listeners, payloads bajo 1KB, escrituras por segundo en los cientos bajos. Esa es la zona cómoda, no el techo. El post de la escalera sitúa el techo práctico cerca de 1000 listeners, y la distancia entre ambos números es el margen que quieres tener para el día en que la carga de escritura se dispare. Dentro de la zona cómoda es elegante, confiable y completamente gratis. Fuera de ella, vuelve a leer la escalera.

Foreign Data Wrappers

postgres_fdw, mysql_fdw, mongo_fdw, file_fdw, sqlite_fdw. Puedes consultar tablas de otras bases, archivos CSV en disco, o archivos Parquet (vía parquet_fdw, o parquet_s3_fdw, un derivado que agrega soporte para S3), como si fueran tablas Postgres normales. Los joins entre ellas funcionan. Las vistas por encima funcionan.

Esta es la que más importa durante migraciones. Pasar una tabla MySQL legacy a Postgres antes implicaba pipeline ETL, ventana de cutover y un sábado incómodo con todo el equipo conectado. Con mysql_fdw creas la foreign table, escribes vistas que cruzan los datos legacy con tu schema nuevo, migras de forma incremental, y tiras el FDW cuando la última fila ya es local. Cero código ETL, ningún cutover con pánico.

pg_cron

Jobs programados, dentro de Postgres, definidos como SQL:

sql
SELECT cron.schedule(
  'cleanup-old-events',
  '0 2 * * *',
  $$DELETE FROM events WHERE created_at < NOW() - INTERVAL '90 days'$$
);

Ese es todo tu job de retención. Sin DAG de Airflow, sin CronJob de Kubernetes, sin Lambda más EventBridge. Si lo que necesitas programar es una sentencia SQL o un stored procedure, con pg_cron te alcanza.

Elimina infraestructura de verdad cuando lo usas para refrescar vistas materializadas según calendario, expirar tokens, archivar particiones viejas o correr checks periódicos de calidad de datos. En el momento en que el trabajo programado necesita llamadas a APIs externas, te gradúas a otra cosa. Hasta entonces, esto te ahorra un servicio entero.

Advisory locks

Postgres tiene dos sistemas de locking distintos. Uno es el locking implícito de filas y tablas que viene con las transacciones. El otro son locks definidos por la aplicación, identificados con IDs enteros arbitrarios. Llamas a pg_advisory_lock(id) y lo sostienes hasta que lo liberas o hasta que termina la sesión.

Esto reemplaza a Redis para locking cooperativo en la mayoría de los casos. 'Solo un worker debe correr este cron en todas las instancias' es un advisory lock sobre un ID conocido. 'Solo un servicio debe refrescar esta entrada de caché a la vez' es un advisory lock con clave basada en un hash de la clave de caché. 'Evitar que dos workers procesen al mismo usuario al mismo tiempo' es un advisory lock sobre el user ID.

La pega que debes conocer: los advisory locks con scope de sesión no funcionan con transaction pooling (PgBouncer en modo transaction). Si estás detrás de un pooler, usa las variantes con scope de transacción (pg_advisory_xact_lock).

Tipos range más exclusion constraints

Problema clásico y difícil: dos personas no pueden reservar la misma habitación de hotel en fechas que se solapan. La solución ingenua implica un lock, un select, un check, un insert y un loop de retry cuando la condición de carrera muerde. La solución Postgres es una constraint:

sql
CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE bookings (
  room_id int,
  period tstzrange,
  EXCLUDE USING gist (room_id WITH =, period WITH &&)
);

La extensión btree_gist es la que permite a GiST indexar el operador de igualdad sobre tipos escalares como int; sin ella, room_id WITH = no funciona. Esa constraint hace imposibles los solapamientos a nivel de base. Sin condición de carrera, sin lógica de retry, sin lock distribuido. Reservas, scheduling, asignación de recursos, cualquier problema de rangos temporales se mapea limpiamente a este patrón. El operador && verifica solapamiento de rangos; la exclusion constraint lo prohíbe dentro del mismo room_id.

He visto a equipos construir microservicios enteros para resolver esto con locks en Redis y bugs de corrección, cuando siete líneas de SQL lo hubieran hecho.

pg_stat_statements

Observabilidad de rendimiento de queries, integrada. Cada sentencia que ejecuta tu base, agregada por forma (con los parámetros quitados), con tiempo total, tiempo medio, número de llamadas y lecturas de buffer. Agregas pg_stat_statements a shared_preload_libraries en postgresql.conf, reinicias Postgres y después corres CREATE EXTENSION pg_stat_statements. El reinicio es la parte que pilla a la gente desprevenida: un CREATE EXTENSION pelado, sin el preload, no funciona.

Esto es lo primero que enciendo en cualquier instancia Postgres nueva. Responde la pregunta más común, '¿por qué la base de datos está lenta?', sin un APM. El distributed tracing y el contexto a nivel de app siguen necesitando uno. Esto cubre el lado de la base y nada por encima.

Combínalo con pg_stat_activity (queries en vivo) y pg_stat_user_indexes (uso de índices), y ya tienes una stack de observabilidad funcional para la base, con cero dependencias externas.

Tablas unlogged

Las tablas Postgres normales escriben al WAL (write-ahead log) para garantizar durabilidad. Las tablas unlogged no. No son crash-safe (se truncan ante un apagón sucio), pero las escrituras son 2-3x más rápidas y no presionan la replicación.

Encajan bien en: almacenamiento de sesiones, contadores de rate limiting, métricas agregadas con retención corta, buckets analíticos efímeros. Cualquier carga con forma de Redis donde perder datos en un crash raro es aceptable.

No son un reemplazo general de Redis (no hay semántica in-memory, no hay pub/sub, no hay soporte de TTL). Pero para casos específicos donde quieres performance de escritura tipo Redis sin levantar otro servicio, son una opción legítima de la que nadie se acuerda.

Cómo se componen

Cada una, por separado, es una comodidad pequeña. Lo que importa es que, juntas, cada adopción es un servicio que no tienes que operar. Ninguna instancia de scheduler, ningún cluster de Redis para locks, ningún pipeline ETL, ningún broker pub/sub aparte, ningún APM solo para ver queries lentos.

Ese es el argumento real detrás del 'just use Postgres', despojado del hype: menos cosas que monitorear, menos cosas que actualizar, menos cosas que te paguen a las 3 de la mañana. La escalera te dice dónde trazar la línea. La caja de herramientas te dice cuánto terreno cubres antes de llegar ahí.

Referencias

PostgreSQLDatabasesBackendTools