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.
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_openper call. It fit: thecall 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:
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,8288510dvs8ea98fc8, probe table created last, PG 18:optionspagesCrossover ~1200 tables.
afteris flat at 21 from 500 up;beforetracksthe page count.
#1213,
EXPLAIN (ANALYZE, BUFFERS)overcompact()(@jdatcmd), 20 groupswith 10 retired:
row_grouppagesCrossover ~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.optionsgets a row only whenset_optionsis called;pgcolumnar.projectiononly when a projection is added. An installation doingneither has both empty — and this is the default.
Measured, 200 columnar tables, no
set_options, no projections: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 defaultconfiguration where the change is a straight loss at every size. That is the
owner's call, not a code question.