ClickHouse AggregatingMergeTree Schéma State/Merge
Problème
Lors de l'interrogation de tables ou de vues matérialisées utilisant AggregatingMergeTree avec des fonctions state
(sumState, uniqState, countState), les requêtes échouent ou retournent des résultats incorrects car les
colonnes contiennent des objets d'état d'agrégation, non des valeurs numériques régulières.
Contexte / Conditions déclencheurs
- La requête échoue avec des erreurs de type lors de l'utilisation de
sum()sur les colonnes AggregateFunction - Création de vues qui interrogent des vues matérialisées AggregatingMergeTree
- Les types de colonnes s'affichent comme
AggregateFunction(sum, UInt64)au lieu deUInt64 - La migration crée une vue joignant une vue matérialisée d'agrégation existante
- Obtention de valeurs NULL ou inattendues lors de l'agrégation de colonnes pré-agrégées
Cause racine
AggregatingMergeTree stocke des états d'agrégation intermédiaires, non des valeurs finales. Quand tu définis :
CREATE MATERIALIZED VIEW stats
ENGINE = AggregatingMergeTree()
ORDER BY (user_id)
AS SELECT
user_id,
sumState(amount) as total_amount, -- AggregateFunction(sum, UInt64)
uniqState(session_id) as unique_sessions, -- AggregateFunction(uniq, String)
countState() as event_count -- AggregateFunction(count)
FROM events
GROUP BY user_id;
Les colonnes total_amount, unique_sessions et event_count ne sont PAS des nombres réguliers.
Ce sont des blobs binaires représentant l'état intermédiaire de l'agrégation.
Incorrect :
SELECT user_id, sum(total_amount) FROM stats GROUP BY user_id;
-- Erreur : impossible d'utiliser sum() sur le type AggregateFunction
Correct :
SELECT user_id, sumMerge(total_amount) FROM stats GROUP BY user_id;
-- Retourne la valeur numérique finalisée
Solution
Mapping des fonctions State vers les fonctions Merge
| Fonction State | Fonction Merge | Objectif |
|---|---|---|
sumState(x) |
sumMerge(x) |
Agrégation de somme |
countState() |
countMerge(x) |
Agrégation de comptage |
uniqState(x) |
uniqMerge(x) |
Comptage unique (HyperLogLog) |
avgState(x) |
avgMerge(x) |
Moyenne |
minState(x) |
minMerge(x) |
Minimum |
maxState(x) |
maxMerge(x) |
Maximum |
anyState(x) |
anyMerge(x) |
N'importe quelle valeur |
groupArrayState(x) |
groupArrayMerge(x) |
Agrégation de tableau |
Exemple de correction
Avant (cassé) :
CREATE VIEW leaderboard AS
SELECT
stats.user_id,
sum(stats.daily_views) AS views, -- INCORRECT
sum(stats.daily_unique) AS uniques, -- INCORRECT
sum(stats.videos_watched) AS videos -- INCORRECT
FROM daily_stats stats
GROUP BY stats.user_id;
Après (corrigé) :
CREATE VIEW leaderboard AS
SELECT
stats.user_id,
sumMerge(stats.daily_views) AS views, -- Correct
uniqMerge(stats.daily_unique) AS uniques, -- Correct
countMerge(stats.videos_watched) AS videos -- Correct
FROM daily_stats stats
GROUP BY stats.user_id;
Identification des colonnes affectées
Vérifie le schéma de la table pour voir quelles colonnes sont des états d'agrégation :
DESCRIBE TABLE your_table;
La sortie affiche les types de colonne comme :
daily_views AggregateFunction(sum, UInt64)
daily_unique AggregateFunction(uniq, String)
videos_watched AggregateFunction(count)
Toute colonne avec le type AggregateFunction(...) requiert la fonction *Merge() correspondante.
Vérification
- Vérifie la définition de ta vue matérialisée pour les fonctions
*State() - Assure-toi que toutes les requêtes utilisent les fonctions
*Merge()correspondantes - Teste que la requête retourne les valeurs numériques attendues, non NULL ou des blobs binaires
-- Devrait retourner des nombres réels
SELECT sumMerge(total_views), uniqMerge(unique_visitors)
FROM aggregated_stats
WHERE stat_date >= today() - 7;
Exemple
Migration 020 (crée la table d'agrégation) :
CREATE MATERIALIZED VIEW creator_daily_stats
ENGINE = AggregatingMergeTree()
ORDER BY (video_author_pubkey, stat_date)
AS SELECT
video_author_pubkey,
toDate(created_at) as stat_date,
sumState(view_count) as daily_views, -- Fonction state
uniqState(viewer_hash) as daily_unique_viewers, -- Fonction state
sumState(toFloat64(total_loops)) as daily_loops,-- Fonction state
countState() as videos_watched -- Fonction state
FROM view_counts
GROUP BY video_author_pubkey, toDate(created_at);
Migration 033 (interroge la table d'agrégation - CORRIGÉE) :
CREATE VIEW leaderboard_creators_day AS
SELECT
cds.video_author_pubkey AS pubkey,
p.name,
sumMerge(cds.daily_views) AS views, -- Fonction merge
uniqMerge(cds.daily_unique_viewers) AS unique_viewers, -- Fonction merge
sumMerge(cds.daily_loops) AS loops, -- Fonction merge
countMerge(cds.videos_watched) AS videos_with_views -- Fonction merge
FROM creator_daily_stats cds
LEFT JOIN user_profiles p ON cds.video_author_pubkey = p.pubkey
WHERE cds.stat_date >= today() - 1
GROUP BY cds.video_author_pubkey, p.name
ORDER BY views DESC;
Notes
-
SummingMergeTree est différent : Il stocke des valeurs régulières et les somme lors des fusions. Avec SummingMergeTree, tu utilises
sum()régulier dans les requêtes. Seul AggregatingMergeTree utilise le schéma State/Merge. -
Pourquoi utiliser AggregatingMergeTree ? : Pour les comptages uniques (
uniq), tu ne peux pas simplement additionner les comptages de différentes parties — cela causerait un surcomptage. AggregatingMergeTree préserve l'état HyperLogLog pour que la fusion donne des comptages uniques corrects sur les partitions. -
Performance : Les fonctions
*Merge()sont efficaces — elles sont conçues pour combiner des états d'agrégation pré-calculés, non pour retraiter les données brutes. -
L'ordre des migrations compte : Si la migration A crée une vue AggregatingMergeTree, et la migration B crée une vue qui l'interroge, la migration B doit utiliser les fonctions
*Merge().