3 puntos por GN⁺ 4 시간 전 | 1 comentarios | Compartir por WhatsApp
  • Basado en los problemas que Hatchet enfrentó durante 2 años en producción, resume principios operativos paso a paso, desde el diseño inicial de esquemas y consultas hasta escrituras masivas y migraciones de tablas.
  • Para lecturas rápidas, hay que alinear los índices con ORDER BY, pero como el planificador de consultas puede elegir un escaneo secuencial según estadísticas y costos, se debe comparar la estimación con la ejecución real usando EXPLAIN ANALYZE.
  • El rendimiento y la estabilidad de escritura dependen de transacciones cortas, bloquear solo las filas necesarias, CREATE INDEX CONCURRENTLY y connection pooling; en las mediciones de Hatchet, el procesamiento por lotes aumentó el throughput unas 10 veces.
  • En entornos de escritura de alta frecuencia, la configuración predeterminada de autovacuum puede no recuperar a tiempo dead tuples ni transaction IDs, y si se llega a transaction ID wraparound puede producirse un downtime importante.
  • A medida que crece la escala, conviene usar colas de trabajo basadas en FOR UPDATE SKIP LOCKED, particionamiento, triggers y backfill por lotes, pero es necesario poder controlar SQL directamente fuera de la abstracción del ORM.

Público objetivo y límites del ORM

  • Es una guía pensada para desarrolladores que conocen los conceptos básicos de SQL, filas, tablas e índices y necesitan responder a problemas de Postgres en producción.
  • El manual de Postgres es amplio, pero no es fácil consultarlo rápido durante incidentes, así que se condensa alrededor de la experiencia operativa que Hatchet acumuló durante 2 años.
  • Aunque se use un ORM, los principios aplican igual, pero a medida que aumenta la escala hay muchas optimizaciones que solo son posibles si se escribe SQL directamente fuera de la capa de abstracción.
    • Con funciones como Prisma TypedSQL, se puede usar el ORM junto con SQL directo.
    • Hatchet, basado en Go, usa sqlc, que ofrece un comportamiento similar.
    • Para entornos donde Claude escribe consultas, recomiendan supabase/agent-skills.

Diseño de esquema difícil de cambiar

  • Después del despliegue, lo más difícil es cambiar el esquema, así que conviene crear un borrador de tablas y claves primarias, escribir las consultas que necesita la aplicación e iterar el diseño.
  • Durante el diseño, se revisa cómo se usarán las tablas con preguntas como estas:
    • Qué ocurre con más frecuencia: lecturas o escrituras
    • Cuáles son los filtros más usados al leer
    • Cuáles son las columnas que más se actualizan
  • Se pueden aplicar 1NF, 2NF y 3NF de la normalización de bases de datos, pero a veces las formas normales chocan con la eficiencia de las consultas o con la facilidad de uso necesaria para desarrollar más rápido.
    • En algunos casos, es más simple poner los datos en una columna jsonb.
  • Estas son las reglas prácticas que aplican al diseñar esquemas:
    • Usar enteros autoincrementales de tipo identity o UUID nativo de Postgres para las claves primarias.
    • Las columnas identity son un poco más rápidas que bigserial.
    • Usar siempre timestamptz para fechas y horas.
    • Poner una clave primaria en todas las tablas.
    • Usar claves foráneas, incluyendo cascade delete, en tablas pequeñas donde la consistencia y la precisión son importantes, pero con cuidado en entornos de alto volumen.

Consultas de lectura e índices

  • Un modelo simple para entender SELECT rápidos es que Postgres puede encontrar una fila rápido con un índice o leer todas las filas de una tabla con un escaneo secuencial (seq scan).
  • Para buscar una sola fila rápidamente, se usan estas estructuras:
    • Un índice explícito
    • Un unique constraint, que es una forma especial de índice
    • La clave primaria, que Postgres indexa automáticamente
  • El índice básico usa btree, y puede entenderse como una tabla separada que almacena los datos en una forma optimizada para búsqueda.
    • El tiempo de búsqueda de filas es aproximadamente log(n), donde n es la cantidad de filas de la tabla.
  • Si no puede usarse un índice, se ejecuta un escaneo secuencial, pero las bases de datos modernas cargan filas en memoria muy rápido, así que en tablas con menos de 20 mil filas esto puede terminar casi de inmediato.

Joins e índices compuestos

  • En general, el lado objetivo de un inner join debería usar una clave primaria; si no, puede haber un problema en el diseño del esquema o en la normalización.
  • La cláusula ON debe tratarse como la cláusula WHERE, y hay que usar el índice adecuado para la condición del join.
  • Las consultas de listado sobre tablas grandes suelen ser las primeras que se vuelven lentas en una aplicación.
    • Si se filtra y ordena a la vez por organización y fecha de creación, puede usarse un índice compuesto.
CREATE INDEX CONCURRENTLY idx_documents_org_created
    ON documents (organization_id, created_at DESC);
  • En consultas complejas, una regla práctica es poner la columna de ORDER BY al final del índice y hacer coincidir también la dirección del ordenamiento.
    • Como Postgres puede escanear btree en ambos sentidos, en una sola columna DESC puede no tener importancia, pero en índices compuestos conviene alinearlo.
    • El funcionamiento detallado de los índices descendentes puede revisarse en este material relacionado.

Escrituras, bloqueos y migraciones

  • La primera condición para escrituras exitosas es mantener las transacciones cortas.
    • Salvo que haya una razón especial, no se consulta un servicio externo durante una transacción.
  • La segunda condición es bloquear solo las filas necesarias.
    • Cuando se actualiza una fila, queda bloqueada hasta que la transacción haga commit.
    • A medida que crece la carga del sistema, el impacto de los bloqueos se vuelve más visible.
  • Si se ejecuta CREATE INDEX normal sobre una tabla grande existente, la tabla queda bloqueada y se frenan insert y update, así que siempre se usa CREATE INDEX CONCURRENTLY.
  • Una buena capacidad de migración de esquemas acelera el desarrollo iterativo y aumenta el uptime.
    • En lo posible, evitar eliminar columnas y preferir cambios aditivos.
    • Cuando se pueda, ejecutar dentro de una transacción para poder manejar rollback y aplicación parcial.
    • Como enfoque más avanzado, se pueden usar migraciones expand and contract.
  • Primero hay que evaluar si una migración bloqueará todas las escrituras.
    • Crear un índice sin CONCURRENTLY puede bloquear todas las escrituras y provocar downtime.
    • Las operaciones ALTER TABLE deben revisarse otra vez, y agregar un check constraint a una tabla grande también puede bloquear escrituras.
    • Si se agrega el check constraint como NOT VALID, se puede evitar ese bloqueo.

Gestión de conexiones

  • Todas las consultas y transacciones usan una conexión a la base de datos, y como las conexiones tienen alto costo de CPU y memoria, conviene mantenerlas vivas.
  • Crear y destruir conexiones con frecuencia desperdicia recursos.
    • Si aparecen muchas conexiones nuevas al mismo tiempo, una connection storm puede causar problemas difíciles de depurar relacionados con bloqueos internos de Postgres.
  • Primero conviene considerar un connection pooler externo como pgbouncer; si no puede usarse, un pool de conexiones en memoria es la alternativa.
    • Como Hatchet no puede asumir que la base de datos del usuario usa un pooler externo, utiliza pgxpool para Go.

Planificador de consultas y estadísticas

  • Las consultas complejas con muchos joins o mezclas de varios métodos de join no se resuelven solo agregando índices.
    • Los índices también tienen overhead, así que no deben agregarse sin límite.
  • El planificador de consultas convierte SQL en operaciones internas de la base de datos y decide, entre otras cosas, si usar índices, pero por información limitada puede no elegir el plan óptimo.
  • La información que usa el planificador son las estadísticas de las tablas, que pueden consultarse en pg_stats.
SELECT *
FROM pg_stats
WHERE tablename = 'mytable';
  • Las estadísticas se recolectan en ANALYZE y también se actualizan cuando corre autovacuum.
    • Si se aumenta la frecuencia de autovacuum, las estadísticas de consultas también se mantienen más actualizadas.
    • Una causa común de consultas que se comportan mal es una frecuencia de análisis insuficiente.
  • Si se juzga una consulta de forma simple según si cae en escaneo secuencial o no, se reduce el riesgo de aumentar la imprevisibilidad del planificador con microoptimizaciones.
    • Si las consultas se centran en claves primarias e índices, al planificador le resulta más fácil elegir un plan.

Análisis de planes de ejecución y escaneo secuencial

  • Algunos proveedores, como Google CloudSQL, muestrean consultas y guardan las lentas, pero no todos los servicios lo soportan.
  • EXPLAIN ANALYZE ejecuta la consulta de verdad y compara la cantidad de filas estimadas según las estadísticas de la tabla con la cantidad real de filas escaneadas.
    • En producción, hay que tener cuidado porque la consulta sí se ejecuta realmente.
    • Si se quiere ver solo el plan sin ejecutar, se usa EXPLAIN sin ANALYZE.
  • El plan detallado puede guardarse en JSON y visualizarse en explain.dalibo.com.
psql -XqAt -f explain.sql -d $DATABASE_URL > analyze.json
  • Si las estadísticas y los índices están bien y aun así hace un escaneo secuencial, es posible que el planificador haya calculado que el costo del escaneo secuencial es menor.
    • Los índices se almacenan por separado del heap donde están los datos reales de la tabla, así que leer varias filas encontradas en el índice implica volver a leerlas desde el heap.
    • Si no puede reestructurarse mucho la consulta, conviene aceptar el escaneo secuencial o evaluar particionamiento.

Escrituras masivas y procesamiento por lotes

  • Cada consulta tiene overhead: el tiempo de ida y vuelta a la base de datos, el tiempo para obtener una conexión del pool de la aplicación y el tiempo de procesamiento dentro de Postgres.
    • Los bloqueos internos de Postgres también pueden volverse un cuello de botella en entornos de alto throughput.
  • Si se agrupan varias filas en una sola consulta, esos costos se reducen.
    • La forma más simple es enviar varias consultas juntas al servidor como una transacción implícita.
    • En Go, puede usarse SendBatch de pgx.
  • En Hatchet, el procesamiento por lotes aumentó el throughput unas 10 veces, y hay más optimizaciones de inserción en su guía de inserciones rápidas en Postgres.

autovacuum y transaction ID wraparound

  • autovacuum se encarga de limpiar dead tuples y gestionar transaction IDs, y en entornos de escritura de alta frecuencia puede requerir ajustes de configuración.
  • Un tuple es una versión de una fila almacenada en el sistema de archivos.
    • Aunque una fila se actualice o elimine, la versión anterior permanece hasta que todas las transacciones iniciadas antes de ese cambio hagan commit o rollback.
    • Una versión que ya no puede ser leída por ninguna transacción es un dead tuple.
  • Si la velocidad de escritura es demasiado alta, autovacuum puede no alcanzar el ritmo de creación de dead tuples y el estado de la base de datos puede degradarse rápidamente.
  • Si al revisar procesos activos en pg_stat_activity se ve que una consulta de autovacuum lleva alrededor de más de 1 hora ejecutándose, conviene considerar un cambio de configuración.
  • Si se consumen todos los transaction IDs antes de que autovacuum los recupere, ocurre transaction ID wraparound y puede derivar en un downtime importante.

Hinchamiento de tablas e índices

  • Postgres almacena filas en páginas de 8 KB en disco, y si no puede poner una fila nueva en una página existente, crea una nueva.
  • Cuando se recuperan dead tuples y las páginas quedan parcialmente vacías, se produce el table bloat, lo que puede aumentar mucho el uso de disco.
    • La mejor prevención es ajustar autovacuum antes de que aparezca el bloat.
    • En tablas que ya tienen bloat, pueden usarse extensiones como pg_repack.
    • El comando integrado VACUUM FULL casi nunca es una buena opción.
    • En Postgres 19 se planea agregar REPACK...CONCURRENTLY para reempaquetado concurrente de tablas, pero Hatchet todavía no lo ha probado.
  • El index bloat también es una forma especial de bloat de tabla, y puede reducirse con una configuración apropiada de autovacuum.
    • En índices que ya tienen bloat, puede usarse el comando integrado REINDEX INDEX CONCURRENTLY.

Concurrencia basada en FOR UPDATE SKIP LOCKED

  • FOR UPDATE SKIP LOCKED reserva las filas seleccionadas para la transacción actual sin bloquear otras consultas.
  • Hatchet lo usa en una cola de trabajos, donde una sola consulta puede bloquear tareas en espera y cambiar su estado a RUNNING.
WITH eligible_tasks AS (
    SELECT *
    FROM tasks
    WHERE status = 'QUEUED'
    ORDER BY id ASC
    FOR UPDATE SKIP LOCKED
    LIMIT 100
)
UPDATE tasks
SET status = 'RUNNING'
FROM eligible_tasks
WHERE tasks.id = eligible_tasks.id
RETURNING tasks.*;
  • También es útil cuando se actualizan al mismo tiempo filas independientes o cuando varias instancias de una aplicación gestionan el lease de un objeto.
    • Hatchet lo usa para distribuir tenant leases entre varios engines.

Particionamiento

  • El particionamiento integrado de Postgres divide una tabla según valores de fila como timestamp o hash.
  • En datos de series temporales y en los datos históricos de trabajos de Hatchet, ofrece estas ventajas:
    • Permite ejecutar autovacuum de forma independiente por partición y ampliar la escala del procesamiento de autovacuum de la tabla.
    • Los datos antiguos pueden eliminarse casi de inmediato desmontando la tabla particionada, sin borrar fila por fila.
  • Si Postgres no logra descartar particiones innecesarias en la etapa de planificación, puede agregarse overhead a las consultas de lectura.

Movimiento de datos entre tablas grandes

  • La migración de tablas grandes aquí no se refiere a cambios de esquema, sino a mover grandes volúmenes de datos de una tabla a otra.
  • Copiar una tabla muy grande en una sola transacción puede tomar horas.
    • Una transacción larga impide que autovacuum funcione normalmente y provoca acumulación de dead tuples.
    • Si la tabla anterior sigue recibiendo escrituras, esos datos no se reflejarán en la tabla nueva.
  • Hatchet ejecuta un gran backfill por lotes fuera de la transacción, y las nuevas escrituras que ocurren después de iniciar la migración se copian a la tabla nueva con triggers de Postgres.
    • Usan el unique constraint de la clave primaria para evitar escrituras duplicadas.

1 comentarios

 
GN⁺ 4 시간 전
Comentarios de Hacker News
  • Si es una base de datos de producción, me parece que lo primero debería ser tener un plan de respaldo y recuperación. La alta disponibilidad puede ser opcional al inicio, pero me sorprende que una guía de supervivencia no incluya respaldos y recuperación
    Me pregunto si hoy en día todavía se usa mucho Barman(https://pgbarman.org/) para respaldos de PostgreSQL

    • Si no eres especialista en PostgreSQL, es mejor no operarlo tú mismo y usar una base de datos administrada como RDS. Lo que ahorras por alojarlo tú mismo es mínimo comparado con el costo de obtener alta disponibilidad probada, respaldo y recuperación, recuperación a un punto en el tiempo y réplicas de lectura
    • Estoy usando pgBackRest. Ofrece una recuperación a un punto en el tiempo mejor que la solución casera de respaldos nocturnos que usaba antes, fue relativamente fácil configurarlo para respaldar en Backblaze B2 y no he tenido problemas
    • Para la mayoría, basta con ejecutar pg_dump_all desde cron, comprimirlo con zstd y copiarlo a S3, FTP o algo similar. Cuando los datos crecen, el tiempo y el costo del respaldo completo pesan, pero este método simple aguanta bastante tiempo
    • Si es una base de datos que garantiza durabilidad incluso ante cortes de energía, se puede respaldar con snapshots atómicos de volumen. Para reducir el tiempo de recuperación, primero hay que crear un checkpoint, y para evitar corrupción de datos la atomicidad del snapshot debe estar garantizada
      En AWS respaldé MongoDB de varios TB con snapshots de EBS para lograr respaldos incrementales y recuperación rápidas. No permite recuperación a un punto en el tiempo, pero como se pueden tomar con frecuencia cada hora, sirve bien como estrategia complementaria junto con herramientas específicas de PostgreSQL
    • Si ya operas Kubernetes, entonces puedes usar CloudNativePG
  • Hay varias cosas que se podrían complementar. En vez de un UUIDv4 genérico, usaría UUIDv7, y para evitar interbloqueos no solo hay que unificar de forma determinista el orden de las filas bloqueadas, sino también el de todos los bloqueos en las consultas, por ejemplo con id ASC
    Con EXPLAIN (GENERIC_PLAN) puedes copiar la consulta manteniendo los placeholders de parámetros y también ver el plan de optimización cuando PostgreSQL no conoce los valores reales. En tablas vacías o pequeñas, SET enable_seqscan = off sirve para comprobar si existe posibilidad de usar índices
    Como los índices B-tree que todos usan por defecto son pesados y tienden a inflarse, si solo haces búsquedas simples sin ordenamiento ni búsquedas por rango, también vale la pena considerar índices hash. No se pueden crear índices hash únicos, pero una restricción de exclusión hash puede dar un efecto parecido, y no admite índices únicos multicolumna
    También conviene aprender sobre los índices GIN y GiST. A usuarios de MySQL quizá les sorprenda, pero pueden acelerar consultas normales como LIKE '%foo%' sin necesidad de pasarse a búsqueda de texto completo

    • Los interbloqueos no solo ocurren cuando no hay un ORDER BY consistente para el conjunto de filas a bloquear, sino también cuando el orden de bloqueo entre tablas difiere. Si una transacción bloquea table_a, table_b en ese orden y otra transacción lo hace al revés, habrá interbloqueo aunque dentro de cada tabla uses ORDER BY y FOR UPDATE
      En teoría es obvio, pero en la práctica es mucho más difícil de depurar porque hay que entender globalmente qué tablas toca cada escritura, y me pasó de verdad con cierta extensión. Estoy probando GIN para búsquedas clave-valor sobre JSONB y la mejora de rendimiento fue enorme; también hubo una diferencia considerable entre AND y OR
    • Usar cualquier UUID como clave primaria sale caro porque las uniones por clave primaria son frecuentes y normalmente aporta poco beneficio. Como valor por defecto, es más seguro usar una clave primaria autoincremental y, si hace falta exponerla externamente, agregar una columna UUIDv4 con índice secundario. Me pregunto si UUIDv7 realmente da mejor rendimiento en B-tree que UUIDv4
    • Si desactivas el escaneo secuencial, me parece que PostgreSQL forzará el uso de algún índice si existe al menos uno. Así que quizá no te diga si es el índice correcto
    • Ya se han mencionado varias veces estas herramientas de conversión entre UUIDv7 y UUIDv4: https://github.com/ali-master/uuidv47 y https://github.com/stateless-me/uuidv47
  • Este consejo también es bueno, pero las startups con las que trabajé se toparon antes con problemas organizacionales que con problemas de escalabilidad. Conviene no usar ORM, usar claves primarias autoincrementales en vez de campos con significado y usar JSONB de forma limitada solo cuando sea realmente necesario
    Los datos fuente deberían ser append-only, solo de inserción, sin actualizaciones ni borrados. Las tablas auxiliares desnormalizadas por rendimiento o conveniencia pueden cambiarse, pero no deberían tratarse como fuente de verdad
    Usa pool de conexiones, pero cuidando la cantidad de conexiones; si no hay problemas, quizá ni siquiera haga falta PgBouncer. Evita las transacciones explícitas salvo que haya una razón clara, no las dejes abiertas durante tareas largas como RPC y casi nunca conviene usar SERIALIZABLE
    Si necesitas bloqueos explícitos como SELECT FOR UPDATE, es posible que el diseño esté mal. No hagas que filas de una misma tabla tengan significados distintos según un valor type int, reinventando el sistema de tipos, ni imites una base de datos de grafos con tablas node y edge autorreferenciadas. La mayoría de los casos se resuelven con tablas normalizadas comunes

    • En el backend PHP en el que trabajo, hay que instanciar objetos para revisar permisos y demás, así que el ORM es muy útil. Implementarlo sin ORM parece mucho más trabajo; me pregunto por qué sería una mala decisión
    • Si el costo más grande es el salario de los desarrolladores, la regla de no usar ORM es discutible. Con requisitos de negocio de las tablas, presión de clientes y presupuestos ajustados, el costo sigue corriendo mientras uno pasa mucho tiempo discutiendo con un DBA el diseño correcto, así que reglas como evitar columnas de tipo o estructuras tipo grafo no son tan fáciles de aplicar como suenan
    • Para una startup que necesita lanzar el producto rápido, un ORM es una opción suficientemente buena. Si entiendes trampas como las consultas N+1 y el lazy loading, es un mejor compromiso que volver a construir por tu cuenta la gestión de consultas y la parametrización
      Preferiría dedicar tiempo al desarrollo del producto en vez de obsesionarme demasiado temprano con el esquema de la base de datos y optimizar prematuramente al inicio del proyecto
    • He usado SELECT FOR UPDATE de forma útil en varios lugares y me pregunto cuál sería el problema. También me interesa saber si usar una fuente de verdad append-only elimina la necesidad de ese tipo de bloqueos
    • Los datos fuente append-only son atractivos, pero en varios sistemas en los que trabajé habrían disparado el almacenamiento de muchas tablas por un beneficio dudoso. Es una técnica útil, pero no estoy seguro de que deba imponerse como principio en todos lados
      En cambio, me pregunto qué tal sería usar como fuente de verdad tablas relacionales tradicionales mutables y registrar un log de cambios mediante triggers
  • No me gusta el borrado en cascada. Como la mayoría de los desarrolladores vive más en la capa de aplicación —Python, Node, Go— que en la base de datos, el borrado en cascada puede parecer magia: borras una fila de la tabla A y también desaparecen datos de la tabla B. Si está mal configurado es todavía más peligroso, así que para mantenimiento a largo plazo son mejores los DELETE explícitos, y con solo usar bien las llaves foráneas ya se puede mantener la consistencia
    Son válidas las trampas y soluciones alternativas en migraciones de tablas grandes, pero ya existen herramientas como pg-osc. Debería ser algo tan simple como ejecutar un comando y luego observar con tensión durante las 24 horas en que se copian los datos
    Las implementaciones de la aplicación y de la base de datos deberían separarse desde temprano. Como no puedes desplegar cambios de esquema y de aplicación como una sola transacción totalmente simultánea, una vez en producción hace falta acostumbrarse a hacer solo cambios de esquema retrocompatibles, como crear columnas nuevas nullable o con valor por defecto, y no renombrar tablas ni columnas
    La estrategia de gestión del esquema también debe definirse pronto. Hay que evitar procedimientos de despliegue donde un desarrollador senior ejecuta DDL manualmente en la base de producción desde su computadora; se pueden usar herramientas conocidas como Liquibase o Flyway

    • Hice pgschema, una herramienta declarativa de gestión de esquemas
  • El planificador de consultas optimiza el caso promedio, pero en una aplicación a veces es más útil optimizar el peor caso. Un usuario promedio tenía pocas filas y cierto índice devolvía resultados en menos de 10 ms, pero para usuarios de alto uso la misma consulta podía tardar más de 1 segundo según los parámetros
    Forcé otra ruta de índices con una consulta más compleja; el rendimiento promedio empeoró un poco, pero el peor caso también bajó a menos de 100 ms. Para la empresa era mucho más importante evitar timeouts que ahorrar 10 ms en promedio

  • SKIP LOCKED sirve para colas de trabajo basadas en transacciones interactivas donde la aplicación mantiene la transacción abierta y bloquea filas mientras trabaja. En aplicaciones de alto rendimiento conviene evitar esas transacciones y actualizar la fila de inmediato a pending, así que SKIP LOCKED no hace falta
    A medida que crece la escala, hay que reducir el estado que se mantiene en memoria dentro de la base de datos, y las transacciones interactivas también cuentan como ese tipo de estado. En entornos escalados, la idempotencia conviene más que la atomicidad

  • Las transacciones largas pueden dañar el estado de la base de datos, así que solo deberían usarse con una justificación fuerte. Usa idle_in_transaction_session_timeout para evitar que transacciones inactivas retengan bloqueos o tuplas por demasiado tiempo, y configura lock_timeout en las migraciones para que una sola sentencia DDL no congele todo el sistema
    También hay que configurar statement_timeout para que una consulta costosa no paralice el sistema

  • Tras operar PostgreSQL en una startup temprana, este artículo no enfatiza lo suficiente el monitoreo y las alertas. En PostgreSQL hay varios tipos clave de fallas que deben evitarse sí o sí, y con alertas se puede detectar el riesgo temprano
    Aunque AWS mande un correo diciendo que el ciclo de IDs de transacción está por agotarse, en una startup eso puede pasarse por alto fácilmente, especialmente en días como Boxing Day. Las señales que vigila AWS deberían conectarse a un pager, no a un email

  • Hay grandes diferencias poco conocidas entre implementaciones de pool de conexiones. La mayoría de los pools de conexión de aplicaciones optimizan latencia baja y disponibilidad de conexiones con FIFO, pero al mantener las conexiones siempre calientes es difícil reducir las innecesarias
    PgBouncer y algunos poolers externos usan LIFO para optimizar la cantidad de conexiones que llegan a PostgreSQL y el throughput. Si se reutiliza primero la conexión más reciente, las conexiones sobrantes se enfrían de forma natural y se cierran
    Para aplicaciones nuevas, FIFO basta, pero a mayor escala conviene usar herramientas como PgBouncer para reducir cientos de conexiones hasta en un 90%. Como PostgreSQL crea un proceso por conexión, funciona mejor cuanto menos conexiones tenga

  • En situaciones muy específicas, se obtuvieron buenos resultados haciendo joins en la memoria de la aplicación. A veces, para reducir los viajes de ida y vuelta a la base de datos, se termina creando una sola consulta con JOIN, UNION y CASE complejos mezclados.
    En cambio, ejecutar varias consultas simples de forma independiente y luego recorrer los resultados para vincular las filas relacionadas con mapas puede resultar más conveniente, aunque agregue más viajes de ida y vuelta y costo de iteración, porque el plan de consulta se vuelve más predecible. Debe usarse de forma limitada, y no se recomienda ciegamente solo porque algunos ORM funcionan así internamente.

    • La efectividad de este enfoque depende mucho del caso. Si un join genera un producto cartesiano mucho más grande que los datos originales, traer solo los conjuntos originales y combinarlos localmente puede reducir la carga sobre la BD y el tráfico de red.
      Pero un inner join selectivo produce un resultado mucho más pequeño que los datos originales, por lo que traer todos los registros y hacer localmente la intersección y el filtrado resulta mucho más costoso. En un index join, el planificador de consultas también puede aprovechar los índices para evitar escaneos completos de tablas, ordenamientos y filtrados indiscriminados.
    • También sé que se usa el enfoque de crear dos vistas y luego hacer el join, en lugar de una sola consulta compleja