O ClickHouse é muito rápido e muito literal. Faz o que lhe pediste, e várias das coisas que lhe podes pedir devolvem uma resposta que está errada de uma maneira sobre a qual nada te avisa: sem erro, sem nulo, sem resultado vazio. Apenas um número um bocadinho pequeno de mais, ou um bocadinho grande de mais, ou o texto "NaN" onde devia estar uma percentagem.
Estas são três com que demos ao construir analítica em cima dele. Cada uma esteve em produção antes de alguém reparar, que é a parte que vale a pena apontar.
1. Um estado agregado não é um número
O agregado diário guarda os visitantes únicos e as sessões como AggregateFunction(uniq, UUID), um esboço serializado, não uma contagem. É esse o objetivo todo: podes fundir esboços de vários dias sem voltar às linhas em bruto.
A armadilha é que a coluna continua a parecer uma coluna. Daí decorrem duas coisas, e é a segunda que morde.
As linhas não são colapsadas enquanto não correr uma fusão em segundo plano. A mesma chave pode existir várias vezes, portanto ler a coluna e somar o que se recebe reporta a menos, em silêncio e de forma plausível. Não há erro nenhum nem sintoma óbvio; o número está pura e simplesmente abaixo da verdade por uma quantidade que muda conforme a última fusão. Precisas do uniqMerge e de um GROUP BY, sempre.
E um cliente de SQL não consegue sequer apresentar o estado em bruto. O controlador JDBC do ClickHouse só consegue desserializar estados uniq sobre tipos inteiros nativos, portanto qualquer SELECT * sobre um estado uniq num UUID falha à cabeça:
Only native integer types are supported but we got: UUID
O que quer dizer que clicar na tabela no DataGrip ou no DBeaver (a coisa mais banal que alguém faz enquanto depura) lança uma exceção, e parece uma ligação partida em vez de uma decisão de modelação de dados que tomaste há meses.
A solução é uma vista que funde os estados e expõe um UInt64 simples, para que navegar funcione e os números estejam certos:
CREATE VIEW stats_daily_v AS
SELECT
site_id, date, path,
sum(page_views) AS page_views,
uniqMerge(unique_visitors) AS unique_visitors,
uniqMerge(sessions) AS sessions
FROM stats_daily
GROUP BY site_id, date, path;
O código da aplicação interrogava a tabela de base com o uniqMerge diretamente. A vista existia para que uma pessoa que abra a base de dados veja a verdade em vez de uma exceção.
Em setembro de 2026 removêmos o rollup. Nunca nada o leu: o painel interroga as tabelas em bruto, e o agregado era reconstruído em cada inserção sem um único leitor.
2. O avgIf sobre zero linhas devolve NaN, não NULL
Este chegou a produção e lá ficou.
avgIf(duration_ms, is_bounce = 0)
Num período sem linhas coincidentes, o ClickHouse devolve o Float64 nan. Não NULL, que terias tratado, porque tratar nulos é coisa de que toda a gente se lembra. nan.
Duas consequências, e caem em cima de pessoas diferentes:
- O painel apresentou o texto literal "NaN" onde devia estar uma taxa de rejeição e uma duração média. Feio, obviamente errado, reportado depressa.
- A API de perguntas devolveu HTTP 500. O JSON não consegue codificar de todo um duplo não finito, portanto a serialização lançou uma exceção, e precisamente para os períodos que um cliente tem mais probabilidade de interrogar: hoje, antes do primeiro visitante da manhã, e qualquer intervalo sobre um sítio sossegado.
A segunda é muito pior do que a primeira e foi encontrada muito mais tarde. Um painel que diz "NaN" recebe um relatório de defeito dentro de uma hora. Uma API que dá 500 só quando a resposta teria sido "não aconteceu nada" parece um ponto de acesso instável, e recebe um ciclo de reenvios em vez de um relatório de defeito.
A solução é uma linha na fronteira de leitura: achatar tudo o que não seja finito para zero, que é a mesma resposta que o caminho do denominador zero já dá, portanto os dois concordam:
var value = Convert.ToDouble(reader.GetValue(ordinal));
return double.IsFinite(value) ? value : 0;
O zero é honesto aqui porque a contagem de visitas ao lado dele também lê zero. É o contexto que torna um zero verdadeiro em vez de um palpite.
3. Uma ReplacingMergeTree que é uma mentira sem FINAL
O tempo na página não é o intervalo entre duas páginas vistas. Um visitante que deixa um separador aberto em segundo plano não está a ler, e a última página de uma visita não tem página seguinte a que subtrair-se. Portanto o rastreador reporta o tempo visível, e reporta-o mais do que uma vez: alguém que muda de separador e volta e muda outra vez escondeu genuinamente a página duas vezes, e cada relato traz o total cumulativo corrente.
Somar isso contaria o mesmo minuto várias vezes e reportaria um tempo na página descaradamente exagerado. Portanto a tabela é uma ReplacingMergeTree com chave por visita e página, versionada pelos milissegundos de envolvimento: o relato mais longo ganha e os parciais anteriores colapsam nele.
O que quer dizer que todas as leituras precisam de FINAL, e uma leitura que se esqueça dele vê as linhas parciais além da final. O número não é lixo; é plausível, apenas grande de mais. É esse o tipo de erro que sobrevive a uma revisão.
O custo honesto dessa chave, já que um desenho destes tem sempre um: um visitante que volta à mesma página mais tarde na mesma visita produz uma linha em vez de duas, e a leitura mais curta é deitada fora em vez de somada. Conta a menos num caso pouco comum, que é o sentido certo em que estar errado numa métrica cuja finalidade inteira é impedir que a atenção seja exagerada.
O que as três têm em comum
Nenhuma delas lançou um erro no momento em que o engano foi cometido. Duas produziram números com um ar inteiramente razoável, e a terceira produziu um estoiro noutro sítio completamente diferente, horas ou semanas depois.
A lição que tirámos mesmo: para cada agregado, aponta com que cara ficaria uma resposta errada, e procura essa forma em vez de uma exceção. Um produto de analítica que está 8% abaixo é pior do que um que está em baixo, porque ninguém abre um defeito contra 8%.
Se quiseres ver com que cara ficam os números quando estão certos, há uma demonstração em direto a correr numa loja verdadeira, sem conta nenhuma, e o glossário diz exatamente o que cada um conta.
Os três defeitos estavam por trás do produto Analytics, e nenhum deles se anunciou, que é o argumento para conferir um número contra um segundo método em vez de confiar em que tem ar plausível.