Skip to content

Index probes cost more than scanning a small catalog: the real cost under #1198 and #1213 #1217

Description

@OffgridwithJD

SUPERSEDED — the closing section of this issue is wrong. A size check
(RelationGetNumberOfBlocks) beats both main and #1213 at every size either of
us has measured, and returns the empty-catalog configuration to its exact
pre-#1198 number. This is a bug with a fix, not a judgement about the user
population. See the comments; the remaining work is choosing the threshold by
measurement and fixing #1213's arms, which assert the access path rather than
the work.

Retitled and rewritten. The first version of this issue named
pgcolumnar_index_oid() as the cost. That was wrong — see "The diagnosis
that failed" below. The measurements were right; the cause was not.

What

Two merged/open changes replace a sequential scan of a small catalog with an
index probe. On a small relation the probe costs more than the scan, so
each change is a regression below a crossover point in catalog size.

This is the ordinary index-versus-seqscan crossover on relation size. It is
inherent to the change, not overhead that can be engineered away.

The diagnosis that failed, and why it was convincing

@jdatcmd first attributed the cost to pgcolumnar_index_oid() —
get_namespace_oid + get_relname_relid + table_open per call. It fit: the
call is visible in the diff, it is per-plan and per-group in the right
proportions, and #445's comment in the same file already profiled exactly that
cost
and solved it for the write path with a caching session.

They built the cache to measure it. It removes nothing, identical to the
buffer on every reading:

compact(), other tables       0      50     200    1000
  main                       523     563     775    1845
  #1213                      663     663     674     716
  #1213 + cache              663     663     674     716

planning
  before #1198               305
  #1198                      342
  #1198 + cache              342

Two prototypes had produced the same constant, and that was read as
corroboration.
Both replaced a seq scan with an index probe and both called
pgcolumnar_index_oid. Two shared terms; the one visible in the diff got named.
Agreement between two instruments says they share something, not what.

The #445 paragraph made it worse by being true — a correct profile of a real cost
on a different path, which read as the codebase confirming the guess. A good
number answering the neighbouring question is more expensive than a bad number.

The measurements, which still stand

#1198, planning shared hit + read, second EXPLAIN in a fresh backend,
8288510d vs 8ea98fc8, probe table created last, PG 18:

columnar tables options pages before after delta
10 1 12 17 +5
100 1 12 17 +5
500 3 17 21 +4
1000 6 20 21 +1
2000 11 25 21 −4
3000 17 31 21 −10

Crossover ~1200 tables. after is flat at 21 from 500 up; before tracks
the page count.

#1213, EXPLAIN (ANALYZE, BUFFERS) over compact() (@jdatcmd), 20 groups
with 10 retired:

other tables row_group pages main branch delta
0 1 523 663 +140
50 1 563 663 +100
200 3 775 674 −101
1000 12 1845 716 −1129

Crossover ~120 other tables.

Different units, different fixtures. They share a sign and a mechanism; neither
is "the cost of the change".

The extreme case: catalogs with nothing in them

pgcolumnar.options gets a row only when set_options is called;
pgcolumnar.projection only when a projection is added. An installation doing
neither has both empty — and this is the default.

Measured, 200 columnar tables, no set_options, no projections:

options      rows=0  heap pages=0  index pages=1
projection   rows=0  heap pages=0  index pages=1

one index probe on the empty relation   2 shared hits
a sequential scan of a 0-page heap      0 shared hits

and end to end at 1000 tables: before 19, after 25, a flat +6.

At zero rows the scan being replaced is literally free, so the index cannot
win at any size.
This is the same inherent mechanism, at its limit — not a
separate defect.

What has to be decided

The remedy is not engineering; the fixed cost is not removable. For each change
the options are: keep it (a win above the crossover), revert it, or skip the
probe when the catalog is known empty — and the last is a real change nobody has
measured.

Because the cause is the same, this is one judgement for both PRs, and it
turns entirely on a question about users: how many installations have more than
~1200 columnar tables and call set_options, against how many are the default
configuration where the change is a straight loss at every size. That is the
owner's call, not a code question.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions