Skip to content

FSST keep/drop is re-decided for every row group, though the answer rarely changes: cache the verdict with an age bound #472

Description

@ChronicallyJD

Recovered from an unvalidated stash before dropping it, so the idea and its measurement are not lost. Nothing here is gated, and the numbers below pre-date two mitigations that have since landed. Treat it as a lead to re-measure, not a finding.

The shape

pgcolumnar_flush_row_group decides FSST keep/drop once per row group:

  • PgColumnarFsstBuildChunkTable (src/columnar_write_state.c:1094) builds the symbol table
  • PgColumnarFsstHelpsCompressed (src/columnar_write_state.c:1115) decides whether to keep it

The decision cannot use a sample. The comment at the call site records why: on the sampled prefix FSST can look 12% worse while over the whole column it is 23% better, an inversion no margin on a sample would make safe. So asking the question costs a full FSST encode of the entire corpus, per row group.

For a column whose data does not change character, that recomputes the same answer for every row group of the load.

The measurement, as recorded at the time

Profiled on a 20,000,000-row numeric load, against a tree based on 0b6f24c (2026-07-29):

symbol share of backend
encode_fsst_shared 34.8%
ColumnarFsstBuildChunkTable 17.7%

The verdict was "FSST does not help" every time, and the stored bytes were byte-identical either way. More than half the backend went to re-deriving a constant.

Re-measure before acting. That profile pre-dates both of these:

  • PgColumnarFsstDictWins (Bulk load is 4.9x slower than heap, and import inherits it #155), a cheap distinct probe that skips the build when the dictionary wins outright. It does not touch the repeated whole-corpus decision, and it does not help the case here, where the dictionary does not win but FSST still loses consistently.
  • pgcolumnar_fsst_min_gain_percent, which changed the threshold but not how often it is evaluated.

Whatever remains after those two is the real size of this.

The proposal

Cache the verdict on PgColumnarColumnDef and reuse it for a bounded number of row groups:

#define COLUMNAR_FSST_UNKNOWN   0
#define COLUMNAR_FSST_HELPS     1
#define COLUMNAR_FSST_HURTS     2
#define COLUMNAR_FSST_REUSE     16

int8    fsstVerdict;      /* COLUMNAR_FSST_* */
int     fsstVerdictAge;   /* row groups since the verdict was taken */

A HURTS verdict skips the build and the decision together. A HELPS verdict keeps the table without paying the whole-corpus encode again. The age bound is what keeps it honest: a column whose data changes character mid-load is still noticed, within COLUMNAR_FSST_REUSE row groups.

What has to be proven before this can land

The saving is real only if the stored bytes do not change. That is the whole risk: a stale verdict silently degrades compression, and nothing in the current suites would notice.

  • A differential arm asserting byte-identical storage with the cache on and off, on a column where the verdict is stable.
  • A fixture whose data changes character mid-load, asserting the verdict is re-taken within the bound and the storage matches the uncached result. Without this the age bound is untested and COLUMNAR_FSST_REUSE is a number nobody checked.
  • The load-time win measured in-suite, not from a standalone probe.

Provenance

The stash also carried a COLUMNAR_FSST_MIN_GAIN_SHIFT margin, which is superseded: pgcolumnar_fsst_min_gain_percent does that job configurably. Only the caching is unlanded.

Original diff, 99 insertions across src/columnar_encoding.c and src/columnar_write_state.c, base 0b6f24c. It will not apply cleanly (the surrounding code has moved, and the symbols were renamed to the PgColumnar prefix by #389); it is a description of the approach, not a patch to apply.

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