Ambiente de teste populado utilizando a ferramenta sysbench para gerar carga de trabalho OLTP.
Geração de Dados:
sysbench --db-driver=pgsql --pgsql-host=127.0.0.1 --pgsql-port=5432 --pgsql-user=test02 --pgsql-password=test02 --pgsql-db=postgres --oltp-table-size=200000 --oltp-tables-count=10 --rand-init=on --threads=10 --time=30 --events=0 --report-interval=10 --percentile=99 /usr/share/sysbench/tests/include/oltp_legacy/oltp.lua prepare
sysbench --db-driver=pgsql --pgsql-host=127.0.0.1 --pgsql-port=5432 --pgsql-user=test02 --pgsql-password=test02 --pgsql-db=postgres --oltp-table-size=10000 --threads=10 --time=12000 --events=0 --report-interval=10 --percentile=99 /usr/share/sysbench/tests/include/oltp_legacy/oltp.lua run
Estrutura das Tabelas e Amostra de Dados:
Existem múltiplas tabelas sbtest*. Cada uma contém 200.000 linhas.
SELECT count(*) FROM sbtest1;
-- Resultado: 200000
SELECT count(*) FROM sbtest2;
-- Resultado: 200000
A definição das colunas é idêntica entre as tabelas: id (serial), k (integer), c (char), pad (char). A coluna k contém valores inteiros gerados aleatoriamente.
SELECT k FROM sbtest1 LIMIT 5;
-- Exemplo de saída: 99857, 100394, 100853, ...
SELECT k FROM sbtest2 LIMIT 5;
-- Exemplo de saída: 100979, 100581, 100424, ...
Análise do Problema com Agregação em Junção:
Ao executar consultas de contagem distintas em uma junção entre duas tabelas, os resultados são consistentes:
SELECT count(DISTINCT a.k) FROM sbtest1 a, sbtest2 b WHERE a.k = b.k;
-- Resultado: 18169
SELECT count(DISTINCT b.k) FROM sbtest1 a, sbtest2 b WHERE a.k = b.k;
-- Resultado: 18169
No entanto, uma consulta de contagem simples sobre a mesma junção produz um resultado anômalo:
SELECT count(b.k) FROM sbtest1 a, sbtest2 b WHERE a.k = b.k;
-- Resultado: 14976808
Este valor (14.976.808) é muito superior ao total de linhas em cada tabela individual (200.000). O plano de execução ajuda a elucidar o comportamento.
Causa Raiz: Produto Cartesiano Explícito
A condição de junção a.k = b.k não garantte uma relação um-para-um entre as tabelas. Para cada valor único de k na tabela sbtest1, existem múltiplas linhas correspondentes na tabela sbtest2 (e vice-versa). A consulta sem DISTINCT conta todas essas combinações resultantes do produto cartesiano filtrado.
Por exemplo, se o valor k = 100 aparecer 3 vezes em sbtest1 e 4 vezes em sbtest2, a junção gerará 3 x 4 = 12 linhas para essa chave. A soma dessas combinações para todas as chaves resulta no grande número observado. A contagem DISTINCT elimina essas duplicatas de junção, retornando o número real de valores de chave únicos que existem em ambas as tabelas.