Add skewPopStable, skewSampStable, kurtPopStable, kurtSampStable aggregate functions by coderashed · Pull Request #108685 · ClickHouse/ClickHouse · GitHub
Skip to content

Add skewPopStable, skewSampStable, kurtPopStable, kurtSampStable aggregate functions#108685

Open
coderashed wants to merge 10 commits into
ClickHouse:masterfrom
coderashed:coderashed/skew-kurt-stable
Open

Add skewPopStable, skewSampStable, kurtPopStable, kurtSampStable aggregate functions#108685
coderashed wants to merge 10 commits into
ClickHouse:masterfrom
coderashed:coderashed/skew-kurt-stable

Conversation

@coderashed

Copy link
Copy Markdown

Adds four numerically stable aggregate functions for skewness and kurtosis:

Function Description
skewPopStable Population skewness
skewSampStable Sample skewness
kurtPopStable Population kurtosis
kurtSampStable Sample kurtosis

Motivation

The existing skewPop, skewSamp, kurtPop, and kurtSamp functions use a naive two-pass formula that suffers catastrophic cancellation when the mean is large relative to the standard deviation. For example, power or voltage telemetry (mean ~8000W, σ ~50W), Unix timestamps, or any metric with a large DC offset will produce NaN or wildly incorrect results from the naive variants.

Algorithm

Based on Pébay, Terriberry, Kolla, Bennett (2016) "Numerically stable, scalable formulas for parallel and online computation of higher-order multivariate central moments with arbitrary weights", Computational Statistics 31:1305–1325, DOI 10.1007/s00180-015-0637-z.

  • Online update (add) — Corollary 3.2: single-pass recurrence for M2, M3, M4. Update order is mandatory: M4 → M3 → M2 (each reads the not-yet-updated lower moment).
  • Parallel merge (mergeWith) — Proposition 3.1: formulas for combining two partitions. Keeps all intermediate values in the coordinate frame of the data, so accuracy is independent of the absolute offset.

The -State/-Merge combinators work correctly for distributed aggregation.

Correctness

All four functions return nan on empty input, singleton input, or constant input (zero variance). Verified against analytical exact values:

  • Uniform integers 0..999 (1M rows): skewPop = 0, kurtPop = (9N²−21)/(5N²−5)
  • Bernoulli(1/4) (4M rows): skewPop = 2/√3, kurtPop = 7/3 (exact rational)

Translation invariance is verified in the test suite at a 1e9 offset, where the naive variants return NaN or garbage and the stable variants match the unshifted result.

Changelog category (leave one):

  • New Feature

Changelog entry:

Adds skewPopStable, skewSampStable, kurtPopStable, and kurtSampStable aggregate functions — numerically stable variants of the existing skewness and kurtosis functions that remain accurate even when the mean is large relative to the standard deviation.

The else branch used `source.mean + delta * (na / nf)` which is algebraically
incorrect. The right formula is `mean + delta * (nb / nf)` (Welford/Chan
combined-mean identity: mean_new = mean_A + delta * n_B / n).

With the wrong sign, each chained `mergeWith` call stored a corrupted mean,
causing wrong deltas in subsequent merges. This manifested only under parallel
aggregation (max_threads > 1) where `mergeWith` is called in a chain.
Single-threaded mode was unaffected because m2/m3/m4 read the delta before
mean is updated within a single call.

Removes the `SETTINGS max_threads=1` workaround from the merge-correctness
test (c) in 04412_skew_kurt_stable.
…table, kurtSampStable

Each registration now includes a description, syntax, argument list, return value
description, examples with verified output, version (`IntroducedIn` 26.7), and
category, matching the convention used by all other functions in the file.
…ueryFuzzer aggregate function list

The other stable variants (`stddevPopStable`, `varSampStable`, etc.) were
already present; these four were overlooked.
@alexey-milovidov alexey-milovidov added the can be tested Allows running workflows for external contributors label Jun 27, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [540a7e5]

Summary:

job_name test_name status info comment
Stress test (amd_msan) FAIL
Logical error: Unexpected return type from A. Expected B. Got C (STID: 3262-4259) FAIL cidb

AI Review

Summary

This PR adds numerically stable aggregate variants for population/sample skewness and kurtosis, wires them into source-level documentation and the query fuzzer, and adds focused stateless coverage for translation invariance, parity with existing formulas, merge behavior, and edge cases. I did not find any correctness, safety, or compatibility issue that warrants an inline review comment.

Final Verdict

Status: ✅ Approve

@clickhouse-gh clickhouse-gh Bot added the pr-feature Pull request with new product feature label Jun 27, 2026
@clickhouse-gh

clickhouse-gh Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

LLVM Coverage Report

Metric Baseline Current Δ
Lines 85.40% 85.50% +0.10%
Functions 92.60% 92.60% +0.00%
Branches 77.60% 77.70% +0.10%

Changed lines: Changed C/C++ lines covered by tests: 192/207 (92.75%) | Lost baseline coverage: none · Uncovered code

Full report · Diff report

coderashed added a commit to coderashed/ClickHouse that referenced this pull request Jun 27, 2026
Add the query shape the AST fuzzer aborted on in the `amd_msan` stress
test of ClickHouse#108685 (multi-column
projection with a correlated scalar subquery over a `GROUPING SETS` key
under `WITH TOTALS`), scaled down and deterministic, so the regression
test ties directly to the reported failure.
coderashed added a commit to coderashed/ClickHouse that referenced this pull request Jun 27, 2026
A correlated scalar subquery referencing an outer `GROUP BY` key that
becomes `Nullable` via `group_by_use_nulls` (`GROUPING SETS` / `ROLLUP` /
`CUBE` / `WITH TOTALS`) aborted with:

  Logical error: Unexpected return type from toString. Expected String. Got Nullable(String)

The `group_by_use_nulls` nullability walk in `resolveExpressionNode`
stops at the subquery's own query scope, so a correlated column kept its
non-`Nullable` type while decorrelation fed in the real `Nullable`
column, mismatching a baked function result type (`toString` -> `String`
vs `Nullable(String)`).

Continue the walk past a subquery's query boundary for correlated
columns and convert the column to `Nullable` in place, preserving the
node identity shared with the subquery's correlated-columns set (which
the planner matches by identity).

Discovered by the AST fuzzer in the `amd_msan` stress test:
https://github.com/ClickHouse/ClickHouse/actions/runs/28286443407/job/83839128485?pr=108685
Related: ClickHouse#108685
coderashed added a commit to coderashed/ClickHouse that referenced this pull request Jun 27, 2026
Add the query shape the AST fuzzer aborted on in the `amd_msan` stress
test of ClickHouse#108685 (multi-column
projection with a correlated scalar subquery over a `GROUPING SETS` key
under `WITH TOTALS`), scaled down and deterministic, so the regression
test ties directly to the reported failure.
@coderashed

Copy link
Copy Markdown
Author

The stress test CI failure here is an existing bug unrelated to this PR's changes — it's being fixed in #104350. This PR should be clear to merge once that lands.

@coderashed coderashed marked this pull request as draft June 30, 2026 12:28
@coderashed

Copy link
Copy Markdown
Author

@coderashed coderashed marked this pull request as ready for review July 2, 2026 16:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

can be tested Allows running workflows for external contributors pr-feature Pull request with new product feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants