Skip to content

A columnar text projection costs 3.4x the heap with the per-value copy excluded, and the remainder is in the decode path #768

Description

@jdatcmd

Found while separating width from column type for #766. It is not a costing defect: the
planner charges a text projection less than it charges the equivalent heap scan, and the
columnar scan then takes four and a half times longer.

Measured

4,000,000 rows, 14 columns, 2,000,197 rows returned, PG 17, min of 7 reps, serial plans on
both sides. The heap twin holds the same rows. The four text columns are
md5(i::text)-derived, 32 bytes each.

projection columnar cost columnar ms heap ms columnar ÷ heap
sel (one int4) 61,065 173.7 170.3 1.02x
n1 (numeric) 74,261 269.2 226.8 1.19x
n2 (float8) 73,196 351.5 232.0 1.52x
t1 (one text) 79,853 353.1 212.2 1.66x
t1, t2, t3, t4 136,218 1082.0 235.3 4.60x
eight int4 columns 138,522 271.2 220.6 1.23x
* 239,001 1297.6 186.2 6.97x

Read the two rows priced almost identically:

  • eight int4 columns, cost 138,522, 271.2 ms
  • four text columns, cost 136,218, 1082.0 ms

Nearly the same planner cost. Four times the real time. And against the heap, four text
columns cost the planner less than the heap's 158,109 while taking 4.6 times as long.

* is explained entirely by this: text accounts for 1082 ms of its 1298 ms, and all eight int
columns account for 271 ms. The earlier reading of * as a width result was wrong; it is a
type result.

Why this is its own issue

It is an execution finding, not a planner one. The costing makes it worse — a plan that
projects text is priced as if text were cheap, so it is chosen over alternatives that would be
faster — but correcting the cost model would only stop the planner choosing it. The 1082 ms
would still be 1082 ms.

It is also independent of #753 and #766. It needs no parallelism, and it reproduces on a
single serial scan of one column.

What is not yet established

  • Which part of the text path costs it. Candidates, in the order I would test: the block
    codec (these are md5 strings, so compression is poor and the codec may be doing full work
    for little gain); FSST decode; per-value palloc and varlena construction in the output slot,
    which is where a columnar reader does work a heap reader does not (the heap hands back a
    pointer into the page, this reader must materialise each datum).
  • Whether it is md5-specific. These strings are high entropy and 32 bytes. Low-cardinality
    text would take the dictionary path, and long text the TOAST path, and neither is measured
    here. The fixture was built to defeat compression, so it is the worst case for the codec
    and should not be reported as typical until a low-cardinality and a long-text cell exist
    beside it.
  • Whether the heap comparison is fair on width. The heap stores the same 32-byte strings
    inline, so no TOAST fetch is involved on either side; that much is comparable. But the heap
    arm returns a pointer and the columnar arm materialises, and that difference is the thing
    being measured rather than a flaw in the comparison.

What would make this closable

A columnar text projection within a small factor of the heap's, or a measurement showing the
gap is inherent to materialising varlena datums from a decoded chunk and cannot be closed,
with the cost model then corrected to charge what it actually costs so the planner stops
preferring it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions