Skip to content

Default stripe_row_limit exceeds the 32MB fetch cache, making indexed point queries ~186x slower #353

Description

@jdatcmd

Summary

The default stripe_row_limit of 150,000 puts a realistically wide table over the 32 MB fetch cache cap, so every index fetch re-decodes the entire row group. Indexed point queries cost ~33 ms per row at the default and ~0.18 ms per row one step below it: a 186x difference produced by nothing but the shipped default.

Against heap on the same data with the same index and the same plan, this is roughly 1,700x slower.

Reproduction

Self-contained, 300,000 rows, 21 columns (TSBS-cpu shape). Identical data and identical query in all three arms; only stripe_row_limit differs. All three take an Index Scan.

SELECT count(*), max(usage_user) FROM t WHERE hostname='host_1' -- 600 rows.

stripe_row_limit groups plan time per row
150000 (default) 2 Index Scan 19,788 ms 32.98 ms
100000 3 Index Scan 106.5 ms 0.177 ms
50000 6 Index Scan 75.4 ms 0.125 ms

Script: /root/ct3.sh on pgcolumnar-audit (runs in a throwaway database).

Mechanism

COLUMNAR_FETCH_CACHE_MAX_BYTES is 32 MB (src/columnar_reader.c:1778). A fetch by row number decodes the row group and caches it; when the decoded group exceeds that cap the entry cannot be retained, so the next fetch decodes the whole group again. With 150,000 rows and 21 columns the decoded group is comfortably over 32 MB, so the cache never survives a single fetch and every row costs a full group decode.

The threshold behaviour is sharp, and an independent measurement during an unrelated audit put the cliff at 145,001 rows for a narrower fixture -- consistent with a byte cap rather than a row cap.

Why this matters more than it looks

It is the default. stripe_row_limit defaults to 150,000, so a user who creates a columnar table, adds an index, and runs a selective query gets the pathological path with no indication that a configuration choice caused it.

It scales with table width, not row count. The wider the table, the fewer rows fit under 32 MB, so the more likely the default exceeds it. A 21-column time-series table -- the workload this project targets -- is past it.

It is not the closed quadratic-fetch issue. #143 and #157 both addressed fetch-by-row-number cost and are closed. This is the cache being unable to hold a default-sized group at all.

Measured impact on the 100M bench

Same shape at scale, cpu_pgc (100M rows, 12 columns queried, groups averaging 149,925 rows), against cpu_heap with an equivalent (hostname, time DESC) index and an identical plan:

query rows returned pgColumnar heap per row (pgc)
one host, 1 hour 360 10,335 ms 6 ms 28.7 ms
one host, 12 hours 4,320 123,546 ms 8 ms 28.6 ms

The per-row cost is flat across a 12x change in row count, which is the signature of a fixed per-fetch cost rather than anything proportional to the result.

Options, roughly in order of appeal

  1. Cache the decoded group per column rather than whole, so the cap applies to what the fetch actually needs. A point query touching 3 of 21 columns would then fit comfortably.
  2. Raise or make configurable COLUMNAR_FETCH_CACHE_MAX_BYTES. Simple, but trades a silent performance cliff for silent memory growth, and the right value still depends on table width.
  3. Lower the default stripe_row_limit. Cheapest, but it is the wrong lever: it degrades scan and compression to fix a fetch-path problem, and the cliff would still be there for wide-enough tables.
  4. Warn when a table's configuration puts it over the cap. Not a fix, but it converts an invisible cliff into a visible one.

(1) is the only option that removes the cliff rather than moving it.

Note on scope

Found while re-measuring the cross-engine benchmark for #348. It is the dominant term in q1/q2/q3 there, and those queries cannot be honestly published without it. Filing separately because it is a product defect rather than a documentation one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions