sqlpostgresqlaggregationperformance

COUNT(DISTINCT) no SQL: contar valores unicos e seu custo

Como contar valores unicos com COUNT(DISTINCT), por que fica caro em escala e quando usar alternativas aproximadas com HLL.

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

COUNT(DISTINCT col) responde a uma das perguntas analiticas mais comuns: quantos valores unicos existem em uma coluna. Parece trivial, mas em tabelas grandes e uma das agregacoes mais caras do SQL, e vale entender o porque.

Contagem basica de valores unicos

Um COUNT(*) conta linhas, enquanto COUNT(DISTINCT ...) primeiro remove duplicatas e depois conta o que sobra. Compare:

-- 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;

Alguns comportamentos importantes:

  • NULL e ignorado: COUNT(DISTINCT col) nao trata NULL como um valor.
  • Costuma ser combinado com GROUP BY, por exemplo "compradores unicos por status do pedido":
SELECT status, COUNT(DISTINCT user_id) AS unique_buyers
FROM orders
GROUP BY status;

Por que e caro em escala

COUNT(*) pode se apoiar em estatisticas e indices e as vezes quase nao le dados. COUNT(DISTINCT) nao consegue: para descartar duplicatas o motor precisa materializar todos os valores e ordena-los ou carrega-los em uma tabela hash.

  • Sem atalho por indice: ate um indice sobre user_id nao guarda uma contagem pronta de valores unicos.
  • Memoria: com milhoes de valores unicos a tabela hash ultrapassa o work_mem e o PostgreSQL recorre a uma ordenacao em disco.
  • Varios COUNT(DISTINCT) na mesma consulta sao calculados separadamente, cada um com sua propria estrutura:
-- 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;

Pegadinha: no MySQL COUNT(DISTINCT a, b) funciona diretamente. No PostgreSQL essa sintaxe e invalida; e preciso envolver as colunas em uma tupla ROW(...) ou combina-las de outra forma.

DISTINCT multicoluna

Quando a "unicidade" e definida por um par de colunas, use uma tupla no 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;

Uma alternativa e uma subconsulta com GROUP BY, que costuma ser mais legivel e otimiza de forma mais previsivel:

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 com a concatenacao de strings (user_id || '-' || country): entradas distintas podem colidir na mesma string e gerar resultado errado. Uma tupla ou um GROUP BY e mais seguro.

DISTINCT dentro de outras agregacoes

DISTINCT nao se limita ao COUNT. Voce pode coloca-lo dentro de SUM, AVG, array_agg e string_agg para agregar apenas 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;

Lembre-se de que SUM(DISTINCT salary) raramente faz sentido para dinheiro: dois pagamentos distintos de 1000 se fundem em um. Use DISTINCT dentro de um agregado apenas quando realmente for preciso remover duplicatas.

Alternativas aproximadas: APPROX e HLL

Quando voce precisa de uma ordem de grandeza sobre bilhoes de linhas, um COUNT(DISTINCT) exato e exagero. O algoritmo HyperLogLog estima contagens unicas em memoria fixa (kilobytes) com erro de cerca de 1-2%.

  • ClickHouse: uniq() e uniqHLL12() sao aproximados; uniqExact() e o equivalente exato de COUNT(DISTINCT).
  • BigQuery / Snowflake: APPROX_COUNT_DISTINCT(col).
  • PostgreSQL: o nucleo nao tem, mas a extensao postgresql-hll adiciona.
-- 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;

A grande vantagem do HLL e ser combinavel: esbocos parciais podem ser fundidos. Isso permite guardar esbocos diarios e derivar unicos mensais sem reprocessar os dados brutos, algo que um COUNT(DISTINCT) exato nao consegue. Use a contagem aproximada em dashboards e graficos de tendencia, e reserve a exata para faturamento e relatorios onde cada unidade precisa estar correta.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador