From ea05796d6a34b0339190fff98e9a35c89bb4f552 Mon Sep 17 00:00:00 2001 From: Andrey Kazachkov Date: Tue, 15 Sep 2026 13:34:32 +0300 Subject: [PATCH] Fix ORCA selectivity damping for outer references SelectivityOfPredicate() applies damping to the product of local and outer predicate estimates, which can increase the result above the local selectivity. This violates the bounds of a conjunction. Outer predicate estimates use statistics after local filtering. Treat them as conditional selectivities and combine them using the existing conjunction estimator, then multiply by the local estimate. This preserves the local estimate as an upper bound and leaves it unchanged when an outer predicate has selectivity one. Use the active statistics configuration for this aggregation so that optimizer_damping_factor_filter controls the correction, rather than the hardcoded default. These estimates participate in index selection, so the change can affect planning decisions. --- .../src/statistics/CFilterStatsProcessor.cpp | 72 ++++++------------- 1 file changed, 22 insertions(+), 50 deletions(-) diff --git a/src/backend/gporca/libnaucrates/src/statistics/CFilterStatsProcessor.cpp b/src/backend/gporca/libnaucrates/src/statistics/CFilterStatsProcessor.cpp index a7e458ec353..6f0a604fe8b 100644 --- a/src/backend/gporca/libnaucrates/src/statistics/CFilterStatsProcessor.cpp +++ b/src/backend/gporca/libnaucrates/src/statistics/CFilterStatsProcessor.cpp @@ -81,7 +81,6 @@ CFilterStatsProcessor::SelectivityOfPredicate(CMemoryPool *mp, CColRefSet *used_col_refs = pred->DeriveUsedColumns(); CColRefSet *used_local_col_refs = GPOS_NEW(mp) CColRefSet(mp, *used_col_refs); - ULONG num_outer_ref_preds = 0; if (nullptr != outer_refs) { @@ -101,7 +100,7 @@ CFilterStatsProcessor::SelectivityOfPredicate(CMemoryPool *mp, const COptCtxt *poctxt = COptCtxt::PoctxtFromTLS(); CMDAccessor *md_accessor = poctxt->Pmda(); - // grab default stats config + // use the current optimizer statistics configuration CStatisticsConfig *stats_config = poctxt->GetOptimizerConfig()->GetStatsConf(); // we don't care about the width of the columns, just the row count @@ -115,14 +114,15 @@ CFilterStatsProcessor::SelectivityOfPredicate(CMemoryPool *mp, IStatistics *result_stats = CFilterStatsProcessor::MakeStatsFilter( mp, dynamic_cast(base_table_stats), pred_stats, false); - CDouble result = result_stats->Rows() / base_table_stats->Rows(); - BOOL have_local_preds = (result < 1.0); + const CDouble local_selectivity = + result_stats->Rows() / base_table_stats->Rows(); pred_stats->Release(); used_local_col_refs->Release(); base_table_stats->Release(); dummy_width_set->Release(); - // handle outer_refs + // estimate outer predicates using statistics after local filtering + CDoubleArray *outer_scale_factors = GPOS_NEW(mp) CDoubleArray(mp); if (nullptr != expr_with_outer_refs) { CExpressionArray *outer_ref_exprs = @@ -132,7 +132,13 @@ CFilterStatsProcessor::SelectivityOfPredicate(CMemoryPool *mp, for (ULONG ul = 0; ul < size; ul++) { CExpression *pexpr = (*outer_ref_exprs)[ul]; + if (CUtils::FScalarConstTrue(pexpr)) + { + continue; + } + CColRef *local_col_ref = nullptr; + CDouble scale_factor = 1 / CHistogram::DefaultSelectivity; if (CPredicateUtils::FIdentCompareOuterRefExprIgnoreCast( pexpr, outer_refs, &local_col_ref)) @@ -144,64 +150,30 @@ CFilterStatsProcessor::SelectivityOfPredicate(CMemoryPool *mp, GPOS_ASSERT(nullptr != local_col_ref); CDouble ndv = result_stats->GetNDVs(local_col_ref); - if (ndv < 1.0) - { - // An NDV of less than 1 means that we have no stats on this column - result = result * CHistogram::DefaultSelectivity; - } - else + // an NDV below 1 means that we have no stats on this column + if (ndv >= 1.0) { - result = result * (1 / ndv); + scale_factor = ndv; } } - else - { - // a comparison col op other than an equals - result = result * CHistogram::DefaultSelectivity; - } - num_outer_ref_preds++; - } - else - { - // if it is a true filter, then we had no expressions with outer refs - if (!CUtils::FScalarConstTrue(pexpr)) - { - // some other expression, not of the form col op , - // e.g. an OR expression - result = result * CHistogram::DefaultSelectivity; - num_outer_ref_preds++; - } } + outer_scale_factors->Append(GPOS_NEW(mp) CDouble(scale_factor)); } expr_with_outer_refs->Release(); outer_ref_exprs->Release(); } - // apply damping factor to the outer ref predicates whose selectivities we multiplied above - if (have_local_preds) - { - // add one for the combined non-outer refs which were dampened internally, - // but not in combination with the preds on outer refs - num_outer_ref_preds++; - } - if (1 < num_outer_ref_preds) - { - CStatisticsConfig *stats_config = - CStatisticsConfig::PstatsconfDefault(mp); - - result = - std::min(result.Get() / CScaleFactorUtils::DampedFilterScaleFactor( - stats_config, num_outer_ref_preds) - .Get(), - 1.0); - - stats_config->Release(); - } + const CDouble outer_scale_factor = + CScaleFactorUtils::CalcScaleFactorCumulativeConj(stats_config, + outer_scale_factors); + outer_scale_factors->Release(); result_stats->Release(); local_expr->Release(); - return result; + // Outer selectivities are conditional on the local filter. Damping only + // their conjunction preserves the local estimate as an upper bound. + return local_selectivity / outer_scale_factor; } // create new structure from a list of statistics filters