ClickHouse è molto veloce e molto letterale. Fa quello che gli hai chiesto, e parecchie delle cose che gli puoi chiedere restituiscono una risposta sbagliata in un modo su cui niente ti avvisa: nessun errore, nessun nullo, nessun risultato vuoto. Soltanto un numero un po' troppo piccolo, o un po' troppo grande, o il testo «NaN» dove dovrebbe esserci una percentuale.
Questi sono tre in cui siamo incappati costruendoci sopra dell'analitica. Ognuno era in produzione prima che qualcuno se ne accorgesse, che è la parte che vale la pena scrivere.
1. Uno stato aggregato non è un numero
L'aggregato giornaliero conserva i visitatori unici e le sessioni come AggregateFunction(uniq, UUID), uno schizzo serializzato, non un conteggio. È tutto il punto: puoi fondere gli schizzi di più giorni senza tornare alle righe grezze.
La trappola è che la colonna ha ancora l'aspetto di una colonna. Ne discendono due cose, ed è la seconda che morde.
Le righe non vengono collassate finché non gira una fusione in secondo piano. La stessa chiave può esistere più volte, quindi leggere la colonna e sommare quello che ottieni riporta in difetto, in silenzio e in modo plausibile. Non c'è nessun errore e nessun sintomo ovvio; il numero è semplicemente più basso della verità di una quantità che cambia a seconda di quando è avvenuta l'ultima fusione. Ti servono uniqMerge e un GROUP BY, sempre.
E un client SQL non riesce proprio a mostrare lo stato grezzo. Il driver JDBC di ClickHouse riesce a deserializzare gli stati uniq solo sopra tipi interi nativi, quindi qualunque SELECT * sopra uno stato uniq su un UUID fallisce in partenza:
Only native integer types are supported but we got: UUID
Il che vuol dire che cliccare la tabella in DataGrip o DBeaver (la cosa più banale che chiunque faccia mentre debugga) lancia un'eccezione, e sembra una connessione rotta invece di una decisione di modellazione dei dati che hai preso mesi fa.
La soluzione è una vista che fonde gli stati ed espone un semplice UInt64, così navigare funziona e i numeri sono giusti:
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;
Il codice dell'applicazione interrogava la tabella di base con uniqMerge direttamente. La vista esisteva perché una persona che apre la base dati veda la verità invece di un'eccezione.
A settembre 2026 abbiamo rimosso il rollup. Non lo aveva mai letto nessuno: il cruscotto interroga le tabelle grezze, e l'aggregato veniva ricostruito a ogni inserimento senza un solo lettore.
2. avgIf su zero righe restituisce NaN, non NULL
Questo è arrivato in produzione ed è rimasto.
avgIf(duration_ms, is_bounce = 0)
Su un periodo senza righe corrispondenti, ClickHouse restituisce il Float64 nan. Non NULL, che avresti gestito, perché gestire i nulli è una cosa che tutti si ricordano di fare. nan.
Due conseguenze, e cadono addosso a persone diverse:
- La dashboard ha mostrato il testo letterale «NaN» dove dovevano esserci una frequenza di rimbalzo e una durata media. Brutto, ovviamente sbagliato, segnalato in fretta.
- L'API di interrogazione ha restituito HTTP 500. Il JSON non può proprio codificare un doppio non finito, quindi la serializzazione ha lanciato un'eccezione, e per esattamente i periodi che un cliente ha più probabilità di interrogare: oggi, prima della prima persona della mattina, e qualunque intervallo su un sito tranquillo.
La seconda è molto peggiore della prima ed è stata trovata molto più tardi. Una dashboard che dice «NaN» riceve una segnalazione di difetto entro un'ora. Un'API che dà 500 solo quando la risposta sarebbe stata «non è successo niente» sembra un punto di accesso instabile, e riceve un ciclo di nuovi tentativi invece di una segnalazione.
La soluzione è una riga al confine della lettura: appiattire a zero tutto quello che non è finito, che è la stessa risposta che il percorso con denominatore zero dà già, così i due vanno d'accordo:
var value = Convert.ToDouble(reader.GetValue(ordinal));
return double.IsFinite(value) ? value : 0;
Lo zero qui è onesto perché il conteggio delle visite accanto legge anche lui zero. È il contesto che rende uno zero veritiero invece che un'ipotesi.
3. Una ReplacingMergeTree che è una bugia senza FINAL
Il tempo sulla pagina non è lo spazio fra due pagine viste. Una persona che lascia una scheda aperta in secondo piano non sta leggendo, e l'ultima pagina di una visita non ha nessuna pagina successiva da cui sottrarsi. Quindi il tracker riporta il tempo visibile, e lo riporta più di una volta: chi passa a un'altra scheda e torna e passa di nuovo ha davvero nascosto la pagina due volte, e ogni relazione porta il totale cumulativo corrente.
Sommarle conterebbe lo stesso minuto più volte e riporterebbe un tempo sulla pagina enormemente esagerato. Quindi la tabella è una ReplacingMergeTree con chiave per visita e pagina, versionata dai millisecondi di coinvolgimento: la relazione più lunga vince e le parziali precedenti ci collassano dentro.
Il che vuol dire che ogni lettura ha bisogno di FINAL, e una lettura che se ne dimentica vede le righe parziali oltre a quella finale. Il numero non è spazzatura; è plausibile, solo troppo grande. È quel tipo di sbagliato che sopravvive a una revisione.
Il costo onesto di quella chiave, dato che un disegno del genere ne ha sempre uno: una persona che torna sulla stessa pagina più avanti nella stessa visita produce una riga invece di due, e la lettura più corta viene buttata via invece che sommata. Conta in difetto in un caso poco comune, che è la direzione giusta in cui sbagliare per una metrica il cui scopo intero è impedire che l'attenzione venga esagerata.
Che cosa hanno in comune i tre
Nessuno di loro ha lanciato un errore nel momento in cui l'errore è stato commesso. Due hanno prodotto numeri dall'aria del tutto ragionevole, e il terzo ha prodotto un crash da tutt'altra parte, ore o settimane dopo.
La lezione che ne abbiamo davvero tratto: per ogni aggregato, scriviti che aspetto avrebbe una risposta sbagliata, e cerca quella forma invece di un'eccezione. Un prodotto di analitica che è basso dell'8% è peggio di uno che è giù, perché nessuno apre una segnalazione contro l'8%.
Se vuoi vedere che aspetto hanno i numeri quando sono giusti, c'è una dimostrazione dal vivo che gira su un negozio vero, senza nessun account, e il glossario dice esattamente che cosa conta ognuno.
Tutti e tre quei difetti stavano dietro al prodotto Analytics, e nessuno di loro si è annunciato, che è l'argomento per verificare un numero contro un secondo metodo invece di fidarsi del fatto che abbia un'aria plausibile.