From b6bdbbd55efaa0038a4e119be1d5584ee81782a9 Mon Sep 17 00:00:00 2001 From: Greg Burd Date: Thu, 1 Oct 2026 10:19:05 -0400 Subject: [PATCH v2] Let an ordering index scan hand its ORDER BY values to the target list An Index Scan that orders by an operator (ORDER BY col <-> const) computes each returned row's ORDER BY value inside the index AM and reports it in xs_orderbyvals. If the same expression also appears in the target list, the executor then evaluates it a second time in the scan's projection. For an expensive distance operator that is wasted work; for an AM whose ordering value depends on index-wide state the displayed number is not the number the rows were ordered by. Add IndexAmRoutine.amorderbyvalsexact. When an AM sets it, setrefs.c replaces each top-level target list expression that is equal() to the i'th indexorderbyorig expression with Var(INNER_VAR, i), and nodeIndexscan.c stores the returned row's ORDER BY values in a virtual slot it installs as the projection's inner tuple (a scan node has no inner plan, so INNER_VAR is otherwise unused there). The values stored are the AM's for a row it reported exact, the recomputed ones for a row it asked to recheck, a reorder queue entry's copy for a queued row, and freshly recomputed ones for an EvalPlanQual test tuple. A lossy AM's lower bound is never displayed. Only float4/float8 ORDER BY expressions qualify, as those are the only types index_store_float8_orderby_distances() can deliver. EXPLAIN VERBOSE deparses the INNER_VAR back to the ORDER BY expression and reports "Order By Values Used: N". Above such a scan, an upper node's reference to the value now prints with one more pair of parentheses, the same way any reference to a computed scan column does (box, polygon expected output). GiST and SP-GiST set the flag; their non-rechecked distances are the operator's result, so for them this is a pure optimization. Other AMs default to false and are unaffected. --- doc/src/sgml/indexam.sgml | 19 ++++ src/backend/access/gist/gist.c | 1 + src/backend/access/spgist/spgutils.c | 1 + src/backend/commands/explain.c | 4 + src/backend/executor/nodeIndexscan.c | 91 +++++++++++++++- src/backend/optimizer/plan/createplan.c | 4 + src/backend/optimizer/plan/setrefs.c | 56 ++++++++++ src/backend/optimizer/util/plancat.c | 2 + src/backend/utils/adt/ruleutils.c | 31 ++++++ src/include/access/amapi.h | 9 ++ src/include/nodes/execnodes.h | 7 ++ src/include/nodes/pathnodes.h | 1 + src/include/nodes/plannodes.h | 9 ++ src/test/regress/expected/box.out | 12 +-- src/test/regress/expected/gist.out | 135 ++++++++++++++++++++++++ src/test/regress/expected/polygon.out | 6 +- src/test/regress/sql/gist.sql | 65 ++++++++++++ 17 files changed, 442 insertions(+), 11 deletions(-) diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml index f48da318530..a263f85c003 100644 --- a/doc/src/sgml/indexam.sgml +++ b/doc/src/sgml/indexam.sgml @@ -109,6 +109,8 @@ typedef struct IndexAmRoutine bool amcanorder; /* does AM support ORDER BY result of an operator on indexed column? */ bool amcanorderbyop; + /* are the AM's non-rechecked ORDER BY values exact enough to display? */ + bool amorderbyvalsexact; /* does AM support hashing using API consistent with the hash AM? */ bool amcanhash; /* do operators within an opfamily have consistent equality semantics? */ @@ -989,6 +991,23 @@ amtranslatecmptype (CompareType cmptype, Oid opfamily, Oid opcintype); of that form can be passed to amrescan as described previously. + + Such an access method returns each entry's ordering values in + scan->xs_orderbyvals, and sets + scan->xs_recheckorderby to true when those values are + only approximations that the executor must recompute from the heap + tuple. If the access method also sets + amorderbyvalsexact to true, it promises that + whenever xs_recheckorderby is false, the values in + xs_orderbyvals are the values the query should see; + the executor may then return them directly for a target list entry + that is the same expression as the ORDER BY key, + instead of evaluating the operator again. An access method whose + ordering values differ from what the operator would compute on the + heap tuple (because they are approximate, or depend on index-wide + state) must leave this false unless it intends that difference to be + visible. + diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c index 8565e225be7..1a43314aaf7 100644 --- a/src/backend/access/gist/gist.c +++ b/src/backend/access/gist/gist.c @@ -65,6 +65,7 @@ gisthandler(PG_FUNCTION_ARGS) .amoptsprocnum = GIST_OPTIONS_PROC, .amcanorder = false, .amcanorderbyop = true, + .amorderbyvalsexact = true, .amcanhash = false, .amconsistentequality = false, .amconsistentordering = false, diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c index f2ee333f60d..1d8f0cffec0 100644 --- a/src/backend/access/spgist/spgutils.c +++ b/src/backend/access/spgist/spgutils.c @@ -50,6 +50,7 @@ spghandler(PG_FUNCTION_ARGS) .amoptsprocnum = SPGIST_OPTIONS_PROC, .amcanorder = false, .amcanorderbyop = true, + .amorderbyvalsexact = true, .amcanhash = false, .amconsistentequality = false, .amconsistentordering = false, diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 96f2f0e2e74..924f70af2e4 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -1974,6 +1974,10 @@ ExplainNode(PlanState *planstate, List *ancestors, planstate, es); show_scan_qual(((IndexScan *) plan)->indexorderbyorig, "Order By", planstate, ancestors, es); + if (es->verbose && ((IndexScan *) plan)->indexorderbytlist > 0) + ExplainPropertyInteger("Order By Values Used", NULL, + ((IndexScan *) plan)->indexorderbytlist, + es); show_scan_qual(plan->qual, "Filter", planstate, ancestors, es); if (plan->qual) show_instrumentation_count("Rows Removed by Filter", 1, diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c index 129d005f187..6504fed2bf1 100644 --- a/src/backend/executor/nodeIndexscan.c +++ b/src/backend/executor/nodeIndexscan.c @@ -163,6 +163,33 @@ IndexNext(IndexScanState *node) return ExecClearTuple(slot); } +/* + * Hand the ORDER BY values of the row about to be returned to the projection, + * when the targetlist asks for them (indexorderbytlist > 0, set by setrefs.c + * only for an AM with amorderbyvalsexact). `vals`/`nulls` are what the + * caller established as this row's ordering values: the AM's when it reported + * them exact, the recomputed ones when it asked for a recheck, or a reorder + * queue entry's copy. Never a lossy AM's lower bound. + */ +static inline void +IndexStoreOrderByValues(IndexScanState *node, const Datum *vals, + const bool *nulls) +{ + TupleTableSlot *oslot = node->iss_OrderBySlot; + int i; + + if (oslot == NULL) + return; + ExecClearTuple(oslot); + for (i = 0; i < node->iss_NumOrderByKeys; i++) + { + oslot->tts_values[i] = vals[i]; + oslot->tts_isnull[i] = nulls[i]; + } + ExecStoreVirtualTuple(oslot); + node->ss.ps.ps_ExprContext->ecxt_innertuple = oslot; +} + /* ---------------------------------------------------------------- * IndexNextWithReorder * @@ -251,6 +278,9 @@ IndexNextWithReorder(IndexScanState *node) { HeapTuple tuple; + /* before the pop frees the entry's copies */ + IndexStoreOrderByValues(node, topmost->orderbyvals, + topmost->orderbynulls); tuple = reorderqueue_pop(node); /* Pass 'true', as the tuple in the queue is a palloc'd copy */ @@ -352,6 +382,7 @@ next_indextuple: else { /* Can return this tuple immediately. */ + IndexStoreOrderByValues(node, lastfetched_vals, lastfetched_nulls); return slot; } } @@ -404,7 +435,22 @@ IndexRecheck(IndexScanState *node, TupleTableSlot *slot) /* Does the tuple meet the indexqual condition? */ econtext->ecxt_scantuple = slot; - return ExecQualAndReset(node->indexqualorig, econtext); + if (!ExecQualAndReset(node->indexqualorig, econtext)) + return false; + + /* + * The EPQ test tuple never went through IndexNext, so the ORDER BY values + * the targetlist reads (if any) are not those of this row. Recompute + * them from the tuple, as we would for a rechecked row. + */ + if (node->iss_OrderBySlot != NULL) + { + EvalOrderByExpressions(node, econtext); + IndexStoreOrderByValues(node, node->iss_OrderByValues, + node->iss_OrderByNulls); + } + + return true; } @@ -954,7 +1000,24 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) * Initialize result type and projection. */ ExecInitResultTypeTL(&indexstate->ss.ps); - ExecAssignScanProjectionInfo(&indexstate->ss); + + /* + * A targetlist that reads the ORDER BY values (INNER_VAR Vars, see + * setrefs.c) is never the scan tuple's physical layout, so it always + * projects; ExecConditionalAssignProjectionInfo would assert on the + * INNER_VAR while testing whether it can skip projection. + */ + if (node->indexorderbytlist > 0) + { + ExecInitResultSlot(&indexstate->ss.ps, &TTSOpsVirtual); + indexstate->ss.ps.resultops = &TTSOpsVirtual; + indexstate->ss.ps.resultopsfixed = true; + indexstate->ss.ps.resultopsset = true; + ExecAssignProjectionInfo(&indexstate->ss.ps, + indexstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor); + } + else + ExecAssignScanProjectionInfo(&indexstate->ss); /* * initialize child expressions @@ -1024,6 +1087,30 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) NULL, /* no ArrayKeys */ NULL); + /* + * A targetlist that reads the ORDER BY values needs a slot to read them + * from, typed as the ORDER BY expressions (setrefs.c built it so). + */ + if (node->indexorderbytlist > 0) + { + TupleDesc odesc; + ListCell *lc; + AttrNumber attno = 0; + + odesc = CreateTemplateTupleDesc(list_length(node->indexorderbyorig)); + foreach(lc, node->indexorderbyorig) + { + Node *expr = (Node *) lfirst(lc); + + attno++; + TupleDescInitEntry(odesc, attno, NULL, exprType(expr), + exprTypmod(expr), 0); + } + TupleDescFinalize(odesc); + indexstate->iss_OrderBySlot = + ExecAllocTableSlot(&estate->es_tupleTable, odesc, &TTSOpsVirtual, 0); + } + /* Initialize sort support, if we need to re-check ORDER BY exprs */ if (indexstate->iss_NumOrderByKeys > 0) { diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 2f8a08ad78e..7635707d300 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -3033,6 +3033,10 @@ create_indexscan_plan(PlannerInfo *root, indexorderbyops, best_path->indexscandir); + if (!indexonly && indexorderbys != NIL) + ((IndexScan *) scan_plan)->indexorderbyexact = + indexinfo->amorderbyvalsexact; + copy_generic_path_info(&scan_plan->plan, &best_path->path); return scan_plan; diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index 8a641402a96..3ecea955800 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -134,6 +134,7 @@ static bool flatten_rtes_walker(Node *node, flatten_rtes_walker_context *cxt); static void add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos, RangeTblEntry *rte); static Plan *set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset); +static void replace_orderby_tlist_refs(IndexScan *splan); static Plan *set_indexonlyscan_references(PlannerInfo *root, IndexOnlyScan *plan, int rtoffset); @@ -701,6 +702,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) splan->indexorderbyorig = fix_scan_list(root, splan->indexorderbyorig, rtoffset, NUM_EXEC_QUAL(plan)); + /* after both are fixed, so equal() compares like with like */ + replace_orderby_tlist_refs(splan); } break; case T_IndexOnlyScan: @@ -1360,6 +1363,59 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * replace_orderby_tlist_refs + * Let an ordering IndexScan hand its ORDER BY values to its targetlist. + * + * If the index AM promises that the ORDER BY values it reports for an exact + * (non-rechecked) row are the values the query should see + * (amorderbyvalsexact), replace each top-level targetlist expression that is + * equal() to the i'th indexorderbyorig expression with + * Var(INNER_VAR, i + 1). nodeIndexscan.c then fills a virtual slot with the + * ORDER BY values of every returned row -- the AM's for an exact row, the + * recomputed ones for a rechecked row -- and installs it as the projection's + * inner tuple. A scan node has no inner plan, so INNER_VAR is otherwise + * unused here. + * + * Only float8/float4 expressions qualify: those are the only types + * index_store_float8_orderby_distances() can deliver. + */ +static void +replace_orderby_tlist_refs(IndexScan *splan) +{ + ListCell *lc; + int nrepl = 0; + + if (!splan->indexorderbyexact || splan->indexorderbyorig == NIL) + return; + + foreach(lc, splan->scan.plan.targetlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc); + ListCell *lo; + int i = 0; + + foreach(lo, splan->indexorderbyorig) + { + Expr *orig = (Expr *) lfirst(lo); + Oid typ = exprType((Node *) orig); + + i++; + if ((typ == FLOAT8OID || typ == FLOAT4OID) && + equal(tle->expr, orig)) + { + tle->expr = (Expr *) makeVar(INNER_VAR, i, typ, + exprTypmod((Node *) orig), + exprCollation((Node *) orig), + 0); + nrepl++; + break; + } + } + } + splan->indexorderbytlist = nrepl; +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index d067368cfc1..5e691a7fb65 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -305,6 +305,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, /* We copy just the fields we need, not all of rd_indam */ amroutine = indexRelation->rd_indam; info->amcanorderbyop = amroutine->amcanorderbyop; + info->amorderbyvalsexact = amroutine->amorderbyvalsexact; info->amoptionalkey = amroutine->amoptionalkey; info->amsearcharray = amroutine->amsearcharray; info->amsearchnulls = amroutine->amsearchnulls; @@ -406,6 +407,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, else { info->amcanorderbyop = false; + info->amorderbyvalsexact = false; info->amoptionalkey = false; info->amsearcharray = false; info->amsearchnulls = false; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index 5e8e1683db0..dca0d43e9ce 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -180,6 +180,8 @@ typedef struct List *outer_tlist; /* referent for OUTER_VAR Vars */ List *inner_tlist; /* referent for INNER_VAR Vars */ List *index_tlist; /* referent for INDEX_VAR Vars */ + List *orderby_tlist; /* IndexScan: referent for INNER_VAR Vars that + * stand for its ORDER BY values */ /* Special namespace representing a function signature: */ char *funcname; int numargs; @@ -5227,6 +5229,23 @@ set_deparse_plan(deparse_namespace *dpns, Plan *plan) dpns->index_tlist = ((CustomScan *) plan)->custom_scan_tlist; else dpns->index_tlist = NIL; + + /* + * An IndexScan whose targetlist reads its own ORDER BY values (see + * replace_orderby_tlist_refs in setrefs.c) uses INNER_VAR Vars numbered + * by ORDER BY key; they deparse as the ORDER BY expressions. + */ + dpns->orderby_tlist = NIL; + if (IsA(plan, IndexScan) && ((IndexScan *) plan)->indexorderbytlist > 0) + { + ListCell *lc; + AttrNumber i = 0; + + foreach(lc, ((IndexScan *) plan)->indexorderbyorig) + dpns->orderby_tlist = lappend(dpns->orderby_tlist, + makeTargetEntry((Expr *) lfirst(lc), + ++i, NULL, false)); + } } /* @@ -7988,6 +8007,18 @@ resolve_special_varno(Node *node, deparse_context *context, context->appendparents = save_appendparents; return; } + else if (var->varno == INNER_VAR && dpns->orderby_tlist) + { + TargetEntry *tle; + + tle = get_tle_by_resno(dpns->orderby_tlist, var->varattno); + if (!tle) + elog(ERROR, "bogus varattno for ORDER BY value var: %d", var->varattno); + + resolve_special_varno((Node *) tle->expr, context, + callback, callback_arg); + return; + } else if (var->varno == INNER_VAR && dpns->inner_tlist) { TargetEntry *tle; diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h index 79240333530..e19f7df2454 100644 --- a/src/include/access/amapi.h +++ b/src/include/access/amapi.h @@ -247,6 +247,15 @@ typedef struct IndexAmRoutine bool amcanorder; /* does AM support ORDER BY result of an operator on indexed column? */ bool amcanorderbyop; + + /* + * When an ordering scan reports xs_recheckorderby = false, may the + * executor substitute the AM's xs_orderbyvals for an identical ORDER BY + * expression in the scan's target list, rather than re-evaluating it? + * Setting this asserts that the value the AM reports IS the value the + * query should display. See nodeIndexscan.c. + */ + bool amorderbyvalsexact; /* does AM support hashing using API consistent with the hash AM? */ bool amcanhash; /* do operators within an opfamily have consistent equality semantics? */ diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index 91bb0bd2e13..07a404e665b 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1751,6 +1751,13 @@ typedef struct IndexScanState bool *iss_OrderByTypByVals; int16 *iss_OrderByTypLens; Size iss_PscanLen; + + /* + * When the plan's targetlist reads the ORDER BY values (indexorderbytlist + * > 0), the values of the row being returned, as a virtual slot installed + * as the projection's inner tuple. + */ + TupleTableSlot *iss_OrderBySlot; } IndexScanState; /* ---------------- diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 1c6d1fe3d04..1e4f2bd64e7 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1434,6 +1434,7 @@ typedef struct IndexOptInfo * (IndexAmRoutine). These fields are not set for partitioned indexes. */ bool amcanorderbyop; + bool amorderbyvalsexact; bool amoptionalkey; bool amsearcharray; bool amsearchnulls; diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 09a1ec73180..295d2b9db69 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -616,6 +616,15 @@ typedef struct IndexScan List *indexorderbyops; /* forward or backward or don't care */ ScanDirection indexorderdir; + + /* + * May setrefs.c replace target-list occurrences of an indexorderbyorig + * expression with INNER_VAR references to the scan's ORDER BY values? + * Copied from the AM's amorderbyvalsexact. indexorderbytlist counts the + * replacements actually made (0 if none), for EXPLAIN. + */ + bool indexorderbyexact; + int indexorderbytlist; } IndexScan; /* ---------------- diff --git a/src/test/regress/expected/box.out b/src/test/regress/expected/box.out index 10760870ce7..60d92a26e1e 100644 --- a/src/test/regress/expected/box.out +++ b/src/test/regress/expected/box.out @@ -594,10 +594,10 @@ SET enable_bitmapscan = OFF; EXPLAIN (COSTS OFF) SELECT rank() OVER (ORDER BY b <-> point '123,456') n, b <-> point '123,456' dist, id FROM quad_box_tbl; - QUERY PLAN --------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------- WindowAgg - Window: w1 AS (ORDER BY (b <-> '(123,456)'::point) ROWS UNBOUNDED PRECEDING) + Window: w1 AS (ORDER BY ((b <-> '(123,456)'::point)) ROWS UNBOUNDED PRECEDING) -> Index Scan using quad_box_tbl_idx on quad_box_tbl Order By: (b <-> '(123,456)'::point) (4 rows) @@ -617,10 +617,10 @@ WHERE seq.id IS NULL OR idx.id IS NULL; EXPLAIN (COSTS OFF) SELECT rank() OVER (ORDER BY b <-> point '123,456') n, b <-> point '123,456' dist, id FROM quad_box_tbl WHERE b <@ box '((200,300),(500,600))'; - QUERY PLAN --------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------- WindowAgg - Window: w1 AS (ORDER BY (b <-> '(123,456)'::point) ROWS UNBOUNDED PRECEDING) + Window: w1 AS (ORDER BY ((b <-> '(123,456)'::point)) ROWS UNBOUNDED PRECEDING) -> Index Scan using quad_box_tbl_idx on quad_box_tbl Index Cond: (b <@ '(500,600),(200,300)'::box) Order By: (b <-> '(123,456)'::point) diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out index ac79f94aa80..82d11964161 100644 --- a/src/test/regress/expected/gist.out +++ b/src/test/regress/expected/gist.out @@ -387,6 +387,141 @@ select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; ERROR: lossy distance functions are not supported in index-only scans +drop index gist_tbl_multi_index; +-- Test that an ordering Index Scan returns the AM's ORDER BY values to a +-- targetlist entry equal to the ORDER BY expression, instead of evaluating +-- the expression again (amorderbyvalsexact). +set enable_indexonlyscan = off; +-- An expression index over a counting function proves the operator is not +-- re-evaluated: the count stays at zero for the rows returned. +create sequence gist_cnt_seq; +create function gist_cnt_pt(point) returns point language plpgsql immutable + as $$ begin perform nextval('public.gist_cnt_seq'); return $1; end $$; +create index gist_tbl_cnt_index on gist_tbl using gist (gist_cnt_pt(p)); +explain (verbose, costs off) +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; + QUERY PLAN +------------------------------------------------------------------------ + Limit + Output: p, ((gist_cnt_pt(p) <-> '(0.201,0.201)'::point)) + -> Index Scan using gist_tbl_cnt_index on public.gist_tbl + Output: p, ((gist_cnt_pt(p) <-> '(0.201,0.201)'::point)) + Order By: (gist_cnt_pt(gist_tbl.p) <-> '(0.201,0.201)'::point) + Order By Values Used: 1 +(6 rows) + +select setval('gist_cnt_seq', 1, false); + setval +-------- + 1 +(1 row) + +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; + p | dist +-------------+----------------------- + (0.2,0.2) | 0.0014142135623730963 + (0.25,0.25) | 0.06929646455628163 + (0.15,0.15) | 0.07212489168102787 +(3 rows) + +select nextval('gist_cnt_seq') - 1 as calls_during_scan; + calls_during_scan +------------------- + 0 +(1 row) + +-- Every copy of the ORDER BY expression is served from the index; an +-- expression that merely contains it is not. +explain (verbose, costs off) +select gist_cnt_pt(p) <-> point(0,0) as d1, + (gist_cnt_pt(p) <-> point(0,0)) * 2 as d2, + gist_cnt_pt(p) <-> point(0,0) as d3 + from gist_tbl order by gist_cnt_pt(p) <-> point(0,0) limit 2; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: ((gist_cnt_pt(p) <-> '(0,0)'::point)), (((gist_cnt_pt(p) <-> '(0,0)'::point) * '2'::double precision)), ((gist_cnt_pt(p) <-> '(0,0)'::point)) + -> Index Scan using gist_tbl_cnt_index on public.gist_tbl + Output: ((gist_cnt_pt(p) <-> '(0,0)'::point)), ((gist_cnt_pt(p) <-> '(0,0)'::point) * '2'::double precision), ((gist_cnt_pt(p) <-> '(0,0)'::point)) + Order By: (gist_cnt_pt(gist_tbl.p) <-> '(0,0)'::point) + Order By Values Used: 2 +(6 rows) + +drop index gist_tbl_cnt_index; +drop function gist_cnt_pt(point); +drop sequence gist_cnt_seq; +-- The box opclass's distance to a point is lossy, so this exercises the +-- recheck and reorder-queue paths; the displayed values must match the +-- operator's. +create index gist_tbl_box_index on gist_tbl using gist (b); +explain (verbose, costs off) +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91); + QUERY PLAN +-------------------------------------------------------- + Index Scan using gist_tbl_box_index on public.gist_tbl + Output: b, ((b <-> '(5.2,5.91)'::point)) + Index Cond: (gist_tbl.b <@ '(6,6),(5,5)'::box) + Order By: (gist_tbl.b <-> '(5.2,5.91)'::point) + Order By Values Used: 1 +(5 rows) + +-- the distance numbers are not exactly the same across platforms +set extra_float_digits = 0; +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91); + b | dist +-------------------------+------------------- + (5.55,5.55),(5.55,5.55) | 0.502095608425328 + (5.6,5.6),(5.6,5.6) | 0.5060632371552 + (5.5,5.5),(5.5,5.5) | 0.508035431835221 + (5.65,5.65),(5.65,5.65) | 0.519711458407451 + (5.45,5.45),(5.45,5.45) | 0.523545604508337 + (5.7,5.7),(5.7,5.7) | 0.54230987451825 + (5.4,5.4),(5.4,5.4) | 0.547813836992093 + (5.75,5.75),(5.75,5.75) | 0.572800139664787 + (5.35,5.35),(5.35,5.35) | 0.57974132162543 + (5.8,5.8),(5.8,5.8) | 0.61 + (5.3,5.3),(5.3,5.3) | 0.618142378421024 + (5.85,5.85),(5.85,5.85) | 0.652763356814703 + (5.25,5.25),(5.25,5.25) | 0.661891229734917 + (5.9,5.9),(5.9,5.9) | 0.700071424927486 + (5.2,5.2),(5.2,5.2) | 0.71 + (5.95,5.95),(5.95,5.95) | 0.751065909225016 + (5.15,5.15),(5.15,5.15) | 0.761642960973184 + (6,6),(6,6) | 0.805046582503149 + (5.1,5.1),(5.1,5.1) | 0.816149496109629 + (5.05,5.05),(5.05,5.05) | 0.872983390449097 + (5,5),(5,5) | 0.931718841711383 +(21 rows) + +reset extra_float_digits; +select count(*) filter (where dist = b <-> point(5.2, 5.91)) as same, + count(*) as total + from (select b, b <-> point(5.2, 5.91) as dist from gist_tbl + order by b <-> point(5.2, 5.91) limit 200) ss; + same | total +------+------- + 200 | 200 +(1 row) + +-- Same, under row locking (the tuple is re-fetched by LockRows). +set extra_float_digits = 0; +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91) + limit 3 for update; + b | dist +-------------------------+------------------- + (5.55,5.55),(5.55,5.55) | 0.502095608425328 + (5.6,5.6),(5.6,5.6) | 0.5060632371552 + (5.5,5.5),(5.5,5.5) | 0.508035431835221 +(3 rows) + +reset extra_float_digits; +drop index gist_tbl_box_index; +reset enable_indexonlyscan; -- Test that an index-only scan deforms the tuple it reconstructs with the -- descriptor the AM formed it with, not the scan slot's descriptor. create temp table gist_ios_tupdesc (a inet, r numrange); diff --git a/src/test/regress/expected/polygon.out b/src/test/regress/expected/polygon.out index c01848f103e..e9560664870 100644 --- a/src/test/regress/expected/polygon.out +++ b/src/test/regress/expected/polygon.out @@ -283,10 +283,10 @@ SET enable_bitmapscan = OFF; EXPLAIN (COSTS OFF) SELECT rank() OVER (ORDER BY p <-> point '123,456') n, p <-> point '123,456' dist, id FROM quad_poly_tbl WHERE p <@ polygon '((300,300),(400,600),(600,500),(700,200))'; - QUERY PLAN ---------------------------------------------------------------------------------- + QUERY PLAN +---------------------------------------------------------------------------------- WindowAgg - Window: w1 AS (ORDER BY (p <-> '(123,456)'::point) ROWS UNBOUNDED PRECEDING) + Window: w1 AS (ORDER BY ((p <-> '(123,456)'::point)) ROWS UNBOUNDED PRECEDING) -> Index Scan using quad_poly_tbl_idx on quad_poly_tbl Index Cond: (p <@ '((300,300),(400,600),(600,500),(700,200))'::polygon) Order By: (p <-> '(123,456)'::point) diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql index 57dcc082450..dbd22285c1d 100644 --- a/src/test/regress/sql/gist.sql +++ b/src/test/regress/sql/gist.sql @@ -169,6 +169,71 @@ explain (verbose, costs off) select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; +drop index gist_tbl_multi_index; + +-- Test that an ordering Index Scan returns the AM's ORDER BY values to a +-- targetlist entry equal to the ORDER BY expression, instead of evaluating +-- the expression again (amorderbyvalsexact). +set enable_indexonlyscan = off; + +-- An expression index over a counting function proves the operator is not +-- re-evaluated: the count stays at zero for the rows returned. +create sequence gist_cnt_seq; +create function gist_cnt_pt(point) returns point language plpgsql immutable + as $$ begin perform nextval('public.gist_cnt_seq'); return $1; end $$; +create index gist_tbl_cnt_index on gist_tbl using gist (gist_cnt_pt(p)); + +explain (verbose, costs off) +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; + +select setval('gist_cnt_seq', 1, false); +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; +select nextval('gist_cnt_seq') - 1 as calls_during_scan; + +-- Every copy of the ORDER BY expression is served from the index; an +-- expression that merely contains it is not. +explain (verbose, costs off) +select gist_cnt_pt(p) <-> point(0,0) as d1, + (gist_cnt_pt(p) <-> point(0,0)) * 2 as d2, + gist_cnt_pt(p) <-> point(0,0) as d3 + from gist_tbl order by gist_cnt_pt(p) <-> point(0,0) limit 2; + +drop index gist_tbl_cnt_index; +drop function gist_cnt_pt(point); +drop sequence gist_cnt_seq; + +-- The box opclass's distance to a point is lossy, so this exercises the +-- recheck and reorder-queue paths; the displayed values must match the +-- operator's. +create index gist_tbl_box_index on gist_tbl using gist (b); + +explain (verbose, costs off) +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91); + +-- the distance numbers are not exactly the same across platforms +set extra_float_digits = 0; +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91); +reset extra_float_digits; + +select count(*) filter (where dist = b <-> point(5.2, 5.91)) as same, + count(*) as total + from (select b, b <-> point(5.2, 5.91) as dist from gist_tbl + order by b <-> point(5.2, 5.91) limit 200) ss; + +-- Same, under row locking (the tuple is re-fetched by LockRows). +set extra_float_digits = 0; +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91) + limit 3 for update; +reset extra_float_digits; + +drop index gist_tbl_box_index; +reset enable_indexonlyscan; + -- Test that an index-only scan deforms the tuple it reconstructs with the -- descriptor the AM formed it with, not the scan slot's descriptor. create temp table gist_ios_tupdesc (a inet, r numrange); -- 2.50.1