num_sa_scans in genericcostestimate

Started by Jeff Janesabout 4 years ago4 messageshackers
Beta feature

Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.

won't retrytests failedCI history

You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:

docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t46455
psql -h localhost -U postgres

Built from patchset v1 (message #1), July 28, 2026 at 02:10 AM.

Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:

git clone --branch t46455_1 https://github.com/hackorum-dev/postgres.git

In a checkout you already have, add the fork once:

git remote add hackorum https://github.com/hackorum-dev/postgres.git

then, for this patchset and every later one:

git fetch hackorum t46455_1 && git checkout t46455_1

Patchset v1 (message #1) is on t46455_1

Jump to latest
#1Jeff Janes
jeff.janes@gmail.com

When costing a btree index scan, num_sa_scans gets computed twice, once in
btcostestmeate and once in genericcostestimate. But the computations are
different. It looks like the generic one includes all =ANY in any column
in the index, while the bt one includes only =ANY which or on columns for
which all the preceding index columns are tested for equality.

It looks like the generic one was there first then the bt-specific one was
added later to improve planning of btree indexes. But then shouldn't the
value be passed down to generic, rather than recomputed (differently)?
I've attached a patch to do that. Generic still computes the value itself
for other-than-btree indexes.

Or, is there a reason we want a different value to be used in
genericcostestimate?

The context for this is that I was looking at cases where btree indexes
were not using all the columns they could, but rather shoving some of the
conditions down into a Filter unnecessarily/unhelpfully. This change
doesn't fix that, but it does seem to be moving in the right direction.

This does cause a regression test failure due to an (apparently?)
uninteresting plan change.

Cheers,

Jeff

Attachments:

t46455_1
eq_any.patchapplication/octet-stream; name=eq_any.patchDownload+16-11
#2Jeff Janes
jeff.janes@gmail.com
In reply to: Jeff Janes (#1)
Re: num_sa_scans in genericcostestimate

On Sun, Aug 21, 2022 at 2:45 PM Jeff Janes <jeff.janes@gmail.com> wrote:

...

The context for this is that I was looking at cases where btree indexes
were not using all the columns they could, but rather shoving some of the
conditions down into a Filter unnecessarily/unhelpfully. This change
doesn't fix that, but it does seem to be moving in the right direction.

Added to commitfest.

This does cause a regression test failure due to an (apparently?)
uninteresting plan change.

Looking more at the regression test plan change, it points up an
interesting question which is only tangentially related to this patch.

With patch applied:

[local] 417536 regression=# explain analyze SELECT thousand, tenthous FROM
tenk1
WHERE thousand < 2 AND tenthous IN (1001,3000)
ORDER BY thousand;
QUERY PLAN

---------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=4.55..4.56 rows=1 width=8) (actual time=0.100..0.101 rows=2
loops=1)
Sort Key: thousand
Sort Method: quicksort Memory: 25kB
-> Index Only Scan using tenk1_thous_tenthous on tenk1
(cost=0.29..4.50 rows=1 width=8) (actual time=0.044..0.048 rows=2 loops=1)
Index Cond: ((thousand < 2) AND (tenthous = ANY
('{1001,3000}'::integer[])))
Heap Fetches: 0
Planning Time: 1.040 ms
Execution Time: 0.149 ms
(8 rows)

[local] 417536 regression=# set enable_sort TO off ;

[local] 417536 regression=# explain analyze SELECT thousand, tenthous FROM
tenk1
WHERE thousand < 2 AND tenthous IN (1001,3000)
ORDER BY thousand;
QUERY PLAN

---------------------------------------------------------------------------------------------------------------------------------
Index Only Scan using tenk1_thous_tenthous on tenk1 (cost=0.29..4.71
rows=1 width=8) (actual time=0.021..0.024 rows=2 loops=1)
Index Cond: (thousand < 2)
Filter: (tenthous = ANY ('{1001,3000}'::integer[]))
Rows Removed by Filter: 18
Heap Fetches: 0
Planning Time: 0.156 ms
Execution Time: 0.039 ms
(7 rows)

Why does having the =ANY in the "Index Cond:" rather than the "Filter:"
inhibit it from understanding that the rows will still be delivered in
order by "thousand"?

Cheers,

Jeff

Show quoted text
#3Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Janes (#1)
Re: num_sa_scans in genericcostestimate

Jeff Janes <jeff.janes@gmail.com> writes:

When costing a btree index scan, num_sa_scans gets computed twice, once in
btcostestmeate and once in genericcostestimate. But the computations are
different. It looks like the generic one includes all =ANY in any column
in the index, while the bt one includes only =ANY which or on columns for
which all the preceding index columns are tested for equality.

I think this is correct. As per the comments in btcostestimate:

* For a btree scan, only leading '=' quals plus inequality quals for the
* immediately next attribute contribute to index selectivity (these are
* the "boundary quals" that determine the starting and stopping points of
* the index scan). Additional quals can suppress visits to the heap, so
* it's OK to count them in indexSelectivity, but they should not count
* for estimating numIndexTuples. So we must examine the given indexquals
* to find out which ones count as boundary quals. ...

and further down

/* count number of SA scans induced by indexBoundQuals only */
if (alength > 1)
num_sa_scans *= alength;

This num_sa_scans value computed by btcostestimate is (or should be)
only used in calculations related to numIndexTuples, whereas the one
in genericcostestimate should be used for calculations related to the
overall number of heap tuples returned by the indexscan. Maybe there
is someplace that is using the wrong one, but it's not a bug that they
are different.

The context for this is that I was looking at cases where btree indexes
were not using all the columns they could, but rather shoving some of the
conditions down into a Filter unnecessarily/unhelpfully. This change
doesn't fix that, but it does seem to be moving in the right direction.

If it helps, it's only accidental, because this patch is surely wrong.
We *should* be distinguishing these numbers.

regards, tom lane

#4Tom Lane
tgl@sss.pgh.pa.us
In reply to: Jeff Janes (#2)
Re: num_sa_scans in genericcostestimate

Jeff Janes <jeff.janes@gmail.com> writes:

Why does having the =ANY in the "Index Cond:" rather than the "Filter:"
inhibit it from understanding that the rows will still be delivered in
order by "thousand"?

They won't be. The =ANY in index conditions results in multiple
index scans, that is we effectively do a scan with

Index Cond: (thousand < 2) AND (tenthous = 1001)

and then another with

Index Cond: (thousand < 2) AND (tenthous = 3000)

and only by very good luck would the overall result be sorted by
"thousand". On the other hand, if the ScalarArrayOp is a plain
filter condition, then it doesn't affect the number of index
scans --- it's just a (rather expensive) filter condition.

indxpath.c's get_index_paths() is explicitly aware of these
considerations, maybe reading the comments there would help.

I don't say there couldn't be a bug here, but you haven't
demonstrated one. I believe that get_index_paths() will
generate paths both ways, with the ScalarArrayOp as a filter
condition and an indexqual, and it's evidently deciding the
first way is cheaper.

regards, tom lane