O Que É Cardinalidade - Tipos de relacionamento ou cardinalidade do relacionamento · Banco de ...
Tipos de relacionamento ou cardinalidade do relacionamento · Banco de ...

O que é cardinalidade no contexto de banco de dados

Cardinalidade é basicamente o número de linhas distintas que uma coluna, ou combinação de colunas, possui em uma tabela. Em teoria dos bancos de dados relacionais, isso define como as tabelas se conectam entre si — se é um para um, um para muitos ou muitos para muitos. O conceito existe desde a década de 1970, quando Edgar F. Codd publicou seus artigos fundadores sobre o modelo relacional, e continua sendo um dos pilares para entender performance de queries e normalização de esquemas.

O que é cardinalidade e por que ela importa na prática

Na prática, cardinalidade é o que o otimizador de query usa para decidir o plano de execução. Quando você roda um JOIN entre duas tabelas e o optimizer estima que a cardinalidade de saída será próxima de 50 milhões de linhas, ele vai tentar evitar sort-merge joins e ir direto para hash join. Se a estimativa estiver errada — e essa é a parte chata — você pode acabar com um plano completamente inadequado rodando à toa por minutos a mais. A cardinalidade se divide em dois tipos principais. A cardinalidade de baixo é quando poucos valores distintos existem em uma coluna, como um campo "sexo" com apenas "M" e "F". A cardinalidade alta aparece em colunas como "id_cliente" ou "uuid", onde praticamente cada linha tem um valor único. A cardinalidade média, aquela que dá mais trabalho, é o que você encontra em campos como "cidade" ou "categoria" — valores que se repetem mas não de forma uniforme, e que podem distorcer completamente as estimativas do otimizador se as estatísticas estiverem desatualizadas.

Eu trabalhei em um projeto onde a tabela de logs tinha uma coluna "status_operacao" com cardinalidade baixíssima — quatro valores possíveis — e as estatísticas estavam tão defasadas que o PostgreSQL estimava 30% dos registros em cada status, quando na realidade 92% caíam em um deles. Isso fazia o otimizador escolher sequential scans em subqueries que deveriam usar índice. A solução foi rodar um ANALYZE manual na tabela e ajustar o parâmetro default_statistics_target de 100 para 500 naquela coluna específica. O plano de execução mudou de sequential scan para index scan e a query caiu de 4 segundos para 80 milissegundos. O problema é que depois de algumas semanas, com a distribuição dos dados mudando, a estimativa voltou a errar e tivemos que criar uma estatística estendida com CREATE STATISTICS para capturar correlações entre colunas.

Como calcular e interpretar a cardinalidade

Para descobrir a cardinalidade de uma coluna simples, você usa COUNT(DISTINCT coluna). O problema é que em tabelas grandes isso pode ser custoso. Em sistemas produtores com tabelas de centenas de milhões de linhas, esse tipo de consulta pode travar o banco se não for feita em horário de baixo tráfego ou com LIMIT controlado. Uma abordagem mais segura é consultar as visões de sistema — no PostgreSQL tem pg_stats, no MySQL tem information_schema.columns e o comando SHOW INDEX, no SQL Server tem sys.statistics. Essas visões já trazem a estimativa de distinct_values que o otimizador usa internamente. A cardinalidade de junção, que é o que realmente impacta performance, é calculada multiplicando-se as cardinalidades individuais, mas considerando a restrição imposta pelo JOIN. Se uma tabela de pedidos tem 1 milhão de linhas e cada pedido tem no máximo um cliente, e a tabela de clientes tem 200 mil linhas distintas, a cardinalidade resultante do JOIN não será 200 bilhões — será no máximo 1 milhão, porque a chave estrangeira restringe a combinação. O otimizador precisa estimar isso corretamente, e aí entram os histograms.

Histogramas dividem os valores de uma coluna em buckets baseados em frequência. Quando há poucas distinções, o histograma não ajuda muito porque cada bucket acaba agrupando muitos valores. Já quando a cardinalidade é alta, o histograma se torna essencial para o otimizador entender a distribuição. Tabelas com colunas de data e hora, por exemplo, costumam ter histogramas úteis porque a distribuição temporal segue padrões previsíveis — mas colunas com valores aleatórios, como UUIDs gerados distribuídos, praticamente invalidam a utilidade do histograma para estimativas. Um detalhe que muita gente deixa passar: cardinalidade não é o mesmo que seletividade, embora estejam relacionados. Seletividade é a proporção entre o número de valores distintos e o total de linhas. Uma coluna com 10 mil valores distintos em uma tabela de 1 bilhão de linhas tem cardinalidade de 10 mil, mas seletividade de 0,001%. O otimizador olha seletividade para decidir se vale a pena usar índice. Colunas de baixa seletividade raramente justificam um índice, a menos que sejam usadas em filtros que já retornam poucos registros combinados com outras condições.

Cardinalidade em modelagem e normalização

Na modelagem conceitual, a cardinalidade dos relacionamentos define a estrutura do banco desde o início. Um relacionamento um-para-um é raro e normalmente indica que as tabelas deveriam ter sidoadas — exceto quando há motivos de segurança ou performance que justificam a separação, como deixar dados sensíveis em tabelas isoladas com controle de acesso diferente. Um-para-muitos é o padrão mais comum: uma tabela pai com milhares de filhos, e o índice na chave estrangeira da tabela filha é praticamente obrigatório para performance de query. Muitos-para-muitos exigem uma tabela intermediária, o que adiciona uma camada de complexidade que iniciantes frequentemente subestimam. Cada JOIN adicional reduz a cardinalidade estimada porque impõe restrições, mas também aumenta o custo computacional. Em sistemas de e-commerce que eu vi rodando, relatórios de vendas com três ou quatro JOINs em tabelas de cardinalidade alta podiam levar de 3 a 8 segundos sem índices adequados, e cair para menos de 200ms depois de adicionar índices compostos nas colunas de junção. A diferença está em entender exatamente qual é a cardinalidade esperada de cada join antes de criar o índice.

👉 Clique no botão abaixo para saber mais sobre o assunto!

A normalização funciona basicamente como um mecanismo de controle de cardinalidade. Primeira forma normal elimina grupos repetidos, segunda forma normal remove dependências parciais, terceira forma normal remove dependências transitivas. Cada passo reduz redundância, mas também aumenta o número de tabelas e JOINs necessários para recuperar dados. O resultado é um trade-off clássico: mais normalização significa mais integridade e menos anomalia de atualização, mas também mais JOINs e potencialmente pior performance em leitura. Sistemas que eu vi normalizados até a quinta forma normal tinham queries que precisavam de seis ou sete JOINs para montar uma tela simples de dashboard, e isso só era aceitável porque as tabelas eram pequenas e os índices bem projetados.

Pitfalls comuns e limitações

O maior problema com cardinalidade é que estimativas erradas causam planos de execução ruins, e estimativas erradas acontecem quando as estatísticas estão desatualizadas. Atualizações massivas de dados, inserts em lote sem autovacuum adequado, e tabelas que recebem poucos UPDATEs após o INSERT inicial são os cenários que mais causam estatísticas defasadas. No PostgreSQL, o autovacuum é geralmente suficiente, mas em tabelas muito ativas com padrões de escrita específicos, o autovacuum pode não conseguir acompanhar. Nesses casos, um VACUUM ANALYZE manual resolve, mas não adianta colocar isso no cron sem entender o padrão de acesso — analisar tabela demais gera overhead desnecessário. Outro problema frequente é a hipótese de independência. O otimizador assume que os predicados em colunas diferentes são independentes, mas na prática há correlações fortes. Por exemplo, em uma tabela de transações financeiras, a coluna "tipo_transacao" e a coluna "valor" podem estar correlacionadas — transferências tendem a ter valores altos enquanto compras no crédito tendem a ter valores menores. O otimizador não sabe disso sozinho e vai superestimar o número de linhas resultantes de um filtro combinado. A solução é criar estatísticas estendidas multicoluna, disponíveis no PostgreSQL desde a versão 11 e no SQL Server com statistik groups.

Também existe o problema das colunas com valores nulos. A cardinalidade não conta NULLs como valores distintos na maioria dos SGBDs, mas NULLs ocupam espaço nos histograms e podem distorcer estimativas quando há uma proporção significativa. Em uma tabela com 10 milhões de linhas e 3 milhões de NULLs em uma coluna de filtro, o otimizador pode acabar escolhendo um plano que não considera eficientemente a existência desses nulos. O workaround é criar um índice filtrado (filtered index) ou usar uma constraint NOT NULL com valor padrão adequado. Cardinality sketch, usado pelo Oracle e disponível como extensão no PostgreSQL, é uma técnica que estima cardinalidade sem varrer a tabela inteira, usando estruturas probabilísticas como HyperLogLog. Pode ser útil para estimativas rápidas em tabelas muito grandes, mas não substitui histograms para decisões de plano de execução. A precisão é boa o suficiente para orientação geral, mas ruim o suficiente para causar erros sérios em junções complexas.

Quando a cardinalidade alta é problemática

Colunas de alta cardinalidade, como timestamps com granularidade de milissegundo ou UUIDs, parecem ótimas candidates para índices porque possuem alta seletividade. Na realidade, índices em colunas de altíssima cardinalidade podem ser inúteis quando a consulta retorna uma porcentagem significativa das linhas — digamos acima de 5 a 10%. Nesse caso, o custo de navegar pela árvore B-tree do índice é maior do que fazer um sequential scan. O otimizador deve detectar isso automaticamente, mas com estatísticas desatualizadas ou dados com distribuição muito desigual, ele pode insistir no índice e piorar a performance. Em tabelas particionadas, a cardinalidade é calculada por partição, não pela tabela inteira. Isso pode ser vantajoso porque cada partição tem menos linhas e estimativas mais precisas, mas também significa que o otimizador não vê a distribuição global. Se você tem uma coluna de timestamp particionada por mês e faz uma query que abrange vários meses, as estimativas por partição podem não refletir tendências sazonais que afetam a distribuição geral.

Dicas práticas para lidar com cardinalidade

A primeira coisa é verificar as estatísticas regularmente. No PostgreSQL, uma query simples como SELECT attname, inherited, null_frac, avg_width, n_distinct, correlation FROM pg_stats WHERE relname = 'sua_tabela' mostra exatamente o que o otimizador está vendo. O campo n_distinct é o mais importante: valores positivos indicam cardinalidade distinta conhecida, -1 indica unknown, e valores negativos entre -1 e 0 indicam fração de linhas que são distintas. Se o n_distinct for -1 em uma coluna que deveria ter valores fixos, suas estatísticas estão desatualizadas. A segunda dica é entender que índices compostos têm cardinalidade baseada na ordem das colunas. Um índice (status, data_criacao) tem cardinalidade igual à cardinalidade de status, não de status combinado com data_criacao, a menos que você crie uma estatística estendida. Queries que filtram por data_criacao sem considerar status vão sofrer com estimativas ruins nesse índice. A solução é criar um índice separado para data_criacao ou usar estatísticas multicoluna.

A terceira coisa é saber quando não se preocupar com cardinalidade. Tabelas pequenas, digamos abaixo de 10 mil linhas, não se beneficiam de otimizações avançadas de cardinalidade porque o otimizador simplesmente prefere sequential scan de qualquer forma. Gastar tempo ajustando estatísticas nessas tabelas é perda de tempo. Foque nos modelos de dados que realmente importam — tabelas com milhões de linhas, joins frequentes, e queries que são executadas com alta frequência ou que são críticas para o negócio.