Re: Let an ordering index scan hand its ORDER BY value to the target list - Mailing list pgsql-hackers

From Greg Burd
Subject Re: Let an ordering index scan hand its ORDER BY value to the target list
Date
Msg-id ChGfmrrDcMPM9quqy69FYxEBqDxgkNG9EgqFal9njID6HIc1O_0E6lmVIbe4wOk-MLR4eGOneBki_awfQIBUlYB6Iz-z5B8BdMfGBkwFGGQ=@burd.me
Whole thread
In response to Re: Let an ordering index scan hand its ORDER BY value to the target list  (Greg Burd <greg@burd.me>)
Responses Re: Let an ordering index scan hand its ORDER BY value to the target list
List pgsql-hackers
On Thursday, October 8th, 2026 at 10:25 AM, Greg Burd <greg@burd.me> wrote:
> An ordered or parallel Append still computes the expression above
> itself.  That could be added if anyone cares.

I cared, so v8 adds 0007, which closes the gaps I listed.  0001-0006 are
unchanged from v7.

* The nullable side of an outer join.  Above the join, the query's
  copy of expensive(a.i) has the join's bit in a.i's nullingrels and
  the scan's copy doesn't, so they never matched.  But the join
  null-extends the scan's value along with the Vars, so the two agree
  on every row whenever the expression is strict.  For a strict one,
  0007 relabels the value with the joinrel's Vars as it passes it up.
  At an outer join, setrefs matches it ignoring nullingrels, as it
  already does for Vars there (NRM_SUPERSET), and the cost model credits
  it the same way.  A non-strict expression is still computed above the
  join.  Heikki's expensive() from 2023 isn't declared STRICT, so it's
  in that group.

* Partial paths.  0006 emitted only parallel-safe values from them, but
  a worker never evaluates the expression: it reads the stored value,
  and everything above it references the column.  So a PARALLEL
  RESTRICTED function's index can feed a parallel plan now.  Gather and
  Gather Merge pass the values up.

* Ordered Append, MergeAppend and parallel Append, built the same way
  as 0006's unordered Append.

Against 0006, paired runs as before (two sessions of 11 blocks; the
sessions agreed to within 0.4 points):

  LEFT JOIN, nullable side                   -62%
  FULL JOIN                                  -95%   (173 -> 8.8 ms)
  coalesce(expensive(a.i), -1), nullable     -40%
  partitioned, ordered Append, merge join    -29%
  partitioned join, parallel allowed         -19%
  inner join / no expression index           within +/-0.6%

The FULL JOIN number is a plan change.  Without the value the planner
picks a hash full join over two seq scans and computes expensive() for
every row; with it, a merge full join over the index-only scan.

0007 costs very little at plan time: at most +2.5% planning over 0006
(eleven-rel inner join with the expression).  Planner memory is the same
for inner joins and up to 3% higher for LEFT JOINs with the expression
(60 -> 62 kB at three rels).

MERGE was the one surprise.  The first version stripped nullingrels
with remove_nulling_relids(), which errors on a MERGE's ROWID_VAR, and
merge.out and updatable_views.out caught it.  0007 uses its own small
mutator now.  To check parallel safety, the correctness run has a
PARALLEL RESTRICTED indexed function that raises an error if it's ever
evaluated in a worker.  A canary marked PARALLEL SAFE with the same
check does fail, so the check works.  48 query shapes under seven
settings (including debug_parallel_query), compared with
enable_indexonlyscan off: no mismatches.  0006 and 0007 each build
without warnings and pass the meson suite on master (16b94c40120).

The harness and samples are in cf7392-bench-0007.tar.gz.nocfbot.

best.

-greg

Attachment

pgsql-hackers by date:

Previous
From: Nazir Bilal Yavuz
Date:
Subject: Re: Adding init-po and update-po targets to the meson build system
Next
From: Álvaro Herrera
Date:
Subject: Re: Adding init-po and update-po targets to the meson build system