Hi zthhh,
> In my test, the original IN query already uses Parallel Index Only Scans.
> Each of the 1000 scanned partitions returns one row, with zero heap
> fetches, and all buffer accesses are hits. Nevertheless, execution takes
> about 987 ms.
You're right that the cost isn't just the seq-scan choice, and it isn't the
index work either. I reproduced your plan shape on 18.6, on 19devel
(f2d7570) and on master; all three agree.
Each child reports "Index Searches: 1" -- one descent per child, not one per
array element. The time is in shipping the plan to the parallel workers.
Under perf, the leader spends ~41% in nodeToString -> outDatum, printing each
child's 1000-element bigint array byte by byte, and the workers spend ~37% in
stringToNode -> readDatum parsing them back; the index probes are ~2.6%. In
your plan the Gather returns its first row at 651 of 973 ms; in the same shape
here, 287 of 418 ms, while the 1000 child scans together take 17.7 ms.
Toggling only parallelism on the same full-array index plan, execution goes
from 14.0 ms to 419 ms. The same plan with the Gather but 0 workers launched
is still 290 ms: the leader serializes the plan anyway, and the 1000-element
array is in every child.
Even serially, the full array still costs at execution with the index: 14.0 ms
vs 5.9 ms when each child carries only its own constant, because
_bt_preprocess_keys sorts and de-dups the 1000 keys per child before its
single descent.
So the restriction helps both cases, for different reasons: it takes the
estimate to 1 (which is what picked the seq scan in the original report) and
it shrinks what each child preprocesses and what the Gather ships. Measured
against today's IN, same binary: the serial seq-scan case is 186.8 ms wall
(planning 129.5, execution 42.9); your VALUES rewrite is 25.4 ms (planning
10.5, execution 13.5); and restricting each child's array in place is 29.0 ms
(planning 21.5, execution 5.9). That is an 84% wall drop in the serial case
and 94% in your parallel shape; with the estimate corrected the planner no
longer parallelizes at all. The VALUES rewrite is a good workaround that
needs no planner change.
For where a fix would live: pruning already maps each constant to its
partition once (prune_append_rel_partitions is ~1% here), and that mapping is
in hand in expand_partitioned_rtentry, where restricting each live child to
its own constants gives the right 1000 rows. Range and list partitioning
could instead refute the irrelevant constants through the partition
constraint, but hash cannot, since satisfies_hash_partition() isn't something
predtest can refute -- reusing pruning's mapping is the route that also covers
hash.
I couldn't match your exact cost settings (your child index-only scan is
priced at 352 vs 25.9 here), so I reproduced both of your plan shapes with
explicit knobs rather than inferring them.
Attached are the measurements (scenarios, timings with 95% CIs, and the perf
breakdown), the raw median/CI summary, and the throwaway prototype I used to
bound the benefit -- measurement-only, single-column hash, not a proposal.
The full logs and repro harness are available if useful.
Regards,
Manu
# Full constant array per child after partition pruning: where the cost goes
Thread #75, "Partition-aware simplification of constant IN lists after partition
pruning" (pgsql-hackers). It reconciles two measurements:
- (A) ours, 2026-10-09: with max_parallel_workers_per_gather=0 the full array
inflates each child's estimate (706,992 vs 1,000) and the planner picks seq
scans.
- (B) the follow-up from zthhh, 2026-10-13: the original IN query already runs
as
Parallel Index Only Scans, one row per partition, yet takes ~987 ms of
execution
against ~53 ms for a VALUES rewrite.
Nothing here is a patch. Every number is read from an EXPLAIN, a timing log or a
perf report in `logs/`. Figures that are computed rather than measured are
labelled.
The in-place restriction is a **measurement-only prototype**
(`scripts/PROTOTYPE-restrict-saop.measurement-only.diff`).
## Setup
- Repro: the reporter's `partition_in_repro.sql` run unmodified (1024 HASH
partitions,
1M rows, VACUUM (ANALYZE)), and the follow-up's `rewrite.sql` used verbatim
(`upstream/`). The VALUES list is checked to be exactly the case-B id list.
- Settings: shared_buffers=128MB, enable_partition_pruning=on, jit=off,
autovacuum=off (static data). Parallelism is set per scenario.
- Builds, all `-O2` without asserts, on x86_64 (Zen 5):
- r18 = REL_18_STABLE 30baaacdd1c (reports 18.6)
- f2d7570 = f2d7570cdde (19devel, named in the thread)
- master = bfa1a69edd7
- proto = bfa1a69edd7 + the prototype, switched per session with
`SET proto.restrict_saop = on`. With it off, it plans and times like master
(`logs/structure_proto.jsonl`, `logs/summary.md`).
- Timing: carriles lane A, calibration sentinel per sample. 10 clean samples per
variant, each one the median of 5 executions in a fresh session.
- Client wall comes from psql `\timing`. Planning and execution come from
EXPLAIN (ANALYZE, TIMING OFF) through a helper.
- Reported as median [bootstrap 95% CI]. Comparisons use the same binary and
run.
- Two runs (master run1 gB, f2d7570 run1 gB) fell in a contended window (183
and
211 samples rejected) and were repeated once the machine was quiet.
- Profiles: perf with dwarf call graphs, following the postmaster so the leader
and
every parallel worker are sampled. Percentages are inclusive, of all samples.
### Scenarios
| name | session settings | plan obtained (read from EXPLAIN) |
|---|---|---|
| ser | max_parallel_workers_per_gather=0 | 1000 x Seq Scan (the original
report) |
| ser_idx | ser + enable_seqscan=off | 1000 x Index Only Scan, full array in
each |
| par | max_parallel_workers_per_gather=2 | Gather, 1000 x Parallel Seq Scan |
| par_idx | par + enable_seqscan=off | **the follow-up's shape**: Finalize
Aggregate / Gather (2 workers) / Partial Aggregate / Parallel Append / 1000 x
Parallel Index Only Scan, full array, Index Searches: 1 each |
| nl | par + enable_seqscan/hashjoin/mergejoin=off | **the follow-up's VALUES
shape**: Nested Loop / Values Scan / Append of IOS with run-time pruning, 1000
executed, 24 never executed |
| dpq | par_idx + parallel_setup_cost=1e9 + debug_parallel_query=on | the same
serial plan under a single-copy Gather (1 worker): identical shape with and
without the prototype |
| parF / parF_w0 | parallelism forced; w0 adds max_parallel_workers=0 | parF_w0
keeps the Gather but launches no worker |
| *_proto | + proto.restrict_saop=on | each child gets only its own constant |
The follow-up's attachment gives neither version nor settings. Its planner
prices a
child IOS at 352 (ours: 25.9) and still picks it over seq scans. Its VALUES join
picks a Nested Loop costing 1,435,428 over a hash join. We did not try to infer
its
cost settings. We reproduced its two **plan shapes** with the explicit knobs
above
and say so.
## 1. Where the ~987 ms go: shipping the arrays to the workers, not index probes
In the follow-up's own plan, the Parallel Append finishes in 9.6 ms per process
(`actual time=0.026..9.616`, loops=3). But the Gather returns its first row
only at
651.5 ms of 973.3 ms.
Same shape here (par_idx, master, TIMING ON,
`logs/explain_master_IN_par_idx.txt`):
the Gather's first row arrives at 287.6 ms of 417.7 ms, while the 1000 child
scans
together take 17.7 ms. That fraction (69 %) matches his (67 %).
Each child reports **Index Searches: 1**: one descent per child, not one per
array
element. The "N probes per child" hypothesis is not what this plan does on 18+.
What the time is (perf, `logs/perf_master_IN_par_idx.{leader,workers}.txt`):
| process | inclusive % of all samples | function |
|---|---|---|
| leader | 41.6 | ExecInitParallelPlan |
| leader | 40.8 | nodeToString -> outDatum (each 1000-element bigint array
printed byte by byte) |
| leader | 12.4 | scalararraysel (planning) |
| workers | 37.3 | stringToNode -> readDatum (parsing those arrays back) |
| workers | 3.2 | RelationBuildDesc (cold relcache, 1000 partitions) |
| both | ~2.6 | _bt_first (the index probes themselves) |
Measured split (proto build, run1 / run2, execution ms):
- Gather planned, **0 workers launched** (parF_w0): 289.9 / 280.3. The leader
serializes the plan anyway and then runs it alone.
- Same plan **with workers** (parF): 424.6 / 409.5, i.e. +46.5 % [+44.6, +49.0].
That extra is the workers deserializing the plan.
- Same serial plan, **no Gather** (ser_idx): 13.7.
So in that shape, about 276 ms is leader serialization and about 135 ms is
worker
deserialization; the index scans themselves take about 14 ms. (Computed from
the three
medians above.)
## 2. Reconciling the numbers: why forcing the index gave ~14 ms here and ~987
ms there
Same binary (master, run2), same full-array Index Only Scan plan, only
parallelism
toggled:
| | planning | execution | client wall |
|---|---|---|---|
| ser_idx (no Gather) | 134.3 [131.3, 134.6] | **13.99 [13.90, 14.18]** | 163.1
|
| par_idx (follow-up's shape) | 85.3 [84.7, 86.3] | **419.1 [415.3, 422.6]** |
506.2 |
Execution +2894.8 % [+2850.0, +2928.2]; r18, f2d7570 and master agree (+2948 to
+3101 %).
The two observations measure different things, and both stand:
- Ours (serial): the full array costs a bad estimate and, with it, a seq-scan
plan.
- His (parallel): the full array is also copied into the plan that every worker
receives, and that copy dominates execution even though the index is chosen.
Absolute numbers differ by machine and cost model:
| | planning | execution |
|---|---|---|
| his IN | 324.9 | 987.1 |
| ours, same shape | 85 | 419 |
| his VALUES (attached run) | 22.2 | 59.6 |
| his VALUES (quoted in the mail; not in the attachment) | 31.7 | 53.4 |
| ours VALUES | 10.3 | 13.3 |
What carries over is the shape: Gather-first-row fraction 67 % vs 69 %, Index
Searches 1 per child, 3000 shared hits in both.
Even serially, the full array still costs execution with the index (13.99 vs
5.86 ms restricted). Profile `logs/perf_master_IN_ser_idx.leader.txt`: per
child,
`_bt_preprocess_keys` sorts and dedups the 1000 keys (`qsort_arg`,
`_bt_compare_array_elements`, `deconstruct_array`) before its single descent.
That is
6.5 % of 10.1 % executor samples. It is per-child array preprocessing, not
per-element
probing.
## 3. IN vs VALUES vs restricted array (planning + execution)
Same binary (proto) and run (run1Q), ms, median [95 % CI]:
| query | plan | planning | execution | client wall |
|---|---|---|---|---|
| IN, today (ser) | 1000 x Seq Scan, est 706,992 | 129.50 [128.14, 131.29] |
42.88 [42.51, 43.21] | 186.83 |
| VALUES join (nl, rows) | Nested Loop + run-time pruning | 10.59 [10.21,
10.85] | 13.47 [13.10, 13.80] | 25.44 |
| VALUES join (nl, count(*)) | same | 10.51 [10.21, 10.97] | 13.59 [13.40,
13.75] | 25.71 |
| UNION ALL split (earlier upper bound) | 1000 x IOS under Subquery Scans |
295.73 [294.43, 298.09] | 6.90 [6.60, 7.28] | 304.12 |
| **IN, restricted in place** (ser_proto) | 1000 x IOS, est 1,000 | 21.51
[21.23, 22.20] | **5.88 [5.80, 6.08]** | 28.95 |
Rows vs count(*) forms of the rewrite are within noise (+1.1 % [-3.0, +3.3]
wall).
The ceiling of the in-place restriction, against today's IN, same binary:
| scenario | wall | planning | execution |
|---|---|---|---|
| serial, seq-scan case (ser -> ser_proto) | -84.4 % [-84.7, -84.0] | -83.5 % |
-86.7 % [-87.1, -86.4] |
| serial, forced index (ser_idx -> ser_proto) | -81.6 % | -83.5 % | -57.2 %
[-58.3, -56.1] |
| parallel allowed (par -> par_proto) | -91.7 % | -82.4 % | -97.3 % |
| **follow-up's shape (par_idx -> par_proto)** | **-94.0 % [-94.2, -93.8]** |
-73.9 % | **-98.6 % [-98.6, -98.6]** |
| identical single-worker Gather (dpq -> dpq_proto) | -88.3 % | -77.7 % | -90.2
% [-90.4, -90.1] |
With the restriction, the planner no longer goes parallel at all: estimate
1,000,
cheap serial IOS. That is a large part of the parallel win (par_proto 30.6 ms
wall
vs 510.1).
On a Gather of identical shape (dpq), the restriction still removes 90 % of
execution: 445.0 -> 43.5 ms. The Gather's first row moves from 413 ms to 35.5 ms
(`logs/structure_proto.jsonl`). What remains in the worker is opening 1000
partitions with a cold relcache.
- **In-place vs the UNION ALL bound:** execution is 14.8 % [-19.4, -10.7] lower
in
place (no Subquery Scan per child), so the earlier split was indeed an upper
bound.
Its planning on 19+/master is dominated by the unrelated `choose_plan_name`
quadratic (separate thread).
- **In-place vs VALUES:** execution -56.7 % [-57.6, -55.0] (5.9 vs 13.6 ms: no
run-time pruning step per outer row). Planning +104.7 % (21.5 vs 10.5 ms).
Wall
+12.6 % (29.0 vs 25.7 ms).
The prototype's 21.5 ms of planning is not what a real implementation would
cost.
Profile `logs/perf_proto_IN_ser_proto.leader.txt`:
- The prototype function, inlined in `expand_partitioned_rtentry`, is 52.1 % of
backend samples. `compute_partition_hash_value` alone is 19.2 %, because the
prototype rehashes all 1000 constants for each of the 1000 children (10^6
hashes).
- Partition pruning, which already maps every constant to its partition once, is
1.1 % (`prune_append_rel_partitions`).
**Computed, not measured:** reusing pruning's mapping instead of rehashing per
child
removes most of that 52 %, which puts planning in the 7-10 ms range. That is
the same
as the r18 UNION ALL split (10.76 ms) and the VALUES join (10.5 ms).
## 4. Planning: scalararraysel grows with children x constants (confirmed)
- Today's IN, serial: planning 129.5-133.3 ms on all builds.
- In-place restriction: 21.5 ms (-83.5 %).
- Profile of the follow-up's shape: scalararraysel is 12.4 % of all samples.
Planning
is a smaller share there only because execution became so large. In the serial
profile of 2026-10-09 it was 92.6 % of planning.
- The children x constants growth (0.09-0.13 us per child-constant pair, flip
to seq
scan between 250 and 500 ids) was measured on 2026-10-09
(our earlier message in this thread, 2026-10-09) and is not repeated here.
**Observed, not explained:** with enable_seqscan=off *and* parallelism allowed,
planning is ~35 % lower than serial with seqscan off (85 vs 134 ms, all builds,
narrow CIs). We did not investigate why; it does not change any comparison
above,
which are all within one scenario.
## 5. Is pruning's mapping reusable for the hash case?
- **Measured:** pruning already computes, once, which partition every constant
belongs to. `prune_append_rel_partitions` costs 1.1 % here, and restricting
each
live child to exactly those constants gives correct results: 1000 rows in
every
variant, checked per sample.
- **Measured:** the decision point exists. In `expand_partitioned_rtentry`,
right
after `relinfo->part_rels[i] = childrelinfo`, the child's partition index
`i`, the
parent's `boundinfo` and its translated `baserestrictinfo` are all at hand.
That is
where the prototype runs.
- **Not done and not claimed:** the prototype recomputes the hash per child
instead
of keeping pruning's per-constant result, and handles only a single-column
hash key
with a Const array. Range and list partitioning could instead go through
partition-constraint refutation as in [2]. Hash cannot, since
`satisfies_hash_partition()` is not refutable by predtest. For hash, reusing
pruning's mapping is the available route.
- **Not done and not claimed:** whether the planner should shrink the array
before
`set_rel_size` (estimate), before `create_plan` (serialized plan size), or
both.
The measurements say both matter. The estimate decides seq vs index and
parallel vs
not. The array size decides how much each child preprocesses and how much the
Gather ships.
## Files
- `scripts/`:
- env.sh, setup_cluster.sh (ephemeral clusters; `--drop` removes them)
- gen_queries.py, structure.py, show_structure.py, measure.py, run_all.sh,
summarize.py, profile.sh
- PROTOTYPE-restrict-saop.measurement-only.diff
- `upstream/`: both original attachments, the follow-up's rewrite.sql and
result.txt.
- `logs/`:
- summary.md: every median, CI and comparison
- samples_*.csv: raw samples with sentinel status
- explain_*: plans per build and variant
- structure_*.jsonl
- perf_*: leader and worker reports
- original_script_*.out.gz
# Timing summary: median [95% CI of the median], ms
- dirty samples discarded: f2d7570/run1: 3
- dirty samples discarded: master/run1: 49
- dirty samples discarded: master/run2: 53
- dirty samples discarded: proto/run1: 77
- dirty samples discarded: proto/run2: 7
- dirty samples discarded: r18/run1: 3
| build | run | variant | n | client wall | planning | execution |
|---|---|---|---|---|---|---|
| f2d7570 | run1 | IN@dpq | 10 | 604.03 [601.41, 605.00] | 132.19 [129.51,
133.64] | 459.53 [458.66, 460.70] |
| f2d7570 | run1 | IN@par | 10 | 370.42 [369.20, 372.00] | 126.11 [124.62,
128.49] | 231.23 [230.13, 231.57] |
| f2d7570 | run1 | IN@par_idx | 10 | 523.55 [520.64, 525.83] | 84.77 [83.95,
85.37] | 438.95 [437.52, 440.92] |
| f2d7570 | run1 | IN@ser | 10 | 187.69 [185.21, 190.15] | 129.46 [128.72,
130.31] | 43.85 [43.45, 44.20] |
| f2d7570 | run1 | IN@ser_idx | 10 | 158.17 [155.09, 160.70] | 130.86 [129.38,
131.73] | 13.78 [13.65, 13.94] |
| f2d7570 | run1 | SPLIT@par | 10 | 361.96 [356.68, 365.27] | 319.03 [315.74,
322.00] | 36.85 [36.52, 37.15] |
| f2d7570 | run1 | SPLIT@ser | 10 | 335.49 [334.53, 339.94] | 324.00 [320.25,
326.00] | 6.97 [6.60, 7.19] |
| f2d7570 | run1 | VALUESc@nl | 10 | 26.92 [26.25, 27.50] | 10.95 [10.18,
11.42] | 13.85 [13.71, 14.26] |
| f2d7570 | run1 | VALUESc@par | 10 | 41.36 [40.40, 42.62] | 10.79 [10.43,
11.12] | 34.60 [34.20, 35.54] |
| f2d7570 | run1 | VALUESc@ser | 10 | 47.80 [47.25, 48.36] | 10.33 [10.03,
10.51] | 48.78 [48.47, 49.58] |
| f2d7570 | run1 | VALUESr@nl | 10 | 26.28 [25.38, 27.64] | 10.70 [10.56,
11.40] | 13.62 [13.43, 13.97] |
| f2d7570 | run1 | VALUESr@par | 10 | 41.80 [40.51, 42.70] | 9.84 [9.65, 10.10]
| 35.56 [35.12, 35.92] |
| f2d7570 | run1 | VALUESr@ser | 10 | 47.81 [47.18, 50.09] | 10.27 [9.65,
10.62] | 48.63 [48.09, 49.19] |
| master | run1 | IN@dpq | 10 | 594.79 [588.34, 601.23] | 86.11 [85.26, 87.12]
| 441.76 [440.28, 444.17] |
| master | run1 | IN@par | 10 | 367.57 [364.03, 373.94] | 129.74 [127.82,
131.36] | 224.37 [222.37, 230.93] |
| master | run1 | IN@par_idx | 9 | 510.27 [505.70, 513.59] | 86.69 [86.14,
87.84] | 424.06 [421.21, 427.58] |
| master | run1 | IN@ser | 10 | 193.63 [189.66, 195.30] | 133.22 [131.39,
134.51] | 44.38 [43.67, 44.97] |
| master | run1 | IN@ser_idx | 10 | 163.62 [162.06, 165.43] | 134.53 [133.20,
135.91] | 13.91 [13.76, 14.04] |
| master | run1 | SPLIT@par | 10 | 330.06 [329.06, 330.98] | 291.66 [289.93,
292.94] | 36.80 [36.32, 37.19] |
| master | run1 | SPLIT@ser | 10 | 304.55 [304.00, 307.03] | 297.76 [295.82,
298.90] | 6.91 [6.62, 7.23] |
| master | run1 | VALUESc@nl | 10 | 26.31 [25.73, 26.94] | 10.83 [10.68, 11.16]
| 13.56 [13.47, 13.70] |
| master | run1 | VALUESc@par | 10 | 40.80 [40.32, 41.94] | 10.63 [10.48,
10.80] | 34.87 [34.32, 35.48] |
| master | run1 | VALUESc@ser | 10 | 47.21 [46.78, 48.25] | 10.07 [9.70, 10.67]
| 46.62 [46.43, 46.87] |
| master | run1 | VALUESr@nl | 10 | 25.47 [24.95, 25.95] | 10.27 [10.04, 10.79]
| 13.32 [13.08, 13.56] |
| master | run1 | VALUESr@par | 10 | 41.55 [40.62, 42.31] | 10.53 [10.35,
10.75] | 34.70 [33.94, 34.96] |
| master | run1 | VALUESr@ser | 10 | 47.25 [46.94, 48.21] | 9.82 [9.55, 10.12]
| 46.75 [46.13, 47.27] |
| master | run2 | IN@dpq | 10 | 593.96 [580.26, 596.58] | 84.81 [83.95, 85.73]
| 440.99 [429.92, 445.33] |
| master | run2 | IN@par | 10 | 363.48 [358.27, 368.01] | 128.68 [124.45,
130.65] | 222.08 [218.94, 224.70] |
| master | run2 | IN@par_idx | 10 | 506.18 [501.80, 509.82] | 85.33 [84.74,
86.26] | 419.10 [415.29, 422.60] |
| master | run2 | IN@ser | 10 | 191.52 [188.19, 194.61] | 133.33 [128.18,
135.81] | 44.55 [43.58, 45.14] |
| master | run2 | IN@ser_idx | 10 | 163.12 [162.01, 164.10] | 134.26 [131.27,
134.64] | 13.99 [13.90, 14.18] |
| proto | run1 | IN@dpq | 10 | 597.99 [596.53, 602.60] | 86.97 [85.67, 87.55] |
444.98 [442.38, 447.88] |
| proto | run1 | IN@dpq_proto | 10 | 69.77 [68.21, 71.01] | 19.41 [19.02,
19.81] | 43.49 [42.70, 44.04] |
| proto | run1 | IN@par | 10 | 368.99 [366.56, 372.90] | 129.41 [128.24,
130.27] | 224.44 [221.88, 232.89] |
| proto | run1 | IN@parF | 10 | 507.62 [501.42, 510.35] | 86.44 [85.50, 88.52]
| 424.60 [420.20, 427.98] |
| proto | run1 | IN@parF_w0 | 10 | 376.30 [371.25, 379.68] | 85.48 [84.53,
86.02] | 289.91 [285.57, 292.47] |
| proto | run1 | IN@par_idx | 10 | 510.12 [505.31, 514.32] | 87.39 [86.65,
88.40] | 423.80 [421.72, 425.64] |
| proto | run1 | IN@par_proto | 10 | 30.55 [29.96, 31.60] | 22.80 [22.13,
23.20] | 5.97 [5.73, 6.10] |
| proto | run1 | IN@ser | 10 | 190.78 [189.01, 192.56] | 133.34 [129.76,
134.07] | 44.19 [43.45, 44.47] |
| proto | run1 | IN@ser_idx | 10 | 161.90 [158.01, 163.97] | 133.17 [130.43,
133.94] | 13.69 [13.58, 13.78] |
| proto | run1 | IN@ser_proto | 10 | 29.81 [29.30, 30.61] | 22.02 [21.67,
22.85] | 5.86 [5.71, 5.98] |
| proto | run1Q | IN@ser | 10 | 186.83 [184.30, 189.50] | 129.50 [128.14,
131.29] | 42.88 [42.51, 43.21] |
| proto | run1Q | IN@ser_proto | 10 | 28.95 [28.16, 29.92] | 21.51 [21.23,
22.20] | 5.88 [5.80, 6.08] |
| proto | run1Q | SPLIT@ser | 10 | 304.12 [302.36, 305.73] | 295.73 [294.43,
298.09] | 6.90 [6.60, 7.28] |
| proto | run1Q | VALUESc@nl | 10 | 25.71 [25.18, 26.31] | 10.51 [10.21, 10.97]
| 13.59 [13.40, 13.75] |
| proto | run1Q | VALUESr@nl | 10 | 25.44 [25.14, 26.34] | 10.59 [10.21, 10.85]
| 13.47 [13.10, 13.80] |
| proto | run2 | IN@dpq_proto | 10 | 64.98 [64.17, 66.95] | 18.44 [18.13,
18.65] | 41.20 [40.62, 42.53] |
| proto | run2 | IN@parF | 10 | 492.21 [490.53, 494.30] | 83.56 [82.62, 84.22]
| 409.47 [406.06, 412.81] |
| proto | run2 | IN@parF_w0 | 10 | 362.28 [359.36, 364.13] | 83.24 [81.77,
85.08] | 280.32 [278.97, 281.84] |
| proto | run2 | IN@par_proto | 10 | 28.63 [27.86, 30.02] | 22.01 [21.55,
22.71] | 5.78 [5.70, 6.00] |
| proto | run2 | IN@ser_proto | 10 | 28.44 [27.84, 29.65] | 21.28 [21.15,
22.13] | 5.65 [5.58, 5.84] |
| r18 | run1 | IN@dpq | 10 | 598.76 [595.57, 599.94] | 82.19 [81.07, 83.73] |
455.13 [452.82, 456.76] |
| r18 | run1 | IN@par | 10 | 366.48 [365.63, 367.49] | 123.45 [122.33, 125.33]
| 228.53 [227.48, 231.00] |
| r18 | run1 | IN@par_idx | 10 | 517.55 [516.56, 518.91] | 82.26 [81.92, 83.05]
| 436.26 [434.98, 437.12] |
| r18 | run1 | IN@ser | 10 | 183.84 [181.53, 186.52] | 126.53 [124.31, 128.00]
| 43.57 [43.30, 43.96] |
| r18 | run1 | IN@ser_idx | 10 | 152.52 [151.61, 156.54] | 126.56 [125.11,
128.16] | 13.63 [13.54, 13.81] |
| r18 | run1 | SPLIT@par | 10 | 52.81 [51.98, 53.81] | 11.08 [11.00, 11.64] |
37.82 [37.34, 38.81] |
| r18 | run1 | SPLIT@ser | 10 | 24.37 [23.79, 25.15] | 10.76 [10.53, 11.21] |
6.87 [6.69, 7.26] |
| r18 | run1 | VALUESc@nl | 10 | 27.18 [26.70, 27.70] | 10.73 [10.51, 11.77] |
14.31 [14.14, 15.29] |
| r18 | run1 | VALUESc@par | 10 | 42.49 [41.48, 43.68] | 10.82 [10.75, 11.23] |
36.62 [35.76, 37.21] |
| r18 | run1 | VALUESc@ser | 10 | 47.57 [47.02, 49.23] | 10.18 [10.10, 10.48] |
49.34 [48.94, 49.96] |
| r18 | run1 | VALUESr@nl | 10 | 26.74 [26.45, 27.06] | 10.61 [10.38, 11.01] |
13.89 [13.75, 14.27] |
| r18 | run1 | VALUESr@par | 10 | 41.79 [41.04, 43.21] | 9.67 [9.60, 9.97] |
36.56 [35.67, 37.75] |
| r18 | run1 | VALUESr@ser | 10 | 48.28 [47.59, 50.09] | 10.21 [9.65, 11.03] |
48.97 [48.77, 50.95] |
# Comparisons: (to - from) / from, median [95% CI], same binary and run
| build | run | from -> to | what | wall % | planning % | execution % |
|---|---|---|---|---|---|---|
| f2d7570 | run1 | IN@ser_idx -> IN@par_idx | parallel penalty, same full-array
index plan | +231.0 [+226.3, +237.6] | -35.2 [-36.1, -34.5] | +3086.1 [+3043.6,
+3117.9] |
| f2d7570 | run1 | IN@par_idx -> VALUESc@nl | IN (follow-up shape) vs VALUES
nested loop | -94.9 [-95.0, -94.7] | -87.1 [-88.0, -86.5] | -96.8 [-96.9,
-96.7] |
| f2d7570 | run1 | VALUESr@nl -> VALUESc@nl | rows vs count(*) form of the
rewrite | +2.4 [-3.0, +6.4] | +2.4 [-6.6, +6.8] | +1.7 [-0.7, +5.0] |
| master | run1 | IN@ser_idx -> IN@par_idx | parallel penalty, same full-array
index plan | +211.9 [+208.0, +215.3] | -35.6 [-36.4, -34.4] | +2947.8 [+2917.9,
+2992.2] |
| master | run1 | IN@par_idx -> VALUESc@nl | IN (follow-up shape) vs VALUES
nested loop | -94.8 [-95.0, -94.7] | -87.5 [-87.7, -87.1] | -96.8 [-96.8,
-96.8] |
| master | run1 | VALUESr@nl -> VALUESc@nl | rows vs count(*) form of the
rewrite | +3.3 [+0.2, +7.4] | +5.5 [+0.1, +9.2] | +1.9 [-0.1, +3.6] |
| master | run2 | IN@ser_idx -> IN@par_idx | parallel penalty, same full-array
index plan | +210.3 [+207.0, +213.8] | -36.4 [-36.9, -34.9] | +2894.8 [+2850.0,
+2928.2] |
| proto | run1 | IN@ser_idx -> IN@par_idx | parallel penalty, same full-array
index plan | +215.1 [+209.9, +221.8] | -34.4 [-35.0, -32.9] | +2994.9 [+2971.7,
+3024.6] |
| proto | run1 | IN@ser -> IN@ser_proto | restriction, serial (seq-scan case) |
-84.4 [-84.7, -84.0] | -83.5 [-83.8, -82.7] | -86.7 [-87.1, -86.4] |
| proto | run1 | IN@ser_idx -> IN@ser_proto | restriction vs forced index,
serial | -81.6 [-82.0, -81.0] | -83.5 [-83.8, -82.7] | -57.2 [-58.3, -56.1] |
| proto | run1 | IN@par -> IN@par_proto | restriction, parallel allowed | -91.7
[-91.9, -91.4] | -82.4 [-82.9, -82.0] | -97.3 [-97.5, -97.3] |
| proto | run1 | IN@par_idx -> IN@par_proto | restriction vs the follow-up's
plan shape | -94.0 [-94.2, -93.8] | -73.9 [-74.8, -73.4] | -98.6 [-98.6, -98.6]
|
| proto | run1 | IN@dpq -> IN@dpq_proto | restriction, identical single-worker
Gather plan | -88.3 [-88.6, -88.1] | -77.7 [-78.1, -77.1] | -90.2 [-90.4,
-90.1] |
| proto | run1 | IN@parF_w0 -> IN@parF | workers launched vs Gather with 0
workers | +34.9 [+32.3, +36.8] | +1.1 [-0.3, +3.8] | +46.5 [+44.6, +49.0] |
| proto | run1Q | IN@ser -> IN@ser_proto | restriction, serial (seq-scan case)
| -84.5 [-85.0, -84.0] | -83.4 [-83.7, -82.8] | -86.3 [-86.5, -85.8] |
| proto | run1Q | VALUESc@nl -> IN@ser_proto | VALUES nested loop vs restricted
IN | +12.6 [+8.6, +17.5] | +104.7 [+96.1, +113.9] | -56.7 [-57.6, -55.0] |
| proto | run1Q | SPLIT@ser -> IN@ser_proto | UNION ALL upper bound vs in-place
restriction | -90.5 [-90.8, -90.1] | -92.7 [-92.8, -92.5] | -14.8 [-19.4,
-10.7] |
| proto | run1Q | VALUESr@nl -> VALUESc@nl | rows vs count(*) form of the
rewrite | +1.1 [-3.0, +3.3] | -0.8 [-4.7, +4.8] | +0.9 [-2.1, +3.3] |
| proto | run2 | IN@parF_w0 -> IN@parF | workers launched vs Gather with 0
workers | +35.9 [+35.0, +37.1] | +0.4 [-2.5, +2.3] | +46.1 [+44.6, +47.4] |
| r18 | run1 | IN@ser_idx -> IN@par_idx | parallel penalty, same full-array
index plan | +239.3 [+230.6, +241.9] | -35.0 [-35.8, -34.0] | +3101.2 [+3057.6,
+3122.4] |
| r18 | run1 | IN@par_idx -> VALUESc@nl | IN (follow-up shape) vs VALUES nested
loop | -94.7 [-94.8, -94.6] | -87.0 [-87.3, -85.7] | -96.7 [-96.8, -96.5] |
| r18 | run1 | VALUESr@nl -> VALUESc@nl | rows vs count(*) form of the rewrite
| +1.6 [-0.4, +3.7] | +1.2 [-2.8, +11.9] | +3.0 [-0.3, +10.0] |
diff --git a/src/backend/optimizer/util/inherit.c
b/src/backend/optimizer/util/inherit.c
index 6e1d2b14bc4..019d94b9150 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -36,9 +36,116 @@
#include "parser/parse_relation.h"
#include "partitioning/partdesc.h"
#include "partitioning/partprune.h"
+#include "nodes/nodeFuncs.h"
+#include "partitioning/partbounds.h"
+#include "utils/array.h"
+#include "utils/guc.h"
+#include "utils/lsyscache.h"
#include "utils/rel.h"
+/*
+ * MEASUREMENT PROTOTYPE ONLY -- not a proposal.
+ *
+ * When the placeholder GUC "proto.restrict_saop" is "on", reduce a live HASH
+ * partition's "partkey = ANY (const array)" qual to the array elements that
+ * hash to that partition, using the same hash computation that pruning uses
+ * to select the partition. Single-column hash partitioning only; any other
+ * shape is left untouched. It exists to bound what a per-child
+ * simplification could gain, measured end to end.
+ */
+static void
+proto_restrict_child_saop(PlannerInfo *root, RelOptInfo *parent,
+ RelOptInfo *child, int
partidx)
+{
+ const char *opt = GetConfigOption("proto.restrict_saop", true, false);
+ PartitionScheme ps = parent->part_scheme;
+ AppendRelInfo *appinfo;
+ Node *keyexpr;
+ ListCell *lc;
+
+ if (opt == NULL || strcmp(opt, "on") != 0)
+ return;
+ if (ps == NULL || ps->strategy != PARTITION_STRATEGY_HASH ||
+ ps->partnatts != 1 || parent->boundinfo == NULL ||
+ parent->partexprs == NULL || parent->partexprs[0] == NIL)
+ return;
+
+ appinfo = root->append_rel_array[child->relid];
+ keyexpr = adjust_appendrel_attrs(root,
+ (Node
*) linitial(parent->partexprs[0]),
+ 1,
&appinfo);
+
+ foreach(lc, child->baserestrictinfo)
+ {
+ RestrictInfo *rinfo = lfirst_node(RestrictInfo, lc);
+ ScalarArrayOpExpr *saop;
+ ScalarArrayOpExpr *newsaop;
+ Const *arr;
+ ArrayType *at;
+ Oid elemtype;
+ int16 typlen;
+ bool typbyval;
+ char typalign;
+ Datum *elems;
+ bool *nulls;
+ int nelems;
+ int nkeep = 0;
+ Datum *keep;
+
+ if (!IsA(rinfo->clause, ScalarArrayOpExpr))
+ continue;
+ saop = (ScalarArrayOpExpr *) rinfo->clause;
+ if (!saop->useOr || list_length(saop->args) != 2 ||
+ !IsA(lsecond(saop->args), Const) ||
+ !equal(strip_implicit_coercions(linitial(saop->args)),
keyexpr) ||
+ !op_in_opfamily(saop->opno, ps->partopfamily[0]))
+ continue;
+ arr = (Const *) lsecond(saop->args);
+ if (arr->constisnull)
+ continue;
+ at = DatumGetArrayTypeP(arr->constvalue);
+ elemtype = ARR_ELEMTYPE(at);
+ if (elemtype != ps->partopcintype[0])
+ continue;
+ get_typlenbyvalalign(elemtype, &typlen, &typbyval, &typalign);
+ deconstruct_array(at, elemtype, typlen, typbyval, typalign,
+ &elems, &nulls, &nelems);
+ keep = palloc_array(Datum, nelems);
+ for (int k = 0; k < nelems; k++)
+ {
+ bool isnull = false;
+ uint64 h;
+
+ if (nulls[k])
+ continue; /* NULL never satisfies
"=" */
+ h = compute_partition_hash_value(1, ps->partsupfunc,
+
ps->partcollation,
+
&elems[k], &isnull);
+ if (parent->boundinfo->indexes[h %
parent->boundinfo->nindexes] == partidx)
+ keep[nkeep++] = elems[k];
+ }
+ if (nkeep == 0 || nkeep == nelems)
+ continue;
+
+ newsaop = copyObject(saop);
+ newsaop->args = list_make2(linitial(saop->args),
+
makeConst(arr->consttype, arr->consttypmod,
+
arr->constcollid, -1,
+
PointerGetDatum(construct_array(keep, nkeep, elemtype,
+
typlen, typbyval,
+
typalign)),
+
false, false));
+ lfirst(lc) = make_restrictinfo(root, (Expr *) newsaop,
+
rinfo->is_pushed_down,
+
rinfo->has_clone,
+
rinfo->is_clone,
+
rinfo->pseudoconstant,
+
rinfo->security_level,
+
NULL, NULL, NULL);
+ }
+}
+
static void expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo *relinfo,
RangeTblEntry *parentrte,
Index parentRTindex, Relation parentrel,
@@ -404,6 +511,7 @@ expand_partitioned_rtentry(PlannerInfo *root, RelOptInfo
*relinfo,
/* Create the otherrel RelOptInfo too. */
childrelinfo = build_simple_rel(root, childRTindex, relinfo);
relinfo->part_rels[i] = childrelinfo;
+ proto_restrict_child_saop(root, relinfo, childrelinfo, i);
relinfo->all_partrels = bms_add_members(relinfo->all_partrels,
childrelinfo->relids);