Skip to content

Commit bb94828

Browse files
committed
cost: route inner-unique INNER NL through SEMI early-stop branch
PG's final_cost_nestloop (costsize.c:3389) shares the SEMI/ANTI early-stop code with INNER joins that have inner_unique=true: each outer row matches at most one inner row, so the unmatched-outer arm bills only "1-tuple worth of rescan" against an indexed inner instead of a full inner_rescan_run_cost. CCostModelPG::CostNLJoin was only entering the early-stop branch for explicit SEMI/ANTI ops, so a plain INNER NL on a PK / unique-index lookup paid the full rescan per outer row -- pushing the optimizer toward HashJoin on PK-lookup plans. Detect inner-unique cheaply at cost time: walk the join predicate's conjuncts, keep only inner_ident=outer_ident equalities, collect the inner-side colrefs into a set, and test it against the inner's CKeyCollection via FKey(...,fExactMatch=false) -- the same primitive CXformLeftSemiJoin2InnerJoin uses to decide whether to interpose a GbAgg for dedup. Only fire for uncorrelated InnerNLJoin / InnerIndexNLJoin (correlated IndexApply already bills exactly one probe per outer row, no double-correction wanted). Inside the early-stop branch the only new fork is match_count = 1.0 for the inner-unique case; outer_match_frac falls through to the existing semi-style output/outer formula, which is exactly the inner-unique semantics (each matched outer => 1 row). Verification: - cost_align same-plan counts unchanged (311 / 183 in-tol / 7 baseline FAIL / 121 diff-plan) and row-level cost diff against baseline empty - TPC-H SF=1 cost_model=pg: all 22 ratios stable vs prior baseline (Q17 21.7x, Q20 5.25x, Q9 1.48x)
1 parent 506e136 commit bb94828

1 file changed

Lines changed: 93 additions & 6 deletions

File tree

libgpdbcost/src/CCostModelPG.cpp

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
#include "gpopt/operators/CScalarBitmapIndexProbe.h"
4343
#include "gpopt/operators/CScalarConst.h"
4444
#include "gpopt/operators/CScalarIdent.h"
45+
#include "gpopt/operators/CPredicateUtils.h"
4546
#include "gpopt/base/CColRefTable.h"
47+
#include "gpopt/base/CColRefSet.h"
48+
#include "gpopt/base/CKeyCollection.h"
4649
#include <functional>
4750
#include <unordered_set>
4851
#include "naucrates/md/CMDIdColStats.h"
@@ -901,6 +904,62 @@ FindIndexScanInGroup(CGroup *group, ULONG max_depth)
901904
return nullptr;
902905
}
903906

907+
// Detect whether the inner side of an INNER NL is unique on the join keys —
908+
// the cost-model analogue of PG's `inner_unique` flag (final_cost_nestloop
909+
// costsize.c:3389). When true, each outer row matches AT MOST one inner row,
910+
// so for the cost of unmatched outers we can use the SEMI branch's cheap
911+
// "1-tuple worth of rescan" formula instead of charging a full inner rescan
912+
// per outer row.
913+
//
914+
// Criterion (conservative): some key set in the inner's CKeyCollection must
915+
// be fully covered by inner-side colrefs that appear in equi-clauses of the
916+
// form `inner_ident op outer_ident` in the join predicate. Non-equi quals,
917+
// constant-comparison quals, and expression-on-expression quals are ignored
918+
// (they can only further filter, never relax uniqueness).
919+
static BOOL
920+
IsInnerUniqueOnJoinKeys(CMemoryPool *mp, CExpressionHandle &exprhdl,
921+
CExpression *pexprJoinPred)
922+
{
923+
CKeyCollection *pkc = exprhdl.DeriveKeyCollection(1);
924+
if (nullptr == pkc || nullptr == pexprJoinPred) return false;
925+
926+
CColRefSet *pcrsOuterOut = exprhdl.DeriveOutputColumns(0);
927+
CColRefSet *pcrsInnerOut = exprhdl.DeriveOutputColumns(1);
928+
if (nullptr == pcrsOuterOut || nullptr == pcrsInnerOut) return false;
929+
930+
CColRefSet *pcrsInnerEqui = GPOS_NEW(mp) CColRefSet(mp);
931+
CExpressionArray *conjs =
932+
CPredicateUtils::PdrgpexprConjuncts(mp, pexprJoinPred);
933+
for (ULONG i = 0; i < conjs->Size(); i++)
934+
{
935+
CExpression *conj = (*conjs)[i];
936+
if (!CPredicateUtils::IsEqualityOp(conj)) continue;
937+
if (2 != conj->Arity()) continue;
938+
CExpression *e0 = (*conj)[0];
939+
CExpression *e1 = (*conj)[1];
940+
if (COperator::EopScalarIdent != e0->Pop()->Eopid() ||
941+
COperator::EopScalarIdent != e1->Pop()->Eopid())
942+
continue;
943+
const CColRef *cr0 = CScalarIdent::PopConvert(e0->Pop())->Pcr();
944+
const CColRef *cr1 = CScalarIdent::PopConvert(e1->Pop())->Pcr();
945+
if (pcrsInnerOut->FMember(cr0) && pcrsOuterOut->FMember(cr1))
946+
{
947+
pcrsInnerEqui->Include(cr0);
948+
}
949+
else if (pcrsOuterOut->FMember(cr0) && pcrsInnerOut->FMember(cr1))
950+
{
951+
pcrsInnerEqui->Include(cr1);
952+
}
953+
}
954+
conjs->Release();
955+
956+
const BOOL covers_a_key =
957+
(pcrsInnerEqui->Size() > 0) &&
958+
pkc->FKey(pcrsInnerEqui, false /* fExactMatch */);
959+
pcrsInnerEqui->Release();
960+
return covers_a_key;
961+
}
962+
904963
// Semi/anti joins (early termination on first match) follow PG's
905964
// final_cost_nestloop SEMI branch (costsize.c:3389): matched outer rows
906965
// scan only inner_scan_frac = 2/(match_count+1) of the inner per probe,
@@ -911,7 +970,7 @@ FindIndexScanInGroup(CGroup *group, ULONG max_depth)
911970
// HashJoin+HashAggregate dedup plans that build the entire inner.
912971
//---------------------------------------------------------------------------
913972
CCost
914-
CCostModelPG::CostNLJoin(CMemoryPool *, // mp
973+
CCostModelPG::CostNLJoin(CMemoryPool *mp,
915974
CExpressionHandle &exprhdl,
916975
const SCostingInfo *pci)
917976
{
@@ -986,6 +1045,19 @@ CCostModelPG::CostNLJoin(CMemoryPool *, // mp
9861045
COperator::EopPhysicalCorrelatedNotInLeftAntiSemiNLJoin == nl_op_id);
9871046
const BOOL is_semi_or_anti = is_semi || is_anti;
9881047

1048+
// Inner-unique INNER NL: each outer row matches AT MOST one inner row, so
1049+
// PG (costsize.c:3389) routes through the same early-stop branch as
1050+
// SEMI/ANTI. Only check for plain (uncorrelated) INNER NLs — correlated
1051+
// IndexApply variants already bill exactly one probe per outer row.
1052+
const BOOL is_inner_nl_kind =
1053+
(COperator::EopPhysicalInnerNLJoin == nl_op_id ||
1054+
COperator::EopPhysicalInnerIndexNLJoin == nl_op_id);
1055+
const BOOL is_inner_unique =
1056+
(!is_semi_or_anti && is_inner_nl_kind && inner_rebinds <= 1.0 &&
1057+
IsInnerUniqueOnJoinKeys(mp, exprhdl,
1058+
exprhdl.PexprScalarRepChild(2)));
1059+
const BOOL early_stop_branch = is_semi_or_anti || is_inner_unique;
1060+
9891061
// Inner is "indexed" (analogue of PG's has_indexed_join_quals) when the
9901062
// physical operator under the NL is an IndexScan/IndexOnlyScan, or when
9911063
// the NL itself is a CPhysicalInner/SemiIndexNLJoin shape (where the
@@ -1004,7 +1076,7 @@ CCostModelPG::CostNLJoin(CMemoryPool *, // mp
10041076

10051077
DOUBLE extra_rescan = 0.0;
10061078
DOUBLE ntuples = outer_rows * inner_rows;
1007-
if (is_semi_or_anti && outer_rows > 0.0 && inner_rows > 0.0)
1079+
if (early_stop_branch && outer_rows > 0.0 && inner_rows > 0.0)
10081080
{
10091081
// PG's match_count = avg # matches per matched outer row, derived
10101082
// from compute_semi_anti_join_factors via clauselist_selectivity
@@ -1026,10 +1098,25 @@ CCostModelPG::CostNLJoin(CMemoryPool *, // mp
10261098
// per-outer matches as inner_rows / outer_rows (uniform
10271099
// distribution); not exact but better-by-orders than treating
10281100
// full inner as a single match group.
1029-
const BOOL inner_is_correlated = (inner_rebinds > 1.0);
1030-
const DOUBLE match_count = inner_is_correlated
1031-
? std::max(1.0, inner_rows)
1032-
: std::max(1.0, inner_rows / std::max(1.0, outer_rows));
1101+
// match_count semantics:
1102+
// - inner_unique INNER: PG sets match_count=1 explicitly (each
1103+
// outer matches at most one inner row).
1104+
// - correlated SEMI/ANTI: inner_rows is already the per-probe
1105+
// expected match count (CJoinStatsProcessor scales it).
1106+
// - uncorrelated SEMI/ANTI: inner_rows is total inner cardinality;
1107+
// approximate per-outer matches as inner_rows / outer_rows.
1108+
DOUBLE match_count;
1109+
if (is_inner_unique)
1110+
{
1111+
match_count = 1.0;
1112+
}
1113+
else
1114+
{
1115+
const BOOL inner_is_correlated = (inner_rebinds > 1.0);
1116+
match_count = inner_is_correlated
1117+
? std::max(1.0, inner_rows)
1118+
: std::max(1.0, inner_rows / std::max(1.0, outer_rows));
1119+
}
10331120
const DOUBLE inner_scan_frac = 2.0 / (match_count + 1.0);
10341121

10351122
// PG's outer_match_frac = jselec (SEMI selectivity) = fraction of

0 commit comments

Comments
 (0)