On 8/3/2024 18:53, Andy Fan wrote:
I just reviewed the bad queries plan for the past half years internally, I found many queries used the Nested loop which is the direct cause. now I think I find out a new reason for this, because the missed optimizer statistics cause the rows in outer relation to be 1, which make the Nest loop is choosed. I'm not sure your idea could help on this or can help on this than mine at this aspect.
Having had the same problem for a long time, I've made an attempt and invented a patch that probes an index to determine whether the estimated constant is within statistics' scope. I remember David's remark on the overhead problem, but I don't argue it here. This patch is on the table to have one more solution sketch for further discussion. Also, Andy, if you have a specific problem with index choosing, you can try a tiny option that makes the index-picking technique less dependent on the ordering of index lists [1].
[1] https://www.postgresql.org/message-id/9b3dbf6d-776a-4953-a5a4-609929393...@postgrespro.ru
-- regards, Andrei Lepikhov Postgres Professional
diff --git a/src/backend/utils/adt/like_support.c b/src/backend/utils/adt/like_support.c index 2635050861..bc8e59bbf3 100644 --- a/src/backend/utils/adt/like_support.c +++ b/src/backend/utils/adt/like_support.c @@ -643,7 +643,7 @@ patternsel_common(PlannerInfo *root, /* * Pattern specifies an exact match, so estimate as for '=' */ - result = var_eq_const(&vardata, eqopr, collation, prefix->constvalue, + result = var_eq_const(root, &vardata, eqopr, collation, prefix->constvalue, false, true, false); } else @@ -1294,7 +1294,7 @@ prefix_selectivity(PlannerInfo *root, VariableStatData *vardata, * probably off the end of the histogram, and thus we probably got a very * small estimate from the >= condition; so we still need to clamp. */ - eq_sel = var_eq_const(vardata, eqopr, collation, prefixcon->constvalue, + eq_sel = var_eq_const(root, vardata, eqopr, collation, prefixcon->constvalue, false, true, false); prefixsel = Max(prefixsel, eq_sel); diff --git a/src/backend/utils/adt/selfuncs.c b/src/backend/utils/adt/selfuncs.c index cea777e9d4..be6213046e 100644 --- a/src/backend/utils/adt/selfuncs.c +++ b/src/backend/utils/adt/selfuncs.c @@ -273,7 +273,7 @@ eqsel_internal(PG_FUNCTION_ARGS, bool negate) * in the query.) */ if (IsA(other, Const)) - selec = var_eq_const(&vardata, operator, collation, + selec = var_eq_const(root, &vardata, operator, collation, ((Const *) other)->constvalue, ((Const *) other)->constisnull, varonleft, negate); @@ -286,13 +286,133 @@ eqsel_internal(PG_FUNCTION_ARGS, bool negate) return selec; } +/* + * Lookup the value in the index and try to estimate number of the tuples with + * the same value. + */ +static Selectivity +estimate_ntuples_by_index(PlannerInfo *root, VariableStatData *vardata, + Datum constval, + Oid collation, Oid regprocoid, int32 stawidth) +{ + RelOptInfo *rel = vardata->rel; + RangeTblEntry *rte = root->simple_rte_array[rel->relid]; + ListCell *lc; + int ntuples = 0; + Selectivity selec = -1.; + + foreach(lc, rel->indexlist) + { + IndexOptInfo *index = (IndexOptInfo *) lfirst(lc); + ScanKeyData scankeys[1]; + SnapshotData SnapshotNonVacuumable; + IndexScanDesc index_scan; + Relation heapRel; + Relation indexRel; + + if (index->relam != BTREE_AM_OID) + continue; + if (index->indpred != NIL) + continue; + if (index->hypothetical) + continue; + if (!match_index_to_operand(vardata->var, 0, index)) + continue; + + heapRel = table_open(rte->relid, NoLock); + indexRel = index_open(index->indexoid, NoLock); + + ScanKeyEntryInitialize(&scankeys[0], + 0, + 1, + BTEqualStrategyNumber, + vardata->atttype, + collation, + regprocoid, + constval); + InitNonVacuumableSnapshot(SnapshotNonVacuumable, + GlobalVisTestFor(heapRel)); + + index_scan = index_beginscan(heapRel, indexRel, + &SnapshotNonVacuumable, + 1, 0); + index_scan->xs_want_itup = true; + index_rescan(index_scan, scankeys, 1, NULL, 0); + while (index_getnext_tid(index_scan, ForwardScanDirection) != NULL) + { + ntuples++; + } + + selec = (ntuples > 0) ? ntuples / vardata->rel->tuples : + 1. / vardata->rel->tuples; + + index_endscan(index_scan); + index_close(indexRel, NoLock); + table_close(heapRel, NoLock); + break; + } + return selec; +} + +/* + * Returns 0 if the value covered by the histogram. + */ +static bool +const_out_of_scope(VariableStatData *vardata, Datum value) +{ + AttStatsSlot sslot; + bool result = false; + + if (HeapTupleIsValid(vardata->statsTuple) && + get_attstatsslot(&sslot, vardata->statsTuple, + STATISTIC_KIND_HISTOGRAM, InvalidOid, + ATTSTATSSLOT_VALUES)) + { + FmgrInfo opproc; + TypeCacheEntry *type; + Oid funcoid; + bool cmpres; + + /* Load operators for the type */ + type = lookup_type_cache(vardata->vartype, TYPECACHE_LT_OPR | TYPECACHE_GT_OPR); + + /* Check: low boundary greater than value */ + funcoid = get_opcode(type->gt_opr); + fmgr_info(funcoid, &opproc); + cmpres = DatumGetBool(FunctionCall2Coll(&opproc, + type->typcollation, + sslot.values[0], + value)); + + + if (cmpres) + result = true; + else + { + /* Check: top boundary lower than value */ + funcoid = get_opcode(type->lt_opr); + fmgr_info(funcoid, &opproc); + cmpres = DatumGetBool(FunctionCall2Coll(&opproc, + type->typcollation, + sslot.values[sslot.nvalues - 1], + value)); + if (cmpres) + result = true; + } + + free_attstatsslot(&sslot); + } + + return result; +} + /* * var_eq_const --- eqsel for var = const case * * This is exported so that some other estimation functions can use it. */ double -var_eq_const(VariableStatData *vardata, Oid oproid, Oid collation, +var_eq_const(PlannerInfo *root, VariableStatData *vardata, Oid oproid, Oid collation, Datum constval, bool constisnull, bool varonleft, bool negate) { @@ -300,6 +420,7 @@ var_eq_const(VariableStatData *vardata, Oid oproid, Oid collation, double nullfrac = 0.0; bool isdefault; Oid opfuncoid; + Form_pg_statistic stats; /* * If the constant is NULL, assume operator is strict and return zero, ie, @@ -314,8 +435,6 @@ var_eq_const(VariableStatData *vardata, Oid oproid, Oid collation, */ if (HeapTupleIsValid(vardata->statsTuple)) { - Form_pg_statistic stats; - stats = (Form_pg_statistic) GETSTRUCT(vardata->statsTuple); nullfrac = stats->stanullfrac; } @@ -402,6 +521,17 @@ var_eq_const(VariableStatData *vardata, Oid oproid, Oid collation, */ selec = sslot.numbers[i]; } + else if (const_out_of_scope(vardata, constval) && + (selec = estimate_ntuples_by_index(root, vardata, constval, + collation, opfuncoid, + stats->stawidth)) >= 0.) + { + /* + * value to estimate is out of the boundaries of the histogram. And + * we attempted to find an index and probe it. + * At this point we successfully done it. + */ + } else { /* @@ -1521,7 +1651,7 @@ boolvarsel(PlannerInfo *root, Node *arg, int varRelid) * A boolean variable V is equivalent to the clause V = 't', so we * compute the selectivity as if that is what we have. */ - selec = var_eq_const(&vardata, BooleanEqualOperator, InvalidOid, + selec = var_eq_const(root, &vardata, BooleanEqualOperator, InvalidOid, BoolGetDatum(true), false, true, false); } else diff --git a/src/include/utils/selfuncs.h b/src/include/utils/selfuncs.h index 2fa4c4fc1b..fe789483f0 100644 --- a/src/include/utils/selfuncs.h +++ b/src/include/utils/selfuncs.h @@ -180,7 +180,7 @@ extern double ineq_histogram_selectivity(PlannerInfo *root, bool isgt, bool iseq, Oid collation, Datum constval, Oid consttype); -extern double var_eq_const(VariableStatData *vardata, +extern double var_eq_const(PlannerInfo *root, VariableStatData *vardata, Oid oproid, Oid collation, Datum constval, bool constisnull, bool varonleft, bool negate); diff --git a/src/test/regress/expected/partition_join.out b/src/test/regress/expected/partition_join.out index 6560fe2416..6522dc64f2 100644 --- a/src/test/regress/expected/partition_join.out +++ b/src/test/regress/expected/partition_join.out @@ -2229,8 +2229,8 @@ select * from prtx1 where not exists (select 1 from prtx2 where prtx2.a=prtx1.a and prtx2.b=prtx1.b and prtx2.c=123) and a<20 and c=120; - QUERY PLAN -------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------- Append -> Nested Loop Anti Join -> Seq Scan on prtx1_1 @@ -2244,17 +2244,12 @@ where not exists (select 1 from prtx2 -> Bitmap Index Scan on prtx2_1_c_idx Index Cond: (c = 123) -> Nested Loop Anti Join + Join Filter: ((prtx2_2.a = prtx1_2.a) AND (prtx2_2.b = prtx1_2.b)) -> Seq Scan on prtx1_2 Filter: ((a < 20) AND (c = 120)) - -> Bitmap Heap Scan on prtx2_2 - Recheck Cond: ((b = prtx1_2.b) AND (c = 123)) - Filter: (a = prtx1_2.a) - -> BitmapAnd - -> Bitmap Index Scan on prtx2_2_b_idx - Index Cond: (b = prtx1_2.b) - -> Bitmap Index Scan on prtx2_2_c_idx - Index Cond: (c = 123) -(23 rows) + -> Index Scan using prtx2_2_c_idx on prtx2_2 + Index Cond: (c = 123) +(18 rows) select * from prtx1 where not exists (select 1 from prtx2