Zurück zur Startseite

ClickHouse-Aggregatfunktionen: uniqHLL12, quantileTDigest, topK

Vollständige Übersicht über ClickHouse-Aggregatfunktionen mit Beispielen an einer echten Wettentabelle. Behandelt Standard count/sum/avg, spezialisierte für eindeutige Zählungen (uniqExact, uniqHLL12, uniqCombined) mit einer Auswahltabelle nach Genauigkeit und Geschwindigkeit, Quantile (quantile, quantileTDigest, quantileExact) für Verteilungen, groupArray und gleitende Summen (groupArrayMovingSum), Top-Elemente via topK, bedingte Aggregate via -If-Kombinatoren (sumIf, countIf, avgIf), AggregateFunction-Typ und -State/-Merge-Kombinatoren für materialisierte Sichten, -OrDefault/-OrNull-Kombinatoren, runningAccumulate für kumulative Metriken. Enthält fertige Abfragen für täglichen GGR (Bruttospielertrag), Kohorten-Retention-Analyse und gleitende 7-Tage-Retention. Enthält Warnungen zu typischen Fehlern (count(DISTINCT), quantileExact bei großen Datenmengen).

ClickHouse-Aggregationen: von count() bis AggregateFunction mit -State/-Merge
Advertisement 728x90

ClickHouse-Aggregatfunktionen: Wie ich aufgehört habe, uniqHLL12 und quantileTDigest zu fürchten

In der Wettanalyse mussten wir eindeutige Spieler pro Stunde zählen. In PostgreSQL hätte ich COUNT(DISTINCT user_id) geschrieben und einen Kaffee geholt. In ClickHouse dauerte dieselbe Abfrage bei 500 Millionen Zeilen 30 Sekunden. Aber das Geschäft benötigte ein Dashboard, das sich alle 5 Sekunden aktualisiert.

Da entdeckte ich uniqHLL12() — approximatives Zählen mit 1-2% Fehler, aber in 0,2 Sekunden. Wir stellten darauf um, und das Dashboard lief. Der Direktor bemerkte den Unterschied in den Zahlen nicht, aber er bemerkte die Geschwindigkeit.

ClickHouse bietet nicht nur Standard-Mathematik, sondern auch Dutzende spezialisierter Aggregationen, von denen PostgreSQL nie geträumt hat. Unten ist alles, was ich in echten Projekten verwende.

Google AdInline article slot

1. Standard-Aggregate: Vertraut, aber schneller

-- Gesamtstatistik für alle Wetten
SELECT 
    count() AS total_bets,                    -- Anzahl der Wetten
    sum(amount) AS total_staked,              -- Gesamtwetteinsatz
    avg(amount) AS avg_bet,                   -- Durchschnittswette
    min(created_at) AS first_bet_time,        -- erste Wette
    max(created_at) AS last_bet_time,         -- letzte Wette
    max(amount) - min(amount) AS range        -- Spanne
FROM betting.bets
WHERE created_at >= today() - 7;

Unterschied zu PostgreSQL: count(*) und count() funktionieren gleich. Aber count(DISTINCT user_id) ist langsam — verwenden Sie spezialisierte Funktionen.

2. Eindeutige Benutzer: Genauigkeit vs. Geschwindigkeit

ClickHouse hat drei Ansätze zum Zählen eindeutiger Werte:

-- Genau, aber langsam (30 Sekunden bei 500 Millionen Zeilen)
SELECT count(DISTINCT user_id) FROM betting.bets;

-- Genau, aber ohne syntaktischen Zucker
SELECT uniqExact(user_id) FROM betting.bets;

-- Approximativ, schnell (0,2 Sekunden, 1-2% Fehler)
SELECT uniq(user_id) FROM betting.bets;

-- HyperLogLog mit kontrolliertem Fehler (wir verwenden dies)
SELECT uniqHLL12(user_id) FROM betting.bets;

-- Noch schneller, aber größerer Fehler
SELECT uniqCombined(user_id) FROM betting.bets;

Wann was verwenden:

Google AdInline article slot
Funktion Fehler Geschwindigkeit Wo ich es anwende
uniqExact() 0% Langsam Steuerberichte, exakte Auszahlungen
uniqHLL12() 1-2% Sehr schnell Dashboards, Trends, KPIs
uniq() 2-4% Schnell Echtzeit-Analysen
uniqCombined() 5-8% Sofort Explorative Analysen, Schätzungen

Reales Produktionsbeispiel: Um eindeutige Spieler pro Stunde in einem Live-Dashboard zu zählen, verwenden wir uniqHLL12(user_id). 99% Genauigkeit ist akzeptabel, und das Dashboard aktualisiert sich alle 3 Sekunden statt 30.

3. Quantile: Wettverteilung ohne Histogramme

Die Frage „Wie viele Spieler setzen weniger als 100 Rubel und wie viele mehr als 1000?“ betrifft Quantile.

-- 50. Perzentil (Median)
SELECT quantile(0.5)(amount) FROM betting.bets;

-- 90. Perzentil (90% der Wetten liegen unter diesem Betrag)
SELECT quantile(0.9)(amount) FROM betting.bets;

-- Mehrere Quantile auf einmal
SELECT quantiles(0.5, 0.75, 0.9, 0.95, 0.99)(amount) FROM betting.bets;

-- Approximatives Quantil (10x schneller)
SELECT quantileTDigest(0.9)(amount) FROM betting.bets;

-- Genau, aber langsam (In-Memory-Sortierung)
SELECT quantileExact(0.9)(amount) FROM betting.bets;

Wo ich auf die Nase gefallen bin: quantileExact() bei 1 Milliarde Zeilen frisst den gesamten Speicher. Wir wechselten zu quantileTDigest() — 0,5% Fehler, 100 MB Speicher statt 8 GB.

Google AdInline article slot

Realer Anwendungsfall: Schwellenwerte für Betrugserkennung bestimmen. Wenn 99% der Wetten unter 50.000 Rubel liegen und ein Spieler 500.000 setzt — zur Überprüfung senden.

4. groupArray: Werte in einem Array sammeln

Manchmal muss man nicht aggregieren, sondern alle Werte erhalten.

-- Alle Wetten eines Benutzers pro Tag in einem Array
SELECT 
    user_id,
    toDate(created_at) AS day,
    groupArray(amount) AS amounts,
    groupArray(odds) AS odds_list,
    arrayMap(x -> x * 2, amounts) AS doubled  -- Arbeiten mit Array
FROM betting.bets
WHERE created_at >= today() - 7
GROUP BY user_id, day
LIMIT 10;

Fortgeschrittener Fall: Gleitende Summen und Durchschnitte.

-- Gleitende Summe der letzten 3 Wetten pro Benutzer
SELECT 
    user_id,
    created_at,
    amount,
    groupArrayMovingSum(3)(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS moving_sum
FROM betting.bets
WHERE user_id = 1001
ORDER BY created_at;

Wann dies wirklich nötig ist: Analyse von Wettsequenzen — ein Bot setzt identische Beträge hintereinander, ein Live-Spieler variiert.

5. topK: Keine Notwendigkeit, exakte Werte zu zählen

Frage: „Was sind die 5 beliebtesten Sportarten?“ GROUP BY sport ORDER BY count() DESC LIMIT 5 funktioniert, aber bei 1 Milliarde Zeilen baut es eine Hash-Tabelle für eine Million eindeutige Werte auf.

-- Approximative Top (schnell)
SELECT topK(5)(sport) FROM betting.bets;

-- Ergebnis: ['football', 'basketball', 'tennis', 'hockey', 'mma']

-- Genau, aber langsam
SELECT sport, count() AS cnt 
FROM betting.bets 
GROUP BY sport 
ORDER BY cnt DESC 
LIMIT 5;

Geschwindigkeitsunterschied: Bei 10 Milliarden Zeilen führt topK in 0,5 Sekunden aus, exaktes GROUP BY in 15 Sekunden. Für ein automatisch aktualisierendes Dashboard ist die Wahl offensichtlich.

6. -If-Kombinatoren: Bedingte Aggregation ohne Unterabfragen

Statt SUM(CASE WHEN ...) schreiben Sie sumIf() — es liest sich besser und läuft schneller.

-- Gewinnende und verlierende Wetten in einer Zeile
SELECT 
    user_id,
    countIf(outcome = 'win') AS wins,
    countIf(outcome = 'loss') AS losses,
    sumIf(amount, outcome = 'win') AS winning_stake,
    sumIf(amount, outcome = 'loss') AS losing_stake,
    avgIf(odds, outcome = 'win') AS avg_win_odds,
    -- Gewinnrate über Bedingung
    round(wins / (wins + losses), 4) AS win_rate
FROM betting.bets
WHERE created_at >= today() - 7
GROUP BY user_id
HAVING wins + losses > 50
ORDER BY win_rate DESC
LIMIT 20;

Andere -If-Funktionen: avgIf(), minIf(), maxIf(), anyIf(), uniqIf(), quantileIf().

7. AggregateFunction-Typ und -State/-Merge-Kombinatoren: Für materialisierte Ansichten

ClickHouses mächtigstes Werkzeug: Sie können nicht Daten, sondern ZWISCHENZUSTÄNDE der Aggregation speichern.

-- Tabelle mit Aggregatzuständen
CREATE TABLE betting.daily_agg
(
    day Date,
    user_id UInt64,
    total_bets AggregateFunction(count, UInt64),
    total_amount AggregateFunction(sum, Decimal(18,2)),
    unique_sports AggregateFunction(uniq, String)
)
ENGINE = AggregatingMergeTree()
ORDER BY (day, user_id);

-- Einfügen mit -State
INSERT INTO betting.daily_agg
SELECT 
    toDate(created_at) AS day,
    user_id,
    countState() AS total_bets,
    sumState(amount) AS total_amount,
    uniqState(sport) AS unique_sports
FROM betting.bets
GROUP BY day, user_id;

-- Ergebnis mit -Merge abrufen
SELECT 
    day,
    user_id,
    countMerge(total_bets) AS bets,
    sumMerge(total_amount) AS total_staked,
    uniqMerge(unique_sports) AS unique_sports_count
FROM betting.daily_agg
GROUP BY day, user_id;

Wo ich dies verwende: Materialisierte Ansichten für stündliche Aggregation. Statt jedes Mal 2 Milliarden Zeilen neu zu berechnen, speichere ich Zustände und führe sie einfach zusammen.

8. Andere Kombinatoren: -OrDefault, -OrNull, -Array

-- -OrDefault: gibt Standardwert statt NULL zurück
SELECT avgOrDefault(amount, 0) FROM betting.bets WHERE 1=0;  -- 0, nicht NULL

-- -OrNull: gibt NULL zurück, wenn keine Zeilen
SELECT avgOrNull(amount) FROM betting.bets WHERE 1=0;  -- NULL

-- -Array: Aggregation über Array-Elemente
SELECT groupArrayArray([[1,2], [3,4], [5,6]]) AS flattened;
-- glätten: [1,2,3,4,5,6]

9. runningAccumulate: Kumulative Summen (Fensterfunktionen auf Steroiden)

ClickHouse unterstützt Fensterfunktionen, aber runningAccumulate ist eine ältere (und manchmal schnellere) Form.

-- Kumulative Summe der Wetten pro Tag
SELECT 
    toDate(created_at) AS day,
    sum(amount) AS daily_amount,
    runningAccumulate(sum(amount)) OVER (ORDER BY day) AS cumulative_amount
FROM betting.bets
WHERE created_at >= today() - 30
GROUP BY day
ORDER BY day;

Warum ich manchmal Fensterfunktionen bevorzuge: runningAccumulate erfordert strenge Reihenfolge und unterstützt kein PARTITION BY. Jetzt schreibe ich sum(amount) OVER (ORDER BY day).

10. Praktische Beispiele: GGR, Kohorten, rollierende Retention

Beispiel 1: Täglicher GGR (Bruttospielertrag)

GGR = Gesamteinsätze - Gesamtauszahlungen.

SELECT 
    toDate(created_at) AS day,
    sum(amount) AS total_staked,
    sumIf(amount * odds, outcome = 'win') AS total_paid,
    total_staked - total_paid AS ggr,
    round(ggr / total_staked, 4) AS hold_percentage
FROM betting.bets
GROUP BY day
ORDER BY day DESC
LIMIT 30;

Beispiel 2: Kohortenanalyse (Spielerbindung nach Tag)

WITH cohorts AS (
    SELECT 
        user_id,
        toDate(min(created_at)) AS cohort_day
    FROM betting.bets
    GROUP BY user_id
),
user_activity AS (
    SELECT 
        b.user_id,
        c.cohort_day,
        toDate(b.created_at) AS activity_day,
        datediff('day', c.cohort_day, activity_day) AS day_number
    FROM betting.bets b
    JOIN cohorts c ON b.user_id = c.user_id
    WHERE day_number <= 30
)
SELECT 
    cohort_day,
    day_number,
    uniqHLL12(user_id) AS active_users
FROM user_activity
GROUP BY cohort_day, day_number
ORDER BY cohort_day DESC, day_number;

Beispiel 3: Rollierende 7-Tage-Retention

SELECT 
    toDate(created_at) AS day,
    uniqHLL12(user_id) AS dau,
    -- Benutzer, die auch vor 7 Tagen aktiv waren
    uniqHLL12If(user_id, 
        created_at >= today() - 7 AND created_at < today() - 6
    ) AS retained_users,
    round(retained_users / uniqHLL12If(user_id, 
        created_at >= today() - 14 AND created_at < today() - 13
    ), 4) AS retention_7d
FROM betting.bets
GROUP BY day
ORDER BY day DESC;

Häufige Fehler bei der Arbeit mit Aggregationen

Fehler 1: COUNT(DISTINCT col) bei Milliarden von Zeilen.
Lösung: uniqHLL12(col) oder uniqExact(col), wenn Genauigkeit erforderlich ist.

Fehler 2: GROUP BY auf einer hochkardinalen Spalte (user_id) ohne Filter.
Lösung: immer WHERE oder HAVING mit einer Grenze hinzufügen.

Fehler 3: quantileExact() bei großen Datenmengen.
Lösung: quantileTDigest() oder quantile(0.9) ohne Exact.

Fehler 4: arrayJoin innerhalb einer Aggregation ohne Verständnis verwenden.
Lösung: Denken Sie daran, dass arrayJoin Zeilen multipliziert. Besser Daten denormalisieren.

Was kommt als Nächstes

Aggregationen sind das Herz der Analytik. Nächster Artikel — über Fensterfunktionen und statistische Tests in ClickHouse.


Vorherige:
Nächste: ClickHouse-Konfiguration: Wie ich die Produktion eingerichtet habe und mir nicht ins Bein geschossen habe

— Editorial Team

Advertisement 728x90

Weiterlesen