1

Por que não mostramos contagem de linhas no Cassandra (500 vira 143)

Sou da equipe que desenvolve o LibreDB Studio, um cliente de banco open source e self-hosted. Escrevo por transparência: não é post de divulgação, é o relato de uma decisão de design que confunde bastante quem abre uma tabela do Cassandra esperando o mesmo comportamento de um Postgres.

A decisão em uma linha: no Cassandra, o LibreDB não mostra a contagem de linhas no rodapé. Deixa o campo em branco. Abaixo explico por que preferimos não mostrar nada a mostrar um número que parece certo e não é.

O sintoma

Você tem uma tabela que sabe ter exatamente 500 linhas. Abre no client, olha o rodapé, e lê 143. Sem filtro, sem WHERE escondido, sem erro no console. Só um número menor que o real. E, pra piorar, se você atualizar a tela alguns minutos depois, o número pode ser outro.

A reação natural é achar que é bug do client. Não é. O número que a gente exibia ali nunca foi uma contagem de linhas de verdade, e no Cassandra isso fica escancarado por causa de como o dado é guardado.

Por que não dá pra simplesmente rodar count(*)

O jeito honesto de saber quantas linhas existem é count(*):

SELECT count(*) FROM loja.pedidos;

O problema é o que isso significa no Cassandra. Os dados não moram num lugar só. A tabela é fatiada por partition key, cada chave vira um token (por padrão via Murmur3), e esses tokens são distribuídos num anel entre os nós do cluster. Cada nó é dono de algumas faixas de token, mais as réplicas.

Então count(*) não lê um contador pronto. É uma varredura. O coordenador precisa passar por todas as partições, em todos os nós, somar e devolver. Isso dá o número certo, mas sai caro. Em tabela grande estoura timeout e pressiona o cluster inteiro. Não é o tipo de coisa que você quer disparar sozinho toda vez que a interface precisa pintar um rodapé. Contagem em rodapé tem que ser barata e instantânea, e count(*) no Cassandra é o oposto disso.

De onde vinha o 143: system.size_estimates

Como count(*) é caro, client de banco costuma pegar um atalho: em vez de contar, lê uma estimativa barata que o próprio banco já mantém. No Postgres isso seria o reltuples do pg_class; no MySQL/InnoDB, o TABLE_ROWS do information_schema. Já são estimativas, mas num banco de nó único elas costumam ficar perto o suficiente pra ninguém reclamar.

No Cassandra a fonte equivalente é a tabela system.size_estimates:

SELECT range_start, range_end, partitions_count, mean_partition_size
FROM system.size_estimates
WHERE keyspace_name = 'loja' AND table_name = 'pedidos';

Cada linha aí é a estimativa de um nó, para uma faixa de token que ele guarda. E é aqui que mora a confusão, então vou destrinchar.

Primeiro problema: é partição, não linha

Repara na coluna: partitions_count. Não é rows_count. É uma contagem estimada de partições, não de linhas.

Numa tabela com clustering columns, uma partição guarda várias linhas. Se os seus 500 pedidos estão distribuídos em 143 partições (agrupados por cliente ou por dia, digamos), a soma de partitions_count dá algo perto de 143. É exatamente esse valor que subia pro rodapé disfarçado de contagem de linhas. O número não mente sobre o que ele mede. O problema é que mede partição, e a gente é que pendurava esse valor num campo escrito "linhas".

Só isso já derruba a ideia de reaproveitar a estimativa como número de linhas.

Segundo problema: mesmo como estimativa de partição, ela balança

Pega uma tabela de uma linha por partição, onde partição e linha praticamente coincidem. Mesmo assim o número não fecha, e o motivo é o modelo de armazenamento.

O Cassandra é LSM. Escrita entra no memtable, em memória, e só depois é despejada em SSTables, que são arquivos imutáveis. Update não reescreve no lugar. Delete não apaga na hora: grava um tombstone, um marcador de "isso foi removido", que continua ocupando espaço até a compaction rodar e, respeitado o gc_grace_seconds, purgar de vez. Antes disso a mesma partição pode existir espalhada em vários SSTables ao mesmo tempo.

O size_estimates é calculado a partir dos metadados desses SSTables. Ele não é recalculado a cada escrita: quem atualiza é uma tarefa periódica em segundo plano, e dá pra forçar na mão com nodetool refreshsizeestimates. Ou seja, o que você lê é sempre uma foto de algum momento anterior. Juntando as pontas:

  • Dado recém escrito, ainda no memtable e sem flush, não entrou na estimativa.
  • Linha deletada continua contando enquanto o tombstone não foi purgado pela compaction.
  • A mesma partição espalhada em vários SSTables pode ser contada de formas diferentes até a compaction juntar tudo.

O número respira junto com o ciclo de compaction do cluster. Não é valor errado por bug: é uma foto aproximada e defasada de propósito, porque o objetivo do size_estimates nunca foi dar contagem exata. Ele existe pra ferramenta de particionamento estimar como quebrar o trabalho por faixa de token, não pra alimentar rodapé de UI. Vale lembrar ainda que os campos e o comportamento dessa tabela mudaram entre versões do Cassandra, então nem sempre bate exatamente com o que você vê num cluster específico.

Terceiro: ainda é por nó e por faixa de token

E tem a camada distribuída por cima. Cada nó descreve as faixas que ele guarda. O que você lê depende de qual nó respondeu e de quais faixas entraram na soma. Não é um total global definitivo, é um agregado de estimativas locais.

A decisão

Junta tudo: o que a gente mostrava era uma contagem de partições (não de linhas), estimada a partir de metadados de arquivo (não contada), defasada pelo ciclo de compaction e agregada por faixa de token de cada nó. Um número desses, num campo que o usuário lê como "linhas", é pior que campo vazio. Com o campo vazio, o usuário desconfia e vai atrás. Com um 143 de aparência confiante, ele anota no relatório e segue a vida.

Então no Cassandra a gente parou de exibir. O rodapé fica em branco para contagem de linhas. Quem precisa do número real roda o count(*) de propósito, ciente de que é uma varredura e de que pode doer. Preferimos não responder a mentir com cara de precisão.

Pano de fundo

Nada disso é culpa do Cassandra. O que quebra é uma suposição que a gente carrega de banco de nó único: a de que sempre existe uma contagem de linhas barata guardada em algum canto do catálogo. Num armazenamento distribuído por token, com LSM e tombstone, essa conta simplesmente não está pronta em lugar nenhum.

No fim, cada engine responde bem a umas perguntas e mal a outras. Contagem de linhas no rodapé é uma das que o Cassandra não entrega de graça, e a gente preferiu respeitar isso a encher o espaço com um número bonito e falso.

Carregando publicação patrocinada...
1

Complementando o post: essa decisão de design foi difícil, mas necessária. No futuro, estamos avaliando a possibilidade de adicionar um tooltip (ícone de informação) ao lado do campo vazio, explicando brevemente por que o count(*) é custoso e como o Cassandra lida com partições. A ideia não é dar um número, mas sim educar sobre o 'porquê' do vazio. Agradeço os feedbacks da comunidade sobre como podemos tornar essa experiência ainda mais clara!