Volver al Blog
Databases & Backend2 de octubre de 202511 min de lectura

La escalera de adopción de Postgres: qué usa de verdad un senior antes de pedir Redis

DR

Doug Ribeiro

11 min de lectura

Últimamente circula un género de artículo que te promete reemplazar toda tu stack con Postgres. Cargarte Redis, cargarte Kafka, cargarte Elasticsearch, cargarte tu base vectorial, todo. Just use Postgres.

Aciertan con la lista de features y fallan con lo que cuesta operarlo. Técnicamente, cada afirmación de esos posts es cierta. JSONB con índices GIN sí te da una flexibilidad tipo MongoDB. FOR UPDATE SKIP LOCKED sí te arma una cola. pg_trgm sí te resuelve búsquedas difusas. pgvector sí hace búsqueda ANN. Lo que no te cuentan es dónde revienta cada feature, cuánto te sale en términos operativos, y cuándo la herramienta especializada que ibas a eliminar vuelve a parecer barata.

Esta es la versión que me hubiera gustado que alguien escribiera para mí: una escalera concreta con números, no un tour de features. Cuatro niveles, desde 'adoptar sin pensarlo' hasta 'no lo hagas'. Cada item trae un umbral aproximado que te indica cuándo parar.

Nivel 1: Adoptar sin dudarJSONB+GIN, pg_trgm, vistas materializadas, pgvector <5M, BRIN + partici… 1 Nivel 2: Adoptar con disciplina operativaColas SKIP LOCKED (tuning de VACUUM), full-text search (<1M docs), PgBo… 2 Nivel 3: Pensarlo dos vecesLISTEN/NOTIFY a alto throughput, RLS como única capa de auth, PostgREST… 3 Nivel 4: No lo hagas100M+ vectores, millones de eventos WS/seg, miles de listeners, escritu… 4

Nivel 1: Adoptar sin dudar

Piezas de Postgres que cargan peso y que cualquier senior debería elegir desde el día uno. Estables, baratas, sin deuda operativa.

JSONB con índices GIN

Aquí viven los datos que son estructurados pero a la vez flexibles. ¿Un perfil de usuario con un schema predecible y un blob de preferencias encima? Una tabla, dos columnas, un índice GIN sobre el blob. Te quedas con joins, ACID y schema-on-read en el mismo sitio. La queja de siempre es el overhead de almacenamiento (JSONB guarda los nombres de campos en cada fila), pero eso empieza a doler a cientos de millones de documentos, no a la escala en la que estás antes de una Serie B.

pg_trgm para búsqueda difusa de texto

Los índices de trigramas resuelven en unas cinco líneas de SQL el clásico 'barra de búsqueda que tolera errores de tipeo'. Cubre el 90% de los casos. Si estás levantando Elasticsearch solo para alimentar un typeahead sobre 200k productos, te estás sobreingenierizando.

Vistas materializadas con REFRESH CONCURRENTLY

Si el query de tu dashboard agrega sobre un millón de filas y tarda seis segundos, la respuesta no es firmar un contrato con Snowflake. Es una vista materializada que se refresca cada cinco minutos. Con un índice único le haces REFRESH CONCURRENTLY, que calcula en segundo plano y cambia las filas sin bloquear a los lectores.

pgvector por debajo de 5M de vectores

El propio benchmark de 2024 de Timescale (hecho por el vendor, 50M de embeddings con 99% de recall) mostró a pgvectorscale (una extensión aparte de Tiger Data construida encima de pgvector, no es pgvector vanilla) superando al índice s1 pod storage-optimized de Pinecone: 28ms p95 contra 784ms p95. Por debajo de 5M de vectores, con índices HNSW, pgvector no es un compromiso, es la decisión correcta. Y lo es todavía más cuando necesitas filtros híbridos ('similitud semántica pero solo documentos de este tenant en la última semana'). Justamente ahí, las bases vectoriales especializadas siguen sufriendo.

Índices BRIN más particionamiento declarativo para series temporales

Si tus datos son aproximadamente monotónicos (logs, eventos, métricas) y particionas por día o semana, BRIN te da un índice de uno a tres órdenes de magnitud más pequeño que un B-tree sobre los mismos datos. Dónde caes dentro de ese rango depende por completo de qué tan de cerca el orden físico de las filas sigue a la columna indexada: una correlación casi perfecta te pone en el tope del rango, una tabla desordenada te pone en el piso y convierte a BRIN en el índice equivocado. Los range scans siguen siendo rápidos. El disco sigue siendo barato.

PostGIS para cualquier cosa geoespacial

PostGIS no es la alternativa a los sistemas GIS comerciales, es el estándar contra el que suelen perder. Si vas a enviar algo que toque coordenadas, arranca aquí y deja de darle vueltas.

Nivel 2: Adoptar con disciplina operativa desde el día uno

Funcionan, pero no puedes dormirte. Si las despliegas sin la disciplina que traen atrás, lo vas a aprender por las malas.

Colas con FOR UPDATE SKIP LOCKED

Es la feature SQL más limpia de Postgres. Convierte una tabla normal en una cola de trabajo concurrente. La pega vive en otra capa: el churn. Una cola con throughput alto implica INSERT, UPDATE y DELETE constantes sobre la misma tabla, lo que implica tuples muertos, lo que implica presión sobre VACUUM. Si autovacuum no da abasto, el planner empieza a elegir malos planes, tu 'job grab instantáneo' pasa a ser un seq scan de 400ms, y tus workers parecen rotos.

Ajusta autovacuum tabla por tabla (bajar autovacuum_vacuum_scale_factor del 0.2 por defecto a 0.02 en tablas de cola es el estándar de la industria, lo que dispara vacuum al 2% de tuples muertos en lugar del 20%), mantén las transacciones cortas, monitorea el ratio de tuples muertos. No es opcional, es parte de adoptar el patrón.

Full-text search con tsvector

Funciona de maravilla por debajo del millón de documentos y en un solo idioma. Por encima empiezas a sentir para qué sirve realmente Elasticsearch: scoring BM25 que puedes ajustar, analizadores multilingües de verdad, búsqueda facetada, fuzzy matching más allá de trigramas. El umbral de graduación ronda el rango de 1M a 5M de registros, o la primera vez que un product manager te pide 'relevancia como la de Algolia'.

PgBouncer delante de todo

Postgres es un proceso por conexión. Cada conexión son 5MB de proceso del sistema operativo. El max_connections por defecto es 100, y el rendimiento se degrada a partir de las 200 a 500. En producción vas a pasarte de ese número más rápido de lo que crees, sobre todo con frameworks modernos que abren conexiones perezosas en todos lados. PgBouncer no es una herramienta 'para después', es parte de la stack base.

PgBouncer tiene tres modos de pooling: session (una conexión de servidor por cliente, el más seguro pero el pool se llena rápido), transaction (la conexión de servidor vuelve al pool cuando cada transacción hace commit, la opción habitual para la mayoría de las cargas) y statement (se libera después de cada sentencia, rompe transacciones, casi nunca es la decisión correcta). Usa transaction pooling, tamaño de pool de 20 a 30 por instancia de app, y vigila la cola de espera. Un detalle importante: algunas features de Postgres dejan de funcionar bien en transaction pooling: LISTEN/NOTIFY, advisory locks con scope de sesión, PREPARE a nivel SQL. (Los prepared statements a nivel de protocolo están soportados desde PgBouncer 1.21 y vienen activados por defecto desde la 1.24.) Ten claro qué pierdes antes de elegir el modo.

Nivel 3: Pensarlo dos veces

Features que se ven geniales en los blog posts y que terminan sosteniendo peso de formas que vas a lamentar.

LISTEN/NOTIFY para pub/sub de alto throughput

Suena gratis porque viene integrado. Esta es la parte que el tour de features se salta: NOTIFY toma un lock exclusivo pesado justo antes del commit y lo mantiene hasta que termina la transacción, así que las transacciones que emiten un NOTIFY hacen commit de a una. Un UPDATE común que no notifica a nadie nunca toca ese lock. Suena acotado hasta que el notify queda en tu camino de escritura más caliente, y tu throughput de escritura queda limitado por una única cola serializada. Los mensajes no se persisten, el payload tiene un tope de 8KB, y todo esto no funciona con transaction pooling. Úsalo para dashboards internos, paneles de admin, señales de invalidación de baja frecuencia. No lo uses como sistema nervioso de un producto de alto tráfico.

Row-Level Security como única capa de autorización

RLS es potente, y para apps multi-tenant es genuinamente una buena herramienta. La trampa es de rendimiento: cada query que ve el planner lleva tu policy de RLS inlineada, y si esa policy toca una columna sin índice o una subquery, un simple SELECT puede terminar leyéndose como un join de 14 tablas. Usa RLS como la red de seguridad y mantén de todos modos checks a nivel de aplicación en los caminos críticos. Trata a RLS con el mismo respeto que le darías a un trigger de base de datos: útil, invisible y capaz de sorprenderte.

PostgREST o pg_graphql como backend entero

Las APIs autogeneradas sobre tu schema son una maravilla para prototipar. Cubren CRUD de forma limpia. En el momento en que entra lógica de negocio de verdad (workflows de varios pasos, llamadas a APIs externas, reglas que pasan por code review), ya estás escribiendo funciones en PL/pgSQL. PL/pgSQL no tiene stack traces modernas, su tooling es débil, su disciplina git por defecto es mala, y no hay language server que valga la pena. El discurso de 'elimina el backend' termina siendo 'toda tu lógica de negocio vive en archivos .sql sueltos que nadie recuerda haber escrito'.

Nivel 4: No lo hagas

Hay cargas que no son trabajo para una sola instancia de Postgres, por más extensiones que le metas encima.

100M+ vectores

Por encima de ese rango, las bases vectoriales dedicadas (Qdrant, Milvus, Weaviate) ganan de manera significativa en recall a una latencia dada y en uso de memoria. Si tu sistema de RAG pasa ese umbral, la especialización ya se ganó su lugar.

Millones de eventos websocket por segundo

Para eso existen Redis Streams, Kafka y NATS. Postgres no está hecho para fan-out de alto throughput a esa tasa, da igual que el backing store sea memoria (Redis) o un log durable (Kafka).

Pub/sub con miles de listeners concurrentes

En la práctica, LISTEN/NOTIFY topa cerca de los 1000 listeners, y se degrada bastante antes de llegar ahí bajo carga de escritura. Cerca de 100 listeners es donde se mantiene aburrido, y ese es el número para el que hay que diseñar. Los mil son el muro, no el objetivo.

Escrituras activo-activo multi-región

Postgres tiene un solo primario. Existe la replicación lógica y existen soluciones activo-activo, pero todas mueven los problemas difíciles (resolución de conflictos, amplificación de escrituras) a tu aplicación. Si de verdad necesitas escribir en tres regiones, la base de datos no es la capa que conviene mantener simple.

El modelo mental

La escalera no intenta decirte que 'Postgres no alcanza'. Lo que dice es que Postgres casi siempre alcanza, y por más tiempo del que piensas, si lo adoptas con la disciplina operativa que espera. La mayoría de las empresas que meten un Redis, un Kafka, un Elasticsearch y una base vectorial en su primer año están pagando por complejidad que todavía no se han ganado.

Como guía personal: empiezo siempre en Nivel 1 sin discutir, promuevo al Nivel 2 solo cuando tengo la madurez de ops para sostenerlo, trato al Nivel 3 como una decisión deliberada con plan de graduación, y reconozco al Nivel 4 como 'otra herramienta', no como 'Postgres más grande'.

La respuesta poco sexy a '¿qué uso?' casi siempre es 'Postgres, con cuidado'. Los posts que dicen 'just use Postgres' aciertan con la dirección y se equivocan con la facilidad.

Referencias

PostgreSQLDatabasesBackendArchitecture