sqlpostgresqlaggregationperformance

COUNT(DISTINCT) en SQL: contar valores unicos y su coste

Como contar valores unicos con COUNT(DISTINCT), por que es caro a gran escala y cuando usar alternativas aproximadas con HLL.

2 min de lecturaReferencesql · postgresql · aggregation · performance · analytics

COUNT(DISTINCT col) responde a una de las preguntas analiticas mas frecuentes: cuantos valores unicos hay en una columna. Suena trivial, pero en tablas grandes es una de las agregaciones mas caras de SQL, y conviene entender por que.

Conteo basico de valores unicos

Un COUNT(*) cuenta filas, mientras que COUNT(DISTINCT ...) primero elimina los duplicados y luego cuenta lo que queda. Compara:

-- Total order rows
SELECT COUNT(*) FROM orders;

-- How many distinct customers actually ordered
SELECT COUNT(DISTINCT user_id) FROM orders;

-- Distinct countries among registered users
SELECT COUNT(DISTINCT country) FROM users;

Algunos comportamientos que conviene conocer:

  • NULL se ignora: COUNT(DISTINCT col) no trata NULL como un valor.
  • Suele combinarse con GROUP BY, por ejemplo "compradores unicos por estado del pedido":
SELECT status, COUNT(DISTINCT user_id) AS unique_buyers
FROM orders
GROUP BY status;

Por que es caro a gran escala

COUNT(*) puede apoyarse en estadisticas e indices y a veces apenas lee datos. COUNT(DISTINCT) no puede: para descartar duplicados el motor debe materializar todos los valores y ordenarlos o cargarlos en una tabla hash.

  • Sin atajo por indice: incluso un indice sobre user_id no guarda un conteo listo de valores unicos.
  • Memoria: con millones de valores unicos la tabla hash supera work_mem y PostgreSQL recurre a un ordenamiento en disco.
  • Varios COUNT(DISTINCT) en una misma consulta se calculan por separado, cada uno con su propia estructura:
-- Each DISTINCT is computed independently -> heavy
SELECT
  COUNT(DISTINCT user_id)  AS buyers,
  COUNT(DISTINCT country)  AS countries
FROM orders o
JOIN users u ON u.id = o.user_id;

Trampa: en MySQL COUNT(DISTINCT a, b) funciona directamente. En PostgreSQL esa sintaxis no es valida; hay que envolver las columnas en una tupla ROW(...) o combinarlas de otra forma.

DISTINCT multicolumna

Cuando la "unicidad" la define un par de columnas, usa una tupla en PostgreSQL:

-- Unique (user, country) pairs in PostgreSQL
SELECT COUNT(DISTINCT (user_id, country)) AS uniq_pairs
FROM orders o
JOIN users u ON u.id = o.user_id;

Una alternativa es una subconsulta con GROUP BY, que suele leerse mejor y se optimiza de forma mas predecible:

SELECT COUNT(*) AS uniq_pairs
FROM (
  SELECT DISTINCT user_id, country
  FROM orders o
  JOIN users u ON u.id = o.user_id
) t;

Cuidado con la concatenacion de cadenas (user_id || '-' || country): entradas distintas pueden colisionar en la misma cadena y dar un resultado incorrecto. Una tupla o un GROUP BY es mas seguro.

DISTINCT dentro de otros agregados

DISTINCT no se limita a COUNT. Puedes ponerlo dentro de SUM, AVG, array_agg y string_agg para agregar solo valores unicos:

-- Sum of distinct salaries per department (dedupes equal salaries first)
SELECT dept, SUM(DISTINCT salary) AS sum_unique_salaries
FROM employees
GROUP BY dept;

-- Comma-separated list of distinct departments per manager
SELECT manager_id, string_agg(DISTINCT dept, ', ') AS depts
FROM employees
GROUP BY manager_id;

Recuerda que SUM(DISTINCT salary) rara vez tiene sentido para dinero: dos pagos distintos de 1000 se colapsan en uno. Usa DISTINCT dentro de un agregado solo cuando de verdad haya que eliminar duplicados.

Alternativas aproximadas: APPROX y HLL

Cuando necesitas un orden de magnitud sobre miles de millones de filas, un COUNT(DISTINCT) exacto es excesivo. El algoritmo HyperLogLog estima conteos unicos en memoria fija (kilobytes) con un error de alrededor del 1-2%.

  • ClickHouse: uniq() y uniqHLL12() son aproximados; uniqExact() es el equivalente exacto de COUNT(DISTINCT).
  • BigQuery / Snowflake: APPROX_COUNT_DISTINCT(col).
  • PostgreSQL: el nucleo no lo trae, pero la extension postgresql-hll lo anade.
-- ClickHouse: fast approximate distinct
SELECT uniq(user_id) AS approx_buyers FROM orders;

-- BigQuery / Snowflake style
SELECT APPROX_COUNT_DISTINCT(user_id) AS approx_buyers FROM orders;

La gran ventaja de HLL es que es combinable: los bocetos parciales se pueden fusionar. Eso permite guardar bocetos diarios y derivar unicos mensuales sin reprocesar los datos crudos, algo que un COUNT(DISTINCT) exacto no puede hacer. Usa el conteo aproximado para paneles y graficas de tendencia, y reserva el exacto para facturacion e informes donde cada unidad debe ser correcta.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador