ClickHouse ist sehr schnell und sehr wörtlich. Es tut, worum Sie gebeten haben, und mehrere der Dinge, um die Sie bitten können, geben eine Antwort zurück, die auf eine Weise falsch ist, vor der nichts warnt: kein Fehler, kein Null, kein leeres Ergebnis. Bloß eine Zahl, die ein bisschen zu klein ist, oder ein bisschen zu groß, oder der Text „NaN" dort, wo ein Prozentwert stehen sollte.
Dies sind drei, auf die wir beim Bauen einer Analytics darauf gestoßen sind. Jede war live, bevor es jemandem auffiel, und das ist der Teil, den aufzuschreiben lohnt.
1. Ein Aggregatzustand ist keine Zahl
Die Tageszusammenfassung speichert eindeutige Besucher und Sitzungen als AggregateFunction(uniq, UUID), eine serialisierte Skizze, keine Zählung. Das ist der ganze Zweck: Man kann Skizzen über Tage hinweg zusammenführen, ohne zu den rohen Zeilen zurückzugehen.
Die Falle ist, dass die Spalte weiterhin wie eine Spalte aussieht. Zwei Dinge folgen daraus, und das zweite ist das, was zubeißt.
Zeilen werden nicht zusammengefasst, bis eine Hintergrund-Zusammenführung läuft. Derselbe Schlüssel kann mehrfach vorkommen, die Spalte zu lesen und zu summieren, was man bekommt, meldet also zu wenig, leise und plausibel. Es gibt keinen Fehler und kein offensichtliches Symptom; die Zahl ist schlicht um einen Betrag niedriger als die Wahrheit, der sich danach ändert, wann die letzte Zusammenführung war. Sie brauchen uniqMerge und ein GROUP BY, immer.
Und ein SQL-Client kann den rohen Zustand überhaupt nicht darstellen. Der JDBC-Treiber von ClickHouse kann uniq-Zustände nur über native Ganzzahltypen deserialisieren, jedes SELECT * über einen uniq-Zustand auf einer UUID scheitert also rundheraus:
Only native integer types are supported but we got: UUID
Was heißt, dass ein Klick auf die Tabelle in DataGrip oder DBeaver (das Gewöhnlichste, was jemand beim Fehlersuchen tut) eine Ausnahme wirft und wie eine kaputte Verbindung aussieht statt wie eine Datenmodellierungsentscheidung, die Sie vor Monaten getroffen haben.
Die Behebung ist eine View, die die Zustände zusammenführt und schlichte UInt64 freilegt, damit das Durchsehen funktioniert und die Zahlen stimmen:
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;
Der Anwendungscode fragte die Basistabelle unmittelbar mit uniqMerge ab. Die View gab es, damit ein Mensch, der die Datenbank öffnet, die Wahrheit sieht statt einer Ausnahme.
Im September 2026 haben wir das Rollup entfernt. Gelesen hat es nie jemand: das Dashboard fragt die Rohtabellen ab, und das Aggregat wurde bei jedem Insert neu gebaut, ohne einen einzigen Leser.
2. avgIf über keine Zeilen gibt NaN zurück, nicht NULL
Diese hier erreichte den Produktivbetrieb und blieb dort.
avgIf(duration_ms, is_bounce = 0)
Über einen Zeitraum ohne passende Zeilen gibt ClickHouse Float64 nan zurück. Nicht NULL, was Sie behandelt hätten, denn Nullwerte zu behandeln ist etwas, woran alle denken. nan.
Zwei Folgen, und sie landen bei verschiedenen Menschen:
- Das Dashboard stellte den wörtlichen Text „NaN" dar, wo eine Absprungrate und eine mittlere Dauer stehen sollten. Hässlich, offensichtlich falsch, schnell gemeldet.
- Die Query-API gab HTTP 500 zurück. JSON kann einen nicht endlichen Double überhaupt nicht kodieren, die Serialisierung warf also, und zwar für genau die Zeiträume, die ein Kunde am ehesten abfragt: heute, vor dem ersten Besucher des Morgens, und jeder Bereich über einer ruhigen Website.
Das zweite ist viel schlimmer als das erste und wurde viel später gefunden. Ein Dashboard, das „NaN" sagt, bekommt binnen einer Stunde eine Fehlermeldung. Eine API, die nur dann 500 liefert, wenn die Antwort „nichts ist geschehen" gewesen wäre, sieht wie ein unzuverlässiger Endpunkt aus und bekommt eine Wiederholschleife statt einer Fehlermeldung.
Die Behebung ist eine Zeile an der Lesegrenze: alles nicht Endliche auf null abflachen, was dieselbe Antwort ist, die der Weg mit Nenner null ohnehin gibt, die beiden stimmen also überein:
var value = Convert.ToDouble(reader.GetValue(ordinal));
return double.IsFinite(value) ? value : 0;
Null ist hier ehrlich, weil die Besuchszahl daneben ebenfalls null liest. Der Zusammenhang ist es, was eine Null wahrhaftig macht statt einer Vermutung.
3. Ein ReplacingMergeTree, der ohne FINAL eine Lüge ist
Zeit auf der Seite ist nicht der Abstand zwischen zwei Seitenaufrufen. Ein Besucher, der einen Tab im Hintergrund offen lässt, liest nicht, und die letzte Seite eines Besuchs hat keine nächste, von der sich abziehen ließe. Der Tracker meldet also sichtbare Zeit, und er meldet sie mehr als einmal: Wer zu einem anderen Tab und zurück und wieder weg wechselt, hat die Seite wirklich zweimal ausgeblendet, und jede Meldung trägt die laufende kumulierte Summe.
Die zu summieren würde dieselbe Minute mehrfach zählen und eine wild überhöhte Zeit auf der Seite melden. Die Tabelle ist also ein ReplacingMergeTree, mit Schlüssel je Besuch und Seite, versioniert nach den engagierten Millisekunden: die längste Meldung gewinnt, und die früheren Teilstücke fallen in sie zusammen.
Was heißt, dass jeder Lesevorgang FINAL braucht, und ein Lesevorgang, der es vergisst, sieht die Teilzeilen ebenso wie die endgültige. Die Zahl ist kein Müll; sie ist plausibel, bloß zu groß. Das ist die Art falsch, die eine Durchsicht übersteht.
Der ehrliche Preis dieses Schlüssels, denn ein solcher Entwurf hat immer einen: Ein Besucher, der später im selben Besuch auf dieselbe Seite zurückkehrt, erzeugt eine Zeile statt zweier, und der kürzere Messwert wird verworfen statt addiert. Er zählt zu wenig in einem seltenen Fall, was für eine Kennzahl, deren ganzer Zweck ist, das Überschätzen von Aufmerksamkeit zu verhindern, die richtige Richtung ist, in der man falsch liegt.
Was die drei gemeinsam haben
Keine von ihnen warf in dem Augenblick einen Fehler, in dem der Fehler gemacht wurde. Zwei erzeugten Zahlen, die völlig vernünftig aussahen, und die dritte erzeugte einen Absturz an ganz anderer Stelle, Stunden oder Wochen später.
Die Lehre, die wir tatsächlich gezogen haben: Schreiben Sie zu jedem Aggregat auf, wie eine falsche Antwort aussähe, und prüfen Sie auf diese Form statt auf eine Ausnahme. Ein Analytics-Produkt, das 8 % zu niedrig liegt, ist schlimmer als eines, das ausgefallen ist, denn niemand meldet einen Fehler gegen 8 %.
Wenn Sie sehen wollen, wie die Zahlen aussehen, wenn sie stimmen: Es gibt eine Live-Demo auf einem echten Shop, ohne Konto, und das Glossar sagt genau, was jede davon zählt.
Alle drei dieser Fehler saßen hinter dem Analytics-Produkt, und keiner davon meldete sich, was das Argument dafür ist, eine Zahl gegen eine zweite Methode zu prüfen, statt darauf zu vertrauen, dass sie plausibel aussieht.