What
pgcolumnar_written_stripe_row_limit() looks up pgcolumnar.storage by
relation_oid. No index on that column exists, so the lookup is a heap scan
that reads every page up to the matching row.
It is on the planner path. Three callers, all in src/columnar_customscan.c:
2064 pgcolumnar_group_count_estimate
2439 (cost path)
3075 PgColumnarSetRelPathlist
So every planned query over a columnar table pays it, twice.
Measured
Worst case is the most recently created table, whose storage row sits on the
last page — which is also the table most likely to be actively queried. PG 18,
built from f7b999d, autovacuum = off, VACUUM pgcolumnar.storage before each
reading:
| columnar tables |
storage rows |
storage pages |
newest row's ctid |
seq_scan |
heap_blks_hit |
| 500 |
501 |
4 |
(3,93) |
2 |
8 |
| 1000 |
1001 |
8 |
(7,50) |
2 |
16 |
| 2000 |
2001 |
15 |
(14,99) |
2 |
30 |
| 3000 |
3001 |
23 |
(22,12) |
2 |
46 |
heap_blks_hit is exactly 2 x pages at all four sizes. Two scans per SELECT,
each reading the whole catalog.
The control: position, not size
Same catalog, same query shape, two tables differing only in creation order:
| arm |
storage row ctid |
seq_scan |
heap_blks_hit |
| created first |
(0,1) |
2 |
4 |
| created last |
(22,9) |
2 |
46 |
The scan stops at the match, so the cost is the position of the row, not the
size of the catalog. An installation that queries its oldest table pays nothing
and one that queries its newest pays for the lot.
Why this is not in #1207
#1207 defines its population as "the scan key is a prefix of an index that
already exists". That framing excludes, by construction, every hot scan with
no index at all — which is the more expensive case, not the lesser one. This
site is excluded from those 21 for exactly the reason it is worth its own issue.
#1198's comment documents it accurately: "storage_pkey is on storage_id and this
looks up by relation_oid ... this one cannot." That is true, and "cannot use an
existing index" is a different claim from "cannot be fixed".
Suggested remedy
An index on pgcolumnar.storage (relation_oid), then pass it to
systable_beginscan at this site as #1198 does elsewhere. That is a catalog
change rather than a one-line scan change, so it needs to land in both
pgcolumnar--1.0-alpha5.sql and pgcolumnar--1.0-alpha4--1.0-alpha5.sql, and
native_upgrade_converge.sh should keep fresh and upgraded identical.
Worth checking before building it: whether relation_oid is unique in storage.
If it is, the index can be UNIQUE and also documents an invariant the schema does
not currently state.
Two instruments that could not see this, recorded so nobody repeats them
seq_tup_read is blind here. It counts tuples returned, not tuples
examined, so it reads a flat 2 whatever the catalog size. My first sweep
produced seq_tup_read = 2 at 11, 21, 41 and 81 rows and looked like a clean
negative result.
CREATE TABLE ... USING pgcolumnar alone adds no storage row. The row
appears on first write. A fixture that creates 2000 tables without inserting
measures a one-row catalog 2000 times; mine did, and only a printed premise
check (before=1 after=1) caught it.
What
pgcolumnar_written_stripe_row_limit()looks uppgcolumnar.storagebyrelation_oid. No index on that column exists, so the lookup is a heap scanthat reads every page up to the matching row.
It is on the planner path. Three callers, all in
src/columnar_customscan.c:So every planned query over a columnar table pays it, twice.
Measured
Worst case is the most recently created table, whose storage row sits on the
last page — which is also the table most likely to be actively queried. PG 18,
built from
f7b999d,autovacuum = off,VACUUM pgcolumnar.storagebefore eachreading:
heap_blks_hitis exactly2 x pagesat all four sizes. Two scans per SELECT,each reading the whole catalog.
The control: position, not size
Same catalog, same query shape, two tables differing only in creation order:
The scan stops at the match, so the cost is the position of the row, not the
size of the catalog. An installation that queries its oldest table pays nothing
and one that queries its newest pays for the lot.
Why this is not in #1207
#1207 defines its population as "the scan key is a prefix of an index that
already exists". That framing excludes, by construction, every hot scan with
no index at all — which is the more expensive case, not the lesser one. This
site is excluded from those 21 for exactly the reason it is worth its own issue.
#1198's comment documents it accurately: "storage_pkey is on storage_id and this
looks up by relation_oid ... this one cannot." That is true, and "cannot use an
existing index" is a different claim from "cannot be fixed".
Suggested remedy
An index on
pgcolumnar.storage (relation_oid), then pass it tosystable_beginscanat this site as #1198 does elsewhere. That is a catalogchange rather than a one-line scan change, so it needs to land in both
pgcolumnar--1.0-alpha5.sqlandpgcolumnar--1.0-alpha4--1.0-alpha5.sql, andnative_upgrade_converge.shshould keep fresh and upgraded identical.Worth checking before building it: whether
relation_oidis unique instorage.If it is, the index can be UNIQUE and also documents an invariant the schema does
not currently state.
Two instruments that could not see this, recorded so nobody repeats them
seq_tup_readis blind here. It counts tuples returned, not tuplesexamined, so it reads a flat
2whatever the catalog size. My first sweepproduced
seq_tup_read = 2at 11, 21, 41 and 81 rows and looked like a cleannegative result.
CREATE TABLE ... USING pgcolumnaralone adds no storage row. The rowappears on first write. A fixture that creates 2000 tables without inserting
measures a one-row catalog 2000 times; mine did, and only a printed premise
check (
before=1 after=1) caught it.