El problema oculto que hace que COUNT(DISTINCT) sea 3.4 veces más lento en PostgreSQL
Una consulta aparentemente simple como SELECT count(DISTINCT user_id) FROM events; puede ser 3.4 veces más lenta que su equivalente optimizado, según el análisis detallado publicado en BoringSQL. La diferencia no es una cuestión de hardware o configuración básica: es un problema fundamental en cómo PostgreSQL maneja las agregaciones con DISTINCT que desactiva completamente la ejecución paralela de consultas.
En pruebas con una tabla de 10 millones de filas y 50,001 usuarios distintos, la consulta count(DISTINCT user_id) tomó 1.211 milisegundos ejecutándose en un solo núcleo, con un derrame de 115 MB a disco. Mientras tanto, la versión optimizada usando GROUP BY completó la misma operación en 360 milisegundos usando 4 workers paralelos y manteniendo todo en memoria.
Por qué DISTINCT mata la paralelización en PostgreSQL
El problema radica en cómo PostgreSQL implementa las agregaciones paralelas. Según la documentación oficial de PostgreSQL, el motor divide el trabajo en dos fases: cada worker ejecuta un Partial Aggregate que construye un estado de transición, y luego el líder ejecuta un Finalize Aggregate que fusiona esos estados parciales usando una función de combinación.
👥 ¿Quieres ir más allá de la noticia?
En nuestra comunidad discutimos las tendencias, compartimos oportunidades y nos ayudamos entre emprendedores. Sin humo, solo acción.
👥 Unirme a la comunidadPara count(*), sum, avg, min y max, esta función de combinación existe y permite la paralelización. Pero count(DISTINCT user_id) no tiene una función de combinación utilizable. ¿Por qué? Para fusionar correctamente los resultados de dos workers, el líder necesitaría saber qué usuarios específicos vio cada worker, porque un usuario que aparece en ambos slices debe contarse solo una vez.
Un conteo parcial de valores distintos no se puede combinar sin enviar todo el conjunto de valores distintos de cada worker y hacer una unión. En ese punto, ya habrías movido todos los datos a un solo lugar, eliminando cualquier beneficio de la agregación paralela.
Cómo detectar si tu consulta está afectada
La señal en EXPLAIN (ANALYZE) es inconfundible una vez que sabes qué buscar:
- Un Aggregate de nivel superior sin Gather debajo
- Un Sort grande con una línea
external merge ... Disk: - Un solo
loops=1escaneando toda la tabla - Ningún
Workers PlannedoWorkers Launched
El análisis verifica que este problema persiste en PostgreSQL 17.10, 18.4 y 19beta1 (según el sitio oficial de PostgreSQL, la versión 19 Beta 2 se lanzó en julio de 2026). Incluso con debug_parallel_query = on, que fuerza al planificador a buscar planes paralelos donde sean legales, el resultado es un Gather con Workers Planned: 1 y Single Copy: true: un solo proceso ejecuta todo el plan.
La solución: reescribir DISTINCT como GROUP BY
La solución efectiva es mover la deduplicación a una operación que PostgreSQL sí puede paralelizar: un GROUP BY:
SELECT count(*)
FROM (SELECT user_id FROM events GROUP BY user_id) s;
Esta reescritura logra exactamente «los user_ids distintos», pero usando una agrupación que tiene modo parcial: cada worker construye un hash parcial de los grupos que vio, y el líder fusiona esos hashes. Contar cuántos grupos salieron es entonces trivial.
El impacto es dramático: según OneUptime, las consultas analíticas paralelas pueden reducir los tiempos de consulta de minutos a segundos para cargas de trabajo grandes. La guía de OneUptime sobre ejecución paralela de consultas en PostgreSQL confirma que operaciones como GROUP BY con agregación parcial, COUNT, SUM y AVG sí admiten paralelismo cuando están correctamente configuradas.
Casos más complejos: conteos distintos por grupo
El problema se complica con la versión por grupo de la misma consulta:
SELECT country, count(DISTINCT user_id) FROM events GROUP BY country;
Esta también es serial (un GroupAggregate sobre un sort en country, user_id). La misma idea aplica: deduplicar primero con una agrupación que los workers puedan dividir, luego agregar:
SELECT country, count(*)
FROM (SELECT country, user_id FROM events GROUP BY country, user_id) s
GROUP BY country;
Esta formulación hace que el trabajo sea paralelizable, pero si el planificador realmente elige el camino paralelo depende de cardinalidades y costos. La regla es la misma: empujar la distinctness a un GROUP BY que el motor pueda dividir.
¿Qué significa esto para tu startup?
1. Revisa tus dashboards y consultas nocturnas
Si tu startup tiene dashboards sobre meses de eventos, rollups nocturnos que corren en un solo núcleo mientras el resto está inactivo, o tablas de hechos grandes, estas son las consultas para inspeccionar. El problema no importa en tablas pequeñas, pero aparece exactamente donde duele: en las operaciones que ya son costosas.
Acción concreta: Usa EXPLAIN (ANALYZE) en tus consultas más pesadas de analytics. Busca el patrón descrito arriba. Si ves external merge Disk: junto con count(DISTINCT), tienes una oportunidad de optimización inmediata.
2. Implementa la reescritura en tu código base
No se trata solo de optimizar consultas manualmente. Incorpora este patrón en tus abstracciones de acceso a datos. Si usas un ORM, considera añadir un método .distinct_count_optimized() que genere la consulta GROUP BY. Si trabajas con SQL directo, documenta este patrón para tu equipo.
Acción concreta: Crea una función helper en tu capa de acceso a datos que convierta count(DISTINCT column) en la versión optimizada automáticamente para tablas por encima de un tamaño umbral.
3. Configura PostgreSQL para aprovechar el paralelismo
Según la guía de OneUptime, la configuración adecuada es crítica:
max_parallel_workers_per_gather = 4(para cargas analíticas)max_parallel_workers = 8min_parallel_table_scan_size = 1MB(para forzar paralelismo en tablas más pequeñas)
Acción concreta: Revisa tu postgresql.conf actual. Para workloads mixtos (OLTP + analytics), considera configuraciones diferentes por base de datos o usar SET para consultas analíticas específicas.
4. Considera HyperLogLog para conteos aproximados
Si una respuesta aproximada es aceptable (como en muchos dashboards), HyperLogLog es la solución diseñada para este problema. La extensión postgresql-hll se basa en esta idea: su sketch es un estado pequeño que, en principio, tiene la función de combinación que carecen los conteos distintos exactos, por lo que los sketches parciales de los workers deberían fusionarse.
Acción concreta: Evalúa postgresql-hll para tus dashboards donde un error del 1-2% en conteos distintos es aceptable a cambio de un rendimiento drásticamente mejorado.
Cuándo NO optimizar
El artículo es claro: ninguno de esto importa en una tabla pequeña. Si el escaneo es de unos pocos miles de filas, la ejecución serial es instantánea y la reescritura solo añade ruido. La penalización de la agregación-distinta es una función de cuántas filas tiene que ordenar el único núcleo, por lo que aparece exactamente donde duele.
Tampoco optimices prematuramente. Primero mide, luego optimiza. Usa EXPLAIN (ANALYZE) para confirmar que tienes el problema antes de aplicar la solución.
El panorama futuro de PostgreSQL
Con PostgreSQL 19 Beta 2 recién lanzado en julio de 2026 (según el sitio oficial), y las versiones 18.4 y 17.10 también actualizadas en mayo de 2026, la comunidad sigue activamente el desarrollo. Aunque este problema específico de count(DISTINCT) persiste incluso en 19beta1, es importante mantenerse actualizado: cada nueva versión trae mejoras de rendimiento y nuevas capacidades de paralelización.
Para founders técnicos, entender estas limitaciones no es solo optimización micro: es arquitectura de datos a escala. Un conteo distinto que tarda 1.2 segundos en lugar de 0.36 segundos puede no parecer mucho, pero multiplicado por docenas de dashboards y usuarios concurrentes, se convierte en minutos de latencia acumulada, costos de infraestructura más altos, y experiencia de usuario degradada.
La lección más profunda aquí es que las abstracciones de base de datos tienen costos ocultos. DISTINCT parece inocuo, pero cambia fundamentalmente cómo PostgreSQL ejecuta tu consulta. Como founder, tu trabajo no es memorizar cada uno de estos detalles, sino cultivar una mentalidad de investigación basada en datos: cuando el rendimiento se degrada, no asumas, mide. Cuando mides, no solo veas el tiempo total, examina el plan de ejecución. Y cuando examinas, busca estos patrones específicos que indican oportunidades de optimización sistémica.
Fuentes
- The DISTINCT in Your COUNT
- PostgreSQL: The world’s most advanced open source database
- How to Use Parallel Query Execution in PostgreSQL
👥 ¿Quieres ir más allá de la noticia?
En nuestra comunidad discutimos las tendencias, compartimos oportunidades y nos ayudamos entre emprendedores. Sin humo, solo acción.
👥 Unirme a la comunidad













