Fix ClickHouse query errors when querying AggregatingMergeTree tables that use state functions. Use when: (1) Query fails with type mismatch on AggregateFunction columns, (2) Using sum/count on columns created with sumState/countState/uniqState, (3) Creating views that JOIN with materialized views using AggregatingMergeTree, (4) Getting unexpected results from aggregate columns that show as AggregateFunction(sum, ...) type. The *State() functions store intermediate aggregate states, not final...
Scanned 5/27/2026
Install via CLI
openskills install divinevideo/divine-mobile---
name: clickhouse-aggregatingmergetree-state-merge
description: |
Fix ClickHouse query errors when querying AggregatingMergeTree tables that use state functions.
Use when: (1) Query fails with type mismatch on AggregateFunction columns, (2) Using sum/count
on columns created with sumState/countState/uniqState, (3) Creating views that JOIN with
materialized views using AggregatingMergeTree, (4) Getting unexpected results from aggregate
columns that show as AggregateFunction(sum, ...) type. The *State() functions store intermediate
aggregate states, not final values - you must use *Merge() functions to finalize them.
author: Claude Code
version: 1.0.0
date: 2026-01-30
---
# ClickHouse AggregatingMergeTree State/Merge Pattern
## Problem
When querying tables or materialized views that use AggregatingMergeTree with state functions
(`sumState`, `uniqState`, `countState`), queries fail or return wrong results because the
columns contain aggregate state objects, not regular numeric values.
## Context / Trigger Conditions
- Query fails with type errors when using `sum()` on AggregateFunction columns
- Creating views that query AggregatingMergeTree materialized views
- Column types show as `AggregateFunction(sum, UInt64)` instead of `UInt64`
- Migration creates a view joining with an existing aggregating materialized view
- Getting NULL or unexpected values when aggregating pre-aggregated columns
## Root Cause
AggregatingMergeTree stores **intermediate aggregate states**, not final values. When you define:
```sql
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;
```
The columns `total_amount`, `unique_sessions`, and `event_count` are NOT regular numbers.
They're binary blobs representing the intermediate state of the aggregation.
**Wrong:**
```sql
SELECT user_id, sum(total_amount) FROM stats GROUP BY user_id;
-- Error: cannot use sum() on AggregateFunction type
```
**Correct:**
```sql
SELECT user_id, sumMerge(total_amount) FROM stats GROUP BY user_id;
-- Returns the finalized numeric value
```
## Solution
### Mapping State Functions to Merge Functions
| State Function | Merge Function | Purpose |
|----------------|----------------|---------|
| `sumState(x)` | `sumMerge(x)` | Sum aggregation |
| `countState()` | `countMerge(x)` | Count aggregation |
| `uniqState(x)` | `uniqMerge(x)` | Unique count (HyperLogLog) |
| `avgState(x)` | `avgMerge(x)` | Average |
| `minState(x)` | `minMerge(x)` | Minimum |
| `maxState(x)` | `maxMerge(x)` | Maximum |
| `anyState(x)` | `anyMerge(x)` | Any value |
| `groupArrayState(x)` | `groupArrayMerge(x)` | Array aggregation |
### Example Fix
**Before (broken):**
```sql
CREATE VIEW leaderboard AS
SELECT
stats.user_id,
sum(stats.daily_views) AS views, -- WRONG
sum(stats.daily_unique) AS uniques, -- WRONG
sum(stats.videos_watched) AS videos -- WRONG
FROM daily_stats stats
GROUP BY stats.user_id;
```
**After (fixed):**
```sql
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;
```
### Identifying Affected Columns
Check the table schema to see which columns are aggregate states:
```sql
DESCRIBE TABLE your_table;
```
Output shows column types like:
```
daily_views AggregateFunction(sum, UInt64)
daily_unique AggregateFunction(uniq, String)
videos_watched AggregateFunction(count)
```
Any column with `AggregateFunction(...)` type requires the corresponding `*Merge()` function.
## Verification
1. Check your materialized view definition for `*State()` functions
2. Ensure all queries use matching `*Merge()` functions
3. Test the query returns expected numeric values, not NULL or binary blobs
```sql
-- Should return actual numbers
SELECT sumMerge(total_views), uniqMerge(unique_visitors)
FROM aggregated_stats
WHERE stat_date >= today() - 7;
```
## Example
**Migration 020 (creates the aggregating table):**
```sql
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, -- State function
uniqState(viewer_hash) as daily_unique_viewers, -- State function
sumState(toFloat64(total_loops)) as daily_loops,-- State function
countState() as videos_watched -- State function
FROM view_counts
GROUP BY video_author_pubkey, toDate(created_at);
```
**Migration 033 (queries the aggregating table - FIXED):**
```sql
CREATE VIEW leaderboard_creators_day AS
SELECT
cds.video_author_pubkey AS pubkey,
p.name,
sumMerge(cds.daily_views) AS views, -- Merge function
uniqMerge(cds.daily_unique_viewers) AS unique_viewers, -- Merge function
sumMerge(cds.daily_loops) AS loops, -- Merge function
countMerge(cds.videos_watched) AS videos_with_views -- Merge function
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 is different**: It stores regular values and sums them during merges.
With SummingMergeTree, you use regular `sum()` in queries. Only AggregatingMergeTree uses
the State/Merge pattern.
- **Why use AggregatingMergeTree?**: For unique counts (`uniq`), you can't simply sum the
counts from different parts—that would overcount. AggregatingMergeTree preserves the
HyperLogLog state so merging gives correct unique counts across partitions.
- **Performance**: The `*Merge()` functions are efficient—they're designed to combine
pre-computed aggregate states, not reprocess raw data.
- **Migration ordering matters**: If migration A creates an AggregatingMergeTree view, and
migration B creates a view that queries it, migration B must use `*Merge()` functions.
## References
- [ClickHouse AggregatingMergeTree](https://clickhouse.com/docs/en/engines/table-engines/mergetree-family/aggregatingmergetree)
- [ClickHouse Aggregate Function Combinators](https://clickhouse.com/docs/en/sql-reference/aggregate-functions/combinators)
- [ClickHouse -State and -Merge combinators](https://clickhouse.com/docs/en/sql-reference/aggregate-functions/combinators#-state)
No comments yet. Be the first to comment!