Repository navigation
fix(orca): merge colid->attno mapping in CStatistics::AppendStats to avoid extended-stats crash - #1966
fix(orca): merge colid->attno mapping in CStatistics::AppendStats to avoid extended-stats crash#1966yjhjstz wants to merge 3 commits into
Conversation
…avoid extended-stats crash When a memo group's cached statistics lack columns needed by a later derivation request (e.g. a ROLLUP expanded into a CTE whose consumers request different column sets), the missing columns are derived separately and merged via CStatistics::AppendStats(). That merge copied histograms and widths only, so the colid -> attno mapping (added for extended statistics) diverged from the histogram map: a column could have a histogram but no attno entry. ApplyCorrelatedStatsToScaleFactorFilterCalculation() then dereferenced the result of the attno lookup without a null check and crashed the coordinator with SIGSEGV whenever the table had any extended statistics object with stored data and a filter referenced one of the appended columns (e.g. TPC-DS Q67 after CREATE STATISTICS (ndistinct) on item/date_dim). Reproduced on main with a cassert build: colid 11 had a histogram (5 entries) but the attno map only held 4 entries, attnum was NULL at CExtendedStatsProcessor.cpp:280. This is the same class of bug as the CTE remapping fix in 124a048e8c6 (gpdb#16212), which repaired CopyStatsWithRemap() but not AppendStats(). Fix both layers: * CStatistics::AppendStats() now also merges the colid -> attno mapping (copy-on-write, since the mapping is shared by refcount across stats objects, see ScaleStats()) and adopts the input's extended-stats info when none is set, restoring the invariant that every column with a histogram has an attno entry. * CExtendedStatsProcessor now null-checks the attno and histogram lookups and skips clauses it cannot translate, falling back to the independence assumption, matching what ApplyCorrelatedStatsToNDistinctCalculation() already does. Also bail out gracefully if no ndistinct item matches the attribute set. * CExtendedStatsProcessor also skips system columns (attno <= 0): extended statistics never cover them, and feeding a negative attno into CBitSet::ExchangeSet() would convert it to a huge unsigned bit index. The backend rejects these clauses too (dependency_is_compatible_clause()). Add a regression test with the minimized reproduction: a ROLLUP over a join where the dimension table has ndistinct extended statistics and the filter column is appended to the scan group's stats after the initial derivation. Verified the gporca test passes with both optimizer=on and optimizer=off answer files. Ported from warehouse-pg/warehouse-pg#251. Co-authored-by: Junfeng Yang <junfengyang@link.cuhk.edu.hk>
Follow-up to the null-histogram fallback in MakeHistHashMapConjFilter(): apply the same treatment in MakeHistHashMapDisjFilter() so that a filter column without a histogram, e.g. in WHERE (c2 > 0 OR c1 = 'x'), is estimated with the default selectivity instead of dereferencing null. * A simple predicate whose column has no histogram is skipped before the per-column bookkeeping, like an unsupported predicate, so the previous column's intermediate result is not flushed twice. * A nested conjunction/disjunction on a single column whose histogram is missing from the child result falls back to the multi-column merge path instead of copying a null histogram. * The conjunction path only replaces the column histogram after a single-column disjunction when the disjunction produced one. Addresses review feedback on apache#1966.
f511d13 to
6574841
Compare
| // the column has no histogram, so the child left none | ||
| // behind; treat the child like a multi-column predicate | ||
| // and only merge its histogram map and scale factor | ||
| is_colid_present = false; |
There was a problem hiding this comment.
It looks like colid = gpos::ulong_max; is missing here.
Without this reset, the next nested branch on the same column overwrites the previous branch’s scale factor instead of accumulating it, making OR estimates depend on branch order. Resetting colid preserves both branches’ contributions.
There was a problem hiding this comment.
Good catch, and it turns out the consequence is worse than just the scale factor.
The multi-column path at the bottom of the loop is otherwise only reached with colid == gpos::ulong_max, which makes IsNewStatsColumn() true at the top of the loop, so that path assumes the pending per-column state has already been flushed. By falling into it with a real colid still set, my patch broke that assumption twice:
- as you say, a later disjunct on the same column sees
previous_colid == colid, soprevious_scale_factoris never appended toscale_factorsand is overwritten instead of accumulated — the OR estimate becomes branch-order dependent; - the path's
previous_histogram = nullptr;(a no-op in the original code, since the flush had already run) now drops a liveCHistogram *— its contribution to the disjunction is lost and the object is leaked.
Fixed in a1af361 by flushing the pending histogram and scale factor in the fallback and then resetting colid:
if (nullptr != previous_histogram)
{
scale_factors->Append(
GPOS_NEW(mp) CDouble(previous_scale_factor.Get()));
CStatisticsUtils::UpdateDisjStatistics(
mp, non_updatable_cols, input_rows, cumulative_rows,
previous_histogram, disjunctive_result_histograms,
previous_colid);
previous_histogram = nullptr;
}
is_colid_present = false;
colid = gpos::ulong_max;The nullptr != previous_histogram guard is what distinguishes "state pending for this column" from "already flushed at the top of this iteration" — in the latter case previous_scale_factor still holds the value that was just appended, so an unconditional append would double-count it.
Verified on a cassert build (ORCA on): gporca (which carries this PR's new test), stats_ext, bfv_statistic, table_statistics, gporca_faults, qp_orca_fallback, bfv_joins, bfv_subquery, bfv_partition_plans, subselect_gp, qp_select, select, subselect, orca_static_pruning all pass.
… histogram The null-histogram fallback added for MakeHistHashMapDisjFilter() routes the child to the multi-column merge path but kept the real column id. That path is otherwise only reached with colid == gpos::ulong_max, which makes IsNewStatsColumn() true at the top of the loop, so it assumes the pending per-column state has already been flushed. With a real column id left in place: * a later disjunct on the same column found previous_colid == colid, so the pending scale factor was never appended to scale_factors and was overwritten instead of accumulated, making the OR estimate depend on branch order; * the pending previous_histogram was dropped on the floor by the path's 'previous_histogram = nullptr', losing its contribution to the disjunction and leaking it. Flush the pending histogram and scale factor in the fallback, then reset colid to gpos::ulong_max so the invariant the multi-column path relies on holds again. Reported-by: Alena Rybakina
| // merging is only valid for stats objects describing the same relation; | ||
| // all callers merge stats of the same memo group / expression today. | ||
| GPOS_ASSERT(nullptr == m_ext_stats || nullptr == stats->m_ext_stats || | ||
| m_ext_stats == stats->m_ext_stats); |
There was a problem hiding this comment.
would MDId()->Equals() be better?
When a memo group's cached statistics lack columns needed by a later derivation request (e.g. a ROLLUP expanded into a CTE whose consumers request different column sets), the missing columns are derived separately and merged via CStatistics::AppendStats(). That merge copied histograms and widths only, so the colid -> attno mapping (added for extended statistics) diverged from the histogram map: a column could have a histogram but no attno entry.
ApplyCorrelatedStatsToScaleFactorFilterCalculation() then dereferenced the result of the attno lookup without a null check and crashed the coordinator with SIGSEGV whenever the table had any extended statistics object with stored data and a filter referenced one of the appended columns (e.g. TPC-DS Q67 after CREATE STATISTICS (ndistinct) on item/date_dim). Reproduced on main with a cassert build: colid 11 had a histogram (5 entries) but the attno map only held 4 entries, attnum was NULL at CExtendedStatsProcessor.cpp:280. This is the same class of bug as the CTE remapping fix in 124a048e8c6 (gpdb#16212), which repaired CopyStatsWithRemap() but not AppendStats().
Fix both layers:
CStatistics::AppendStats() now also merges the colid -> attno mapping (copy-on-write, since the mapping is shared by refcount across stats objects, see ScaleStats()) and adopts the input's extended-stats info when none is set, restoring the invariant that every column with a histogram has an attno entry.
CExtendedStatsProcessor now null-checks the attno and histogram lookups and skips clauses it cannot translate, falling back to the independence assumption, matching what ApplyCorrelatedStatsToNDistinctCalculation() already does. Also bail out gracefully if no ndistinct item matches the attribute set.
CExtendedStatsProcessor also skips system columns (attno <= 0): extended statistics never cover them, and feeding a negative attno into CBitSet::ExchangeSet() would convert it to a huge unsigned bit index. The backend rejects these clauses too (dependency_is_compatible_clause()).
Add a regression test with the minimized reproduction: a ROLLUP over a join where the dimension table has ndistinct extended statistics and the filter column is appended to the scan group's stats after the initial derivation. Verified the gporca test passes with both optimizer=on and optimizer=off answer files.
Ported from warehouse-pg/warehouse-pg#251.
Fixes #1949
What does this PR do?
Type of Change
Breaking Changes
Test Plan
make installcheckmake -C src/test installcheck-cbdb-parallelImpact
Performance:
User-facing changes:
Dependencies:
Checklist
Additional Context
CI Skip Instructions