fix(sql): avoid intermediate overflow in corr() denominator - #7313
Conversation
handle edge case in denominator calculation
fix(sql): improve corr() stability
Compute sqrt(sumY) * sqrt(sumX) rather than sqrt(sumY * sumX) so the denominator does not overflow to +Infinity when both sums-of-squared-deviations are very large (e.g. inputs of magnitude ~1e153). Previously such inputs caused corr() to return 0.0 instead of the true correlation. sumX and sumY are non-negative by construction (Welford), so the NaN guard on the denominator is no longer reachable and is removed. Adds regression tests covering perfect positive and negative correlation at extreme magnitudes.
Switch to sqrt(sumY)*sqrt(sumX) only when sumY*sumX would overflow to +Infinity. For normal-magnitude inputs keep the original sqrt(sumY*sumX) form, which has one rounding instead of two and matches the bit-exact results expected by the existing test suite (e.g. 1.0 vs 1.0000000000000002 for perfectly correlated integer data). Fixes CI failures in CorrGroupByFunctionFactoryTest, CorrParallelGroupByTest, and LateralJoinSharedCursorTest while preserving the overflow fix verified by testCorrLargeMagnitudeOverflow / testCorrLargeMagnitudeNegative.
Per @jovfer's review on #7188, apply the same conditional split-sqrt to the window-function paths in AbstractBivariateStatWindowFunctionFactory: - computeCorr (naive, used by sliding/removable frames) - computeCorrWelford (online, used by running / non-removable frames) Both helpers now compute sqrt(a * b) when the product is finite (preserving bit-exact agreement with prior behaviour for normal-magnitude inputs) and fall back to sqrt(a) * sqrt(b) only when the product overflows to +Infinity. Adds testCorrLargeMagnitudeOverflow in WindowFunctionTest that exercises both code paths via: - corr(y, x) over (partition by i) -- naive computeCorr - corr(y, x) over (partition by i order by ts) -- Welford computeCorrWelford with +/-1e153 inputs, verifying corr() returns +/-1.0 instead of 0.0.
Pearson correlation is mathematically bounded to [-1, 1]. The fallback split-sqrt path used when sumX*sumY overflows to +Infinity (inputs of magnitude ~1e153) performs two sqrt roundings instead of one, which can produce results just outside [-1, 1] (typically by 1-2 ULP, e.g. 1.0000000000000002). Clamp the final ratio to absorb this rounding so corr() never returns a value outside its mathematical range. Applies to both CorrGroupByFunctionFactory.getDouble and the two window helpers computeCorr / computeCorrWelford in AbstractBivariateStatWindowFunctionFactory.
Keep the original PR 7188 commits, then adjust the new corr regression tests for the current assertQuery builder API after merging latest master.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
bluestreak01
left a comment
There was a problem hiding this comment.
Numerical fix itself is correct (verified: overflow prod→+Inf, fallback sqrt(a)*sqrt(b) recovers the denominator, r=1.000…2 → clamp → 1.0), and the out-of-diff callsites are clean. Two things should change before merge.
1. The naive computeCorr path is the modified code with zero coverage — and the test comment claims the opposite
WindowFunctionTest.testCorrLargeMagnitudeOverflow says it "Exercises both the naive (partition-only) and Welford (order-by running) code paths." Both claims are wrong:
corr(y,x) over (partition by i)→BivarStatOverPartitionFunction.preparePass2→computeCorrWelfordcorr(y,x) over (partition by i order by ts)→BivarStatOverUnboundedPartitionRowsFrameFunction→computeCorrWelford
Both routes use Welford. The naive computeCorr (reached only from removable rows between N preceding… / range … preceding frames) is exercised by neither. This matches the coverage bot exactly: AbstractBivariateStatWindowFunctionFactory.java 17/20, the 3 uncovered lines being precisely computeCorr's new fallback + two clamp returns.
Please add a removable-frame regression, e.g. corr(y,x) over (order by ts rows between 1 preceding and current row) (and/or a range … preceding frame) over the ±1e153 data, and correct the comment to name the real paths (both Welford).
2. The fix is asymmetric — the underflow tail is left degraded
The split-sqrt fallback only fires on overflow (!Double.isFinite(prod)). The mirror-image case — tiny non-zero variances whose product underflows to 0.0 — stays finite, so the code takes sqrt(0.0)=0 and returns NaN, although sqrt(a)*sqrt(b) recovers the value. Verified: for sumX=sumY=sumXY=1e-300 (two points, true corr=1.0) the code returns NaN; the split form returns 1.0. Reachable with inputs near 1e-150 — no more exotic than the 1e153 case the PR targets. Either widen the condition to also use the split form when prod==0.0 && sumX!=0 && sumY!=0, or add a comment explicitly scoping the small-magnitude tail out.
Minor
- PR has no labels — add SQL and Bug.
- No
Fixes #NNNin the body (only links PR #7188); add the tracking issue if one exists.
The count<=0→count<=1 change, the dropped sumX==0||sumY==0 short-circuit, and the assertMemoryLeak+.noLeakCheck() usage all verified equivalent/correct — no action needed there.
Widen the corr() split-sqrt denominator fallback so it also fires when the product of the two sums of squared deviations underflows to exactly 0.0 while both factors are non-zero (inputs of very small magnitude, near +/-1e-150). Previously only the overflow tail (near +/-1e153) was handled, so tiny-magnitude perfectly-correlated inputs returned NaN. Applies to all three sites: CorrGroupByFunction.getDouble, computeCorr, and computeCorrWelford. A genuine zero factor (zero variance) still falls through to the denom == 0 guard and yields NaN. Add a removable rows-frame regression that exercises the naive computeCorr path; the existing window tests only reached computeCorrWelford. Correct the testCorrLargeMagnitudeOverflow comment that wrongly claimed it covered the naive path. Add small-magnitude underflow regressions for the group-by and both window compute paths. The fallback's two sqrt roundings leave perfect correlation 1 ULP below the true 1.0 (0.9999999999999999); the clamp only bounds the >1 / <-1 tails, so this is the accurate finite result. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
L3 ReviewRe-reviewed the current diff from scratch and validated it against a fresh build: No blocking issues. The numerical fix is correct on every path I traced: the split-sqrt fallback recovers the denominator in both the overflow (~1e153, product → +Inf) and underflow (~1e-150, product → 0.0) regimes; the split itself can neither overflow ( ModerateExtract the triplicated denominator + clamp logic. The same ~12-line split-sqrt + clamp block now lives in three places:
This is subtle floating-point logic with a bit-exactness contract that must stay synchronised across all copies — a future tweak to one clamp/threshold has three edit sites that must remain identical. The Minor
Optional: the Considered but not issues
SummaryNo blocking issues; the blast radius is fully contained (all 12 callsites verified, no callers outside the two files, covar/regression paths untouched) and the tests pass against a real build. The only substantive item is the triplicated Pearson logic, which the imminent |
Extract the split-sqrt-fallback and [-1,1]-clamp denominator logic that PR #7313 added to corr() into a single Numbers.corrFromSums helper, so the three corr() implementations (group-by and the two window paths) can share one source of truth. This commit adds the helper plus direct unit tests for its overflow and underflow branches; the call sites are rewired separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the triplicated split-sqrt-fallback and [-1,1]-clamp blocks in CorrGroupByFunction.getDouble, computeCorr, and computeCorrWelford with a call to Numbers.corrFromSums. The per-method count guards and negative pre-clamps stay in place, so behavior is unchanged; this also removes the prose cross-reference comments that papered over the duplication. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clarify in the Numbers.corrFromSums rationale comment that the two variance-sum arguments are interchangeable (the product and split sqrt are symmetric), so the group-by caller passing (sumY, sumX) into (sumXX, sumYY) is correct. Comment-only; no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
addressed the moderate above about code duplications. |
|
/azp run macwin |
|
Azure Pipelines successfully started running 1 pipeline(s). |
[PR Coverage check]😍 pass : 15 / 15 (100.00%) file detail
|
bluestreak01
left a comment
There was a problem hiding this comment.
Approved (level 3 review). Numerical fix verified correct on every path — overflow (+Inf product → split-sqrt) and underflow (0 product → split-sqrt) both recover the denominator; genuine zero variance and NaN propagate correctly; the [-1,1] clamp is mathematically sound. Blast radius fully contained: all 3 new callers + 6 internal computeCorr* callsites SAFE, covar paths byte-identical, corrFromSums is a new symbol with no out-of-diff callers. Both prior-round blocking concerns and the triplication moderate are resolved. Only minor doc/style/consistency nits remain (NumbersTest alphabetical ordering, prior-symptom comment says NaN where GroupBy master returned +Infinity, Double.isFinite vs Numbers.isFinite, splitDenom naming, clamp framing in PR body).
Mirror the fix applied to corr() in questdb#7313. The Pearson denominator sqrt(sumX * sumY) can overflow to +Infinity or underflow to 0.0 for large- or small-magnitude inputs. Split into sqrt(sumX) * sqrt(sumY) when the product is non-finite or zero while both factors are non-zero. Clamp the Pearson ratio to [-1, 1] then square to obtain R² in [0, 1]. Fixes questdb#7328.
What
Follow-up to #7188, based on @RoySerbi's original contribution.
Fixes numerical-stability bugs in
corr()where the denominator can overflow or underflow while computing the product of the two sums of squared deviations. The branch keeps the original contribution commits, merges currentmaster, and adds compatibility fixes for the migrated fluentassertQuery(...)test style.The fix applies to:
CorrGroupByFunctionFactoryAbstractBivariateStatWindowFunctionFactory.computeCorrAbstractBivariateStatWindowFunctionFactory.computeCorrWelfordWhy
The Pearson denominator is
sqrt(sumXX * sumYY), a product of two sums of squared deviations.+/-1e153): each sum is finite (~1e306) but their product overflows to+Infinity, sosqrt(...)is infinite and the final division returns0.0instead of the true correlation.+/-1e-150): each sum is finite (~1e-300) but their product underflows to0.0, sosqrt(0.0) = 0and the division returnsNaN.Example:
Both shapes exist in the window-function implementations as well: for partition-only and ordered running frames (
computeCorrWelford) and for removable sliding frames (computeCorr).How
sqrt(a * b)denominator when the product is finite and non-zero, preserving existing bit-exact behavior for normal inputs.sqrt(a) * sqrt(b)when the product would overflow to+Infinityor underflow to0.0while both factors are non-zero. A genuine zero factor (zero variance) still yieldsNaN.[-1, 1]to absorb the small rounding drift possible in the fallback path.computeCorrpath (the prior window tests only reachedcomputeCorrWelford) and correct the test comment that misnamed the exercised paths.Trade-offs
sqrtroundings instead of one, so for perfectly-correlated inputs whose variance product over- or under-flows, the result can land ~1 ULP off the exact value. The[-1, 1]clamp absorbs the overshoot in the overflow tail (so~1e153reads exactly1.0), but the underflow tail lands just below1.0--corr()of two points near+/-1e-150returns0.9999999999999999rather than1.0. This is the accurate finite result for the fallback; the alternative (exact1.0via power-of-two rescaling) was not judged worth the added complexity and edge cases for a 1-ULP difference at these magnitudes.sqrt(product)path and its prior bit-exact results.Out of scope / follow-up
regr_r2()has the identical denominator exposure ((sumXY * sumXY) / (sumX * sumY), no over/underflow guard and no clamp), so it returns0at~1e153and can drift above1.0. Left out here to keep this PR scoped tocorr(); tracked in regr_r2() denominator overflows/underflows like corr() did #7328.Tests
Notes
Original PR: #7188