072 — Persistencia políglota: decidir por evidencia y no por moda
Parte 14 — Arquitectura y proyecto final · Avanzado ·
3 horas estimadas · motores postgresql, mongodb, redis, qdrant · laboratorio
labs/02-polyglot-modeling · 3 fuentes.
Conceptos centrales: carga de trabajo · criterio de selección · costo de operación · complejidad añadida
En este caso se comparan 7 motores: 5 lo resuelven (0 con el resultado comprobado por máquina) y 2 no, con el motivo escrito.
De qué trata esta clase
La elección de motores hecha como decisión técnica: carga de trabajo cuantificada, criterio de comparación escrito antes de mirar los productos, y la complejidad añadida contada como lo que cuesta —otro modelo de fallo, otro respaldo, otra guardia. Es la clase que convierte las catorce partes anteriores en un método de decisión.
flowchart LR
C["🗄️ Clase 072"]
C --> K1["carga de trabajo"]
C --> K2["criterio de selección"]
C --> K3["costo de operación"]
C --> K4["complejidad añadida"]
classDef raiz fill:#0b3d2e,stroke:#3fb950,color:#fff
class C raiz
Antes de empezar
Esta clase supone que ya trabajaste lo siguiente. Si algo de la última columna no te suena, vuelve a esa clase antes de seguir: aquí se usa sin volver a explicarlo.
| # | Clase previa | Lo que se da por sabido |
|---|---|---|
| 010 | El mapa de los motores: seis familias y un criterio | familias de motores · modelo de agregado · patrón de acceso · multimodelo |
| 064 | OLTP frente a OLAP: por qué se separan | carga transaccional · carga analítica · contención · formato de almacenamiento |
Vocabulario de la clase
Los términos que siguen se usan más adelante con este significado exacto. La definición completa, con sus términos relacionados, está en el glosario del programa.
| Término | Qué significa | Procedencia |
|---|---|---|
| carga de trabajo | La descripción cuantificada de lo que el sistema tendrá que aguantar: volumen, proporción de lecturas y escrituras, latencia objetivo, consultas dominantes, crecimiento previsto. Es lo que convierte la elección de motor en una decisión técnica y no en una preferencia. | se introduce aquí |
| criterio de selección | La lista escrita de propiedades que se van a comparar entre candidatos, con su peso, fijada antes de mirar los productos. Escribirla después es escribir la justificación de lo que ya se había decidido. | se introduce aquí |
| costo de operación | Todo lo que cuesta mantener vivo un motor después de instalarlo: respaldos probados, actualizaciones, monitorización, personas de guardia. Suele superar con creces el costo de licencia o de cómputo. | se introdujo en la 009 |
| complejidad añadida | Lo que cuesta cada sistema adicional: otro modelo de fallo, otro respaldo, otra guardia, otra consistencia que reconciliar. Es el argumento más fuerte a favor de un solo motor multimodelo mientras la carga lo permita. | se introduce aquí |
Propósito
Decidir cuántos almacenes de datos usar. Cada motor añadido resuelve un problema y crea varios permanentes; el criterio debe ser una medición, no una tendencia.
Resultados de aprendizaje
Al terminar podrás:
- Caracterizar una carga de trabajo con números antes de elegir motor.
- Enumerar el costo permanente de añadir un almacén.
- Aplicar la regla de agotar primero las capacidades del motor existente.
- Diseñar la sincronización entre almacenes y su verificación.
- Defender una arquitectura de un solo motor cuando corresponda.
Fundamentos
Caracterizar antes de elegir
Sadalage y Fowler acuñaron «persistencia poliglota»: usar el almacén adecuado a cada carga. La parte que se cita menos es que cada almacén adicional es un sistema completo que operar.
La caracterización necesaria, con números reales:
| Dimensión | Pregunta |
|---|---|
| Volumen | ¿Cuántos datos hoy y en 3 años? |
| Caudal | Lecturas/s y escrituras/s, con su pico |
| Patrón de acceso | ¿Por clave, por rango, agregado, recorrido, similitud? |
| Latencia exigida | p50 y p99, por operación |
| Consistencia | ¿Qué invariantes deben ser atómicas? |
| Ciclo de vida | ¿Retención, expiración, histórico? |
| Consultas no previstas | ¿Con qué frecuencia aparecen? |
Sin estas cifras, la elección de motor es una preferencia.
El costo permanente
Añadir un almacén cuesta, para siempre:
| Costo | Detalle |
|---|---|
| Operación | Actualizar, parchear, dimensionar, vigilar |
| Copias | Otro plan de respaldo y otra prueba de restauración (clase 048) |
| Seguridad | Otro modelo de control de acceso, otras credenciales |
| Sincronización | Un canal que puede desincronizarse (clase 056) |
| Consistencia | Invariantes que cruzan almacenes y ya no son atómicas (clase 047) |
| Conocimiento | Alguien debe saber operarlo; y su suplente también |
| Guardia | Otra fuente de incidentes de madrugada |
Regla del programa: un almacén nuevo debe resolver un problema medido que el actual no puede resolver, no un problema anticipado.
Agotar primero lo que ya se tiene
PostgreSQL cubre por sí solo una superficie sorprendente:
| Necesidad | Motor «natural» | PostgreSQL |
|---|---|---|
| Documentos | MongoDB | jsonb + GIN (clase 021) |
| Clave-valor | Redis | UNLOGGED + índice hash |
| Búsqueda de texto | OpenSearch | tsvector + GIN (clase 031) |
| Vectores | Qdrant | pgvector (clase 059) |
| Series temporales | InfluxDB | TimescaleDB (clase 030) |
| Cola de trabajos | RabbitMQ | SELECT ... FOR UPDATE SKIP LOCKED |
| Analítica | ClickHouse | Réplica + agregados, o Parquet + DuckDB |
| Grafos | Neo4j | CTE recursiva (clase 028) |
Ninguna de estas alternativas es la mejor en su especialidad. Todas son suficientes hasta cierto punto, y ese punto está mucho más lejos de lo que se supone. La pregunta correcta no es «¿cuál es el mejor motor para X?», sino «¿ha dejado de bastarme el que ya opero?».
flowchart TD
C["Carga caracterizada<br/>con números"] --> A{"¿El motor actual<br/>la sostiene?"}
A -- "Sí" --> OK["No añadir nada"]
A -- "No" --> B{"¿Con una extensión<br/>o un cambio de diseño?"}
B -- "Sí" --> EXT["Extensión · índice ·<br/>modelo (medir de nuevo)"]
B -- "No" --> D{"¿El costo permanente<br/>es menor que la ganancia<br/>MEDIDA?"}
D -- "No" --> OK
D -- "Sí" --> E["Añadir almacén<br/>+ canal + verificación<br/>+ copias + guardia"]
E --> F["Registrar la decisión<br/>y su criterio de reversión"]
Ejemplo trabajado
Plataforma educativa. Caracterización real de sus cinco cargas:
| # | Carga | Volumen | Caudal | Patrón | Latencia | Consistencia |
|---|---|---|---|---|---|---|
| 1 | Inscripciones y notas | 5 M filas | 300 l/s, 5 e/s | Relacional, reuniones | p99 < 50 ms | Transaccional |
| 2 | Sesiones | 50 k activas | 2 000 l/s, 200 e/s | Clave-valor, TTL | p99 < 5 ms | Se puede perder |
| 3 | Búsqueda de contenido | 200 k fragmentos | 50 c/s | Léxica + semántica | p99 < 200 ms | Desfase de minutos |
| 4 | Panel de dirección | 5 M filas | 12 c/día | Agregación completa | < 30 s | Desfase de horas |
| 5 | Telemetría de uso | 200 M eventos/año | 3 000 e/s | Escritura, ventanas | escritura < 10 ms | Se puede perder |
Análisis carga por carga:
1. PostgreSQL. No hay discusión: transacciones, integridad, reuniones.
2. Sesiones. ¿Redis? Primero la medición sobre PostgreSQL:
CREATE UNLOGGED TABLE sesiones ( -- UNLOGGED: sin WAL, mucho más rápido, se pierde al caer
token TEXT PRIMARY KEY,
student_id INTEGER NOT NULL,
expira_en TIMESTAMPTZ NOT NULL
);
CREATE INDEX ON sesiones (expira_en);
Medición: 2 000 lecturas/s por clave primaria → p99 = 1,8 ms
Exigencia: p99 < 5 ms ✔
Veredicto: NO añadir Redis. PostgreSQL cumple con margen.
UNLOGGED es la clave: sin WAL, y perder las sesiones al caer el servidor ya se había declarado aceptable.
3. Búsqueda. Léxica con tsvector y semántica con pgvector, sobre 200 000 fragmentos:
Medición: híbrida RRF (clase 060) → p99 = 84 ms, recall@5 = 0,89
Exigencia: p99 < 200 ms ✔
Veredicto: NO añadir OpenSearch ni Qdrant.
Revisión: si el corpus llega a 5 M de fragmentos, volver a medir.
La última línea es tan importante como el veredicto: la decisión lleva su condición de revisión escrita.
4. Panel. Ejecutado sobre el primario degradaba el OLTP (clase 054):
Opción A: réplica de solo lectura → 47 s, sin impacto en OLTP ✔
Opción B: exportación a Parquet+DuckDB → 1,2 s, sin servidor nuevo ✔✔
Veredicto: opción B. Coste: un trabajo programado.
5. Telemetría. 3 000 escrituras/s, 200 M de eventos al año:
Medición sobre PostgreSQL con TimescaleDB:
ingesta sostenida: 3 000/s ✔
almacenamiento con compresión y retención por niveles (clase 030): 34 GB/año ✔
consulta de ventana con agregado continuo: 180 ms ✔
Veredicto: TimescaleDB, que es una extensión del motor que YA se opera.
Coste marginal: una extensión, no un sistema.
Arquitectura resultante:
PostgreSQL 16
+ TimescaleDB (telemetría)
+ pgvector (búsqueda semántica)
+ tsvector/GIN (búsqueda léxica)
+ tabla UNLOGGED (sesiones)
+ réplica de solo lectura
+ exportación nocturna a Parquet, consultada con DuckDB
Almacenes que operar: UNO.
Frente a la arquitectura «de manual» —PostgreSQL + Redis + OpenSearch + Qdrant + ClickHouse + InfluxDB, seis sistemas—, esta cumple todas las exigencias medidas con uno.
Y cuándo dejaría de bastar, escrito por adelantado:
| Umbral | Motor a añadir |
|---|---|
| Sesiones > 20 000 lecturas/s | Redis |
| Corpus > 5 M fragmentos o recall < 0,85 | Qdrant |
| Telemetría > 50 000 escrituras/s | ClickHouse |
| Panel sobre > 500 GB o varias fuentes | Almacén columnar |
Este es el entregable de la clase: no una arquitectura, sino una arquitectura con sus condiciones de cambio. Cada umbral es medible y cada uno tiene una alerta.
Comparación
| Enfoque | Sistemas | Coste operativo | Rendimiento por carga | Riesgo |
|---|---|---|---|---|
| Un motor con extensiones | 1 | Bajo | Bueno en todo, óptimo en nada | Techo por carga |
| Poliglota por evidencia | 2–3 | Medio | Óptimo donde se midió | Sincronización |
| Poliglota por tendencia | 5+ | Alto | Óptimo y desaprovechado | Divergencia, guardia, conocimiento |
Errores frecuentes
- Elegir motor por la arquitectura de otra empresa. Sus cifras no son las tuyas.
- No caracterizar la carga. Sin números, la elección es estética.
- Ignorar el costo permanente. Se contabiliza el desarrollo y no la operación.
- Añadir un almacén sin canal verificado. Divergencia silenciosa.
- Suponer que PostgreSQL no sirve. Suele servir mucho más allá de lo que se cree.
- No escribir la condición de revisión. La decisión se vuelve dogma.
- Optimizar para un volumen que no se tiene. Complejidad hoy por un problema hipotético.
De la clase a la operación
Cada almacén añadido multiplica los estados posibles del sistema, y por tanto los modos de fallo. Un equipo pequeño con seis almacenes no los opera bien: los tiene. La arquitectura defendible es la que el equipo puede sostener a las tres de la mañana.
Reto de transferencia
- Caracteriza las cinco cargas principales de tu sistema con números reales.
- Mide cada una contra tu motor actual, con las extensiones disponibles.
- Justifica cada almacén adicional con la medición que lo hace necesario.
- Escribe los umbrales de revisión y configura una alerta para cada uno.
Preguntas de evaluación
- Enumera el costo permanente de añadir un almacén a tu sistema.
- Da una carga tuya que hoy usa un motor especializado y podría volver al principal.
- ¿Qué medirías antes de añadir un motor de búsqueda dedicado?
- Escribe el umbral de revisión de una de tus decisiones y cómo lo vigilarías.
🌐 El mismo problema en cada motor
Caso: Un sistema real con cinco cargas distintas, y la pregunta de si hacen falta cinco motores
Una plataforma educativa tiene cinco cargas: inscripciones (transaccional, con invariantes que no se pueden romper), sesiones (clave-valor, alta frecuencia, desechable), búsqueda del catálogo (texto, con relevancia), panel de dirección (analítico, sobre todo el histórico) y recomendación semántica (vectorial).
La respuesta fácil es un motor por carga. Es casi siempre la respuesta equivocada, porque el costo de la persistencia políglota no está en licencias ni en servidores: está en la coherencia entre sistemas, que ninguna transacción cubre, y en las cinco formas distintas de respaldar, monitorizar, actualizar y contratar personal.
La regla que esta parte defiende: un motor entra en la arquitectura cuando hay evidencia medida de que el anterior no puede, y esa evidencia se anota en un registro de decisión. Aquí se compara qué carga justifica de verdad a cada motor y a partir de qué umbral.
Esta comparación es conceptual: la decisión no se reduce a una consulta con resultado, así que aquí no hay sello de máquina. Lo que se compara es lo que cada motor ofrece y a qué precio, con la página oficial al lado de cada afirmación.
| Motor | ¿Resuelve el caso? | Nivel de prueba | Código | Fuente |
|---|---|---|---|---|
| PostgreSQL | sí | conceptual | — | doc oficial |
| Redis | sí | conceptual | — | doc oficial |
| OpenSearch | sí | conceptual | — | doc oficial |
| DuckDB | sí | conceptual | — | doc oficial |
| Qdrant | sí | conceptual | — | doc oficial |
| MongoDB | no | — | — | doc oficial |
| Apache Cassandra | no | — | — | doc oficial |
Los que resuelven el caso
PostgreSQL
- Cómo se hace aquí: La opción por omisión, y con frecuencia la única necesaria: transaccional para inscripciones,
jsonbpara lo semiestructurado, búsqueda de texto contsvector, analítica de tamaño medio con vistas materializadas, y vectorial con pgvector. Cuatro de las cinco cargas, en un sistema, en una transacción. - Por qué sí: Cada sistema que no entra en la arquitectura ahorra un plan de respaldo, un panel, un ciclo de actualizaciones y una coherencia que mantener. Empezar aquí y salir con evidencia es una decisión defendible; empezar repartido, casi nunca.
- Por qué no: Ninguna de esas cuatro capacidades es la mejor de su categoría, y llega un punto en que se nota: la búsqueda sin sinónimos ni corrección de errores, el panel compitiendo por la caché con las inscripciones, el índice vectorial con millones de vectores. El error simétrico —quedarse aquí después de que la evidencia diga lo contrario— también existe.
- 📄 Documentación oficial: https://www.postgresql.org/docs/current/
Redis
- Cómo se hace aquí: La carga de sesiones: alta frecuencia, latencia de microsegundos, caducidad automática y datos que se pueden perder sin consecuencias.
- Por qué sí: Es la separación más fácil de justificar de las cinco, porque el dato es desechable por naturaleza: no hay coherencia que mantener con la verdad, solo una caché que se puede reconstruir.
- Por qué no: La frontera se cruza sola. En cuanto alguien guarda ahí algo que no se puede perder —un carrito, un contador de facturación—, el sistema pasa a tener dos verdades y ninguna transacción que las una.
- 📄 Documentación oficial: https://redis.io/docs/latest/develop/
OpenSearch
- Cómo se hace aquí: La carga de búsqueda del catálogo, cuando el buscador deja de ser una caja de texto y pasa a ser el producto: sinónimos, corrección de errores, facetas, resaltado y relevancia ajustable.
- Por qué sí: El umbral es claro y medible: cuando
ts_rankde PostgreSQL deja de dar resultados aceptables en el conjunto de evaluación, hay evidencia. Y esa medición es la de la clase 061. - Por qué no: Es un índice secundario que va por detrás del origen: hay que alimentarlo, reindexarlo cuando cambia el analizador, y aceptar que puede mostrar algo que ya no existe. Añade un sistema entero con su propia operación.
- 📄 Documentación oficial: https://docs.opensearch.org/latest/
DuckDB
- Cómo se hace aquí: La carga del panel de dirección, sobre una copia exportada del histórico. Sin servidor y sin competir por los recursos del sistema transaccional.
- Por qué sí: Es la separación más barata que existe: no añade un servicio, solo un fichero y un proceso de exportación. Resuelve el conflicto entre analítica y transaccional sin montar un almacén de datos.
- Por qué no: El panel va con el retraso de la última exportación, y eso hay que declararlo en el propio panel. Cuando el negocio exige el dato al minuto, esta opción se acaba.
- 📄 Documentación oficial: https://duckdb.org/docs/stable/
Qdrant
- Cómo se hace aquí: La carga vectorial, cuando el número de vectores o la exigencia de filtrado superan lo que pgvector sostiene con el recall requerido.
- Por qué sí: El umbral vuelve a ser medible: recall y latencia sobre el mismo conjunto de evaluación, comparados contra pgvector. Si pgvector cumple, no hay decisión que tomar.
- Por qué no: Los vectores viven separados de los datos de negocio, sin transacción común: un documento borrado puede seguir apareciendo en los resultados hasta que alguien sincronice. Esa incoherencia hay que diseñarla, no descubrirla.
- 📄 Documentación oficial: https://qdrant.tech/documentation/
Los que no resuelven este caso — y qué se hace en su lugar
Descartar un motor con un argumento es tan formativo como usarlo. Ninguna de estas filas dice que el motor sea peor: dice que este problema no es el suyo.
| Motor | Por qué no | Qué se hace en su lugar | Fuente |
|---|---|---|---|
| MongoDB | En este sistema no hay ninguna carga que lo justifique frente a PostgreSQL: lo semiestructurado cabe en jsonb con índices GIN y con las transacciones y las claves foráneas del resto del esquema al lado. Añadirlo sería sumar un sistema sin una carga que solo él resuelva. |
Se justifica cuando el modelo documental es el dominio —agregados grandes que se leen y escriben enteros y no se relacionan entre sí— o cuando el esquema tiene que evolucionar sin migraciones coordinadas. Esa es la evidencia que hay que aportar, no la preferencia. | doc |
| Apache Cassandra | Su modelo cuesta lo que cuesta —una tabla por consulta, sin reuniones, sin transacciones, con reparaciones periódicas— y solo se paga cuando el volumen de escritura supera lo que un nodo primario puede absorber. Una plataforma educativa no llega ahí, y adoptarlo «por si crecemos» es pagar el costo sin recibir el beneficio. | Particionado dentro de PostgreSQL y réplicas de lectura, que cubren un crecimiento de dos órdenes de magnitud sin cambiar de modelo de datos ni de forma de razonar. | doc |
Laboratorio
python scripts/validate_repository.py
# labs/02-polyglot-modeling se entrega escrito: no hay guion que ejecutar
Guarda como evidencia la salida completa, la versión del motor y la semilla o los parámetros usados. Una captura sin comando no es evidencia: no se puede repetir.
Evaluación
| Criterio | Peso | Qué se comprueba |
|---|---|---|
| Comprensión conceptual | 25 % | Explica el mecanismo, no solo el resultado |
| Ejecución reproducible | 25 % | Otra persona obtiene lo mismo con las instrucciones dadas |
| Interpretación basada en evidencia | 25 % | Cada conclusión se apoya en una salida o una medición |
| Límites y riesgos declarados | 25 % | Dice qué no demuestra el ejercicio y qué faltaría en producción |
La clase se da por superada cuando la respuesta explica el mecanismo, muestra la salida que la respalda y declara al menos un límite del ejercicio.
Fuentes de esta clase
Todo lo afirmado arriba procede de estas obras. Los identificadores viven en
catalog/sources.json y el estado de los
enlaces se comprueba con python scripts/check_external_links.py.
- Pramod J. Sadalage, Martin Fowler (2012). NoSQL Distilled: A Brief Guide to the Emerging World of Polyglot Persistence. Addison-Wesley. ISBN 978-0-321-82662-6.
Origen del término agregado y de la persistencia políglota que estructura este programa. - Martin Kleppmann (2017). Designing Data-Intensive Applications. O'Reilly. ISBN 978-1-4493-7332-0.
Referencia central del programa para replicación, partición, transacciones distribuidas y streaming. - Peter Bailis, Joseph M. Hellerstein, Michael Stonebraker (2015). Readings in Database Systems. 5.a ed. MIT Press. ISBN 978-0-262-52964-3.
Antologia comentada de acceso libre. Cada capitulo situa los papers en su discusión.