Re-scope #155 after encode_effort, with measurements - #213
Conversation
The plan's order was corrected once after #160. Shipping encode_effort (#202) has since half-answered step 1, so the numbers no longer describe the tree. Re-measured at 6,000,000 rows on PG17.10 non-assert, as ratios against a heap insert of the same rows so common-mode load on the box cancels. encode_effort = fast takes high-entropy text from 6.48x to 1.94x, near parity with heap. It recovers a fifth of low-cardinality text (3.83x to 3.01x) and nothing at all on the 5-column low-cardinality row. Since fast skips the FSST search, the remaining low-cardinality cost is by elimination not FSST, and nothing has attributed it yet. That is now the first thing to measure. Separately, the per-column numeric cost is additive and independent of text: one bigint column beats heap at 0.67x, four integer columns cost 2.52x, while heap stays nearly flat across the same range. Also records what column-at-a-time transfer can and cannot buy, since it applies only to the importers: INSERT ... SELECT has no columnar source, so its ceiling is the slot round trip rather than the encoding work underneath. The headline is shape-dependent, from 0.67x to 8.83x. "4.9x slower than heap" was one point on that curve. Design only; no code change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uKWWwBDt5TWWS5DR2tzDb
ChronicallyJD
left a comment
There was a problem hiding this comment.
The re-scope is right, and the methodology change is the part I most want to endorse: measuring in ratios rather than millisecond counts is the correct answer to exactly what burned my curve on this issue. A ratio cancels common-mode load on the box, so it survives a runaway core the way a raw millisecond count does not — which, given #212, is not hypothetical. This document is now robust to the failure mode that produced my retraction.
I checked the one load-bearing inference rather than nod at it. The argument that the residual 3.01x on low-cardinality text is not FSST rests on encode_effort = fast changing only the FSST search. It does: encodeEffort is read in exactly one place in the encoder, columnar_write_state.c:860, guarding the FSST substring search and nothing else. So the full→fast delta is purely FSST, and by elimination the remaining 3.01x is the dictionary path / value stream / zone maps / compression, exactly as written. The inference is exact, not approximate.
The redirection is the valuable part. "The knob solved the case it was built for and only that case" is the honest reading of the 6.48x→1.94x vs 3.83x→3.01x split, and low-cardinality text (names, regions, statuses) is the more common shape, so a fifth recovered is the smaller half of the problem. Calling for an ablation, not a design, before touching it is the right sequence.
Two things I can corroborate from my own #155 measurements, for whatever a second data point is worth: the narrow/low-entropy end beating heap matches what I saw (a constant-8-char column came in under heap, same shape as your 0.67x bigint), and high-entropy text being where the cost concentrated matches too. The magnitudes differ because my fixtures and pre-#202 tree differ, which is the point — the curve is shape-dependent and "4.9x" was one sample of it, as you now say.
One small note, no change required: the per-column numeric tax reads as isolated (four integer columns, no encoder search) and additive, which makes it the cleanest of the three to attribute — it has no entropy or dictionary confound at all. If step 1's ablation is where the design energy goes, step 2 may be the one that actually moves the median wide-table load, since it applies regardless of type. Worth keeping its priority under review once the text ablation lands.
Approving. And since the step-1 ablation is now the gate on both this issue and the #212 caching decision you're holding, I'll take it if you want it — separate the dictionary build, value stream, zone-map write and block compression on the low-cardinality text shape at fast, on a clean box, in ratios. Say the word.
|
Yes, please take it. Merged at Thank you for checking the ScopeStep 1 as you described it: on the low-cardinality text shape at Two additions worth folding in, both cheap once the harness exists:
Not in scope, deliberatelyThe per-column tax stays a separate measurement. You are right that it may move What it gatesBoth #155's next design step and the caching half of #212, which is deliberately BoxIt is free now. The full matrix finished green at PG17.10 non-assert at 6,000,000 rows keeps it comparable with the figures in the |
) Alpha gate 2 of 4. Audits docs/limitations.md against the current tree, corrects two sections that documented behaviour that has since been fixed, and adds the two missing pieces the pre-alpha review note named. Corrections (documenting a defect is not fixing it, and the reverse holds too -- a fixed defect must not stay listed as a limitation): - Constraints on the import path: removed the claim that a deferrable unique constraint is checked per row rather than deferred to commit. That was #168, fixed by the executor index-maintenance path (#180/#182); import_deferred.sh now asserts import_arrow and import_parquet defer correctly. - Planner statistics: removed the warning that ANALYZE can take a long time on wide tables, the SET STATISTICS 0 workaround, and the point-lookup plan regression. That was #171, both halves fixed (#173 planner, #175 sampler). Additions: - Release status: states what 1.0-dev means, what the extension is appropriate for today, and what hardening is gated before a first alpha (#214, #216, #217). Linked from README.md and docs/index.md. - Bulk load and import throughput: states the range by shape from #213 (0.67x to 8.83x, not a single multiplier), names encode_effort as the knob, and records that import has no overhead beyond the write path. The per-column numeric insert cost is deliberately held until its ablation lands, per the issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Design only, no code. Scoping work for #155 as it stands after #202.
Why re-scope
This plan's order was already corrected once, after #160, and correctly: the per-row round trip is not the cost, and one integer column beats heap. Since then
encode_effort(#202) shipped and half-answered step 1, so the figures in the document no longer describe the tree.Re-measured on PG17.10 non-assert, 6,000,000 rows, reported as ratios against a heap insert of the same rows so common-mode load on the box cancels rather than being argued about.
What the knob actually bought, per shape
'name-'||(g%1000))The knob solved the case it was built for, and only that case. High-entropy text at 1.94x is near parity for a format writing a fraction of the bytes — step 1 delivered where the FSST search was the cost.
It does not touch low-cardinality text, which is the more common shape — names, regions, categories, statuses.
fastskips the FSST search, so by elimination the remaining 3.01x is not FSST: it is dictionary, value stream, zone maps, compression. Nothing has attributed that yet, and the 5-column low-cardinality row is 5.59x with the knob doing nothing at all.The other axis: a per-column tax, independent of text
Heap is nearly flat (2501 → 2674) because row overhead dominates and narrow columns are almost free to add. Columnar pays ~1–2 s per extra column per 6M rows. That is the tax on wide tables and it has nothing to do with encoding search.
One thing worth stating before anyone budgets for it
Column-at-a-time transfer (step 2) applies only to the importers, where a column store is genuinely on both ends.
INSERT ... SELECThas no columnar source — rows arrive from a heap scan — so the per-row shape is not removable there. Since import already measures the same as an ordinary insert, step 2's ceiling is the slot round trip alone, not the encoding work underneath it.Revised order
effort = fast— separate dictionary build, value stream, zone maps, compression. 3.01x with the shipped knob already applied is the number to explain. No design until it is attributed.Method notes
md5()for the high-entropy column and nearly reported a 1.8x regression against this document's 12,989 ms — until I checked and found the plan's ownc1is'name-'||(g%1000), a low-cardinality string. Different fixture, not a regression. The high-entropy column is now sha256 (6,000,000 distinct, verified, 76 chars); string length is itself an FSST variable, so the two shapes are reported separately rather than blended.Not marking this as closing #155: it re-scopes it and hands it two measurable next steps.