跪拜 Guibai
← Back to the summary

When SUM() Lies: A ClickHouse WHERE Clause Skews Aggregate Counts

The first bug I encountered during my internship: the "slow call chain" number on the service instance details page didn't match the total count after clicking into it. Investigation revealed that the ClickHouse aggregate function sum() was affected by WHERE conditions, causing a change in statistical scope. The fix used a CASE WHEN statement. Here's the complete troubleshooting process and solution reasoning.


1. The Problem Emerges

Last week, my mentor sent me a "minor defect":

On the Service Instance Details → Anomaly Analysis page, the number displayed for "Slow Call Chains" is A. After clicking "View All Error Chains" → "View Slow Calls Only", the total number of call chains displayed is B. A ≠ B, but these two numbers should represent the same thing.

Honestly, I was a bit confused when I first saw this—isn't a number just a count? How could it be different?


2. Investigation: Comparing Request Parameters via F12

The first step in debugging any bug is always to see what parameters it's sending.

Opening F12 → Network, I captured the two requests:

When clicking "Anomaly" vs. when clicking "View Slow Calls Only"

I found the problem:

Action stats API list API
Click "Anomaly" Did not send state state=error
Click "View Slow Calls Only" Sent state=isSlow state=isSlow

The same stats API received different parameters in different scenarios, so naturally it returned different numbers.


3. Tracing the Code: From Controller to Repository

Following the call chain layer by layer:

xxxxxxxxxxController (receives Param) → xxxxxxxxxxxImpl (Service layer, directly passes through) → xxxxxxxxxRepository → buildTraceWhere() method

The key code is in buildTraceWhere:

// Dynamically concatenate WHERE conditions
if (StringUtils.hasText(param.getState())) {
    sql.append(switch (param.getState()) {
        case "success" -> " AND isError = 0 AND isSlow = 0";
        case "error"   -> " AND isError > 0";
        case "isSlow"  -> " AND isError = 0 AND isSlow > 0";  // ← Problem is here
        default        -> " AND 1 = 2";
    });
}

The logic is clear:

4. Root Cause: The Aggregate Function Was "Led Astray" by WHERE

The core SQL of the stats API:

SELECT
    sum(isError) AS errorCount,
    sum(isSlow)  AS slowCount    -- ← This has a problem
FROM trace_table
WHERE ...

The problem is subtle:

Scenario WHERE Condition What sum(isSlow) counts
Click "Anomaly" (state not sent) No filtering Slow calls in the full dataset
Click "View Slow Calls Only" (state=isSlow) isError = 0 Slow calls after excluding errors

The statistical scope of the aggregate function follows the WHERE clause—this is the root cause.

5. Eliminating Solutions: My Deliberation Process

This problem wasn't big, but I thought about the solution choice for a long time.

Option 1: Remove isError = 0 from buildTraceWhere

No good. If removed, the slow call list would show error call data, which doesn't meet product requirements—"a call that is both slow and an error does not belong to the slow call category."

Option 2: Set state to null before calling the Repository

No good. If set to null, buildTraceWhere's switch wouldn't execute, and the list API's filtering would also fail. The "View Errors Only" and "View Slow Calls Only" lists would mix data.

Option 3: Don't change WHERE, change the aggregate function itself

✅ Let the aggregate function define its own statistical rules, independent of WHERE filtering.

6. The Fix: One Line of Code

In the end, only one line of SQL was changed:

-- Before fix
sum(isSlow) AS slowCount

-- After fix
sum(CASE WHEN isSlow > 0 AND isError = 0 THEN 1 ELSE 0 END) AS slowCount

My mentor saw it and said: ClickHouse has a more concise way to write this—sumIf:

-- ClickHouse-specific syntax (alternative writing)
sumIf(isSlow, isError = 0) AS slowCount

sumIf(value, condition) : Only accumulates when the condition is met, more concise than CASE WHEN.

Why does this solution work?

After the fix, tests passed. Clicking "Anomaly" and clicking "View Slow Calls Only" now shows consistent numbers.

7. Review: What I Learned

1. Troubleshooting Methodology (Reusable) F12 capture requests → Compare parameter differences → URL → Controller → Service → Repository (trace layer by layer) → Find WHERE / filter conditions → Compare whether filter conditions are consistent across different scenarios → Where they are inconsistent is the Bug

2. ClickHouse Conditional Aggregate Functions

Function Purpose Equivalent Standard SQL
sumIf(value, condition) Sum when condition is met sum(CASE WHEN condition THEN value END)
countIf(condition) Count when condition is met count(CASE WHEN condition THEN 1 END)
avgIf(value, condition) Average when condition is met Similar

ClickHouse-specific optimization, more concise than standard SQL.

3. Awareness of Solution Analysis

This problem made me realize: Before changing one place, first think clearly about which other places will be affected.

I initially wanted to directly change the WHERE condition, but it would affect the list API; I thought about setting it to null, but the two lists would mix data. Finally, I discovered that making the aggregate function self-contained was the correct approach.

This was the first bug I fixed during my internship. It wasn't big, but I went through the complete process from investigation to fix. If you are also using ClickHouse for statistics, remember: Putting conditions inside aggregate functions is more controllable than WHERE filtering

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

梦短情长

Java actually still has newly created positions

予怀960

It's just that the company project uses this kind of database; overall it's still pretty much the same