Skip to content

Every planned query scans pgcolumnar.storage twice; relation_oid has no index #1210

Description

@OffgridwithJD

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.

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