| From: | Roman Eskin <r(dot)eskin(at)arenadata(dot)io> |
|---|---|
| To: | pgsql-hackers(at)lists(dot)postgresql(dot)org |
| Subject: | Sparse attribute fetch hook on TupleTableSlotOps (for column-store slot types) |
| Date: | 2026-08-12 00:09:13 |
| Message-ID: | ead5087a-f7d5-4336-9f23-9146a0edf796@arenadata.io |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-hackers |
Hi hackers,
I'd like early feedback on a small, self-contained addition to the
TupleTableSlotOps vtable before investing further time in it. Patch
attached (against master @ 086f6f17601).
Problem statement:
------------------
TupleTableSlot's deforming convention is "everything up to the highest
referenced attribute is valid" (tts_nvalid as a dense prefix,
slot_getsomeattrs(slot, natts)). That's the right default for row
stores, but it's a poor fit for any slot type backed by
column-oriented storage, for ex, a simple demonstration query:
```
SELECT col_1, col_2, ..., col_50 FROM wide_table WHERE col_50 = 0;
```
actually needs only col_50 to test at every row (assuming indexes are
not used for this particular example, so seq scan is working), but
current approach forces the slot to also touch attributes 1..49 (which
means for column-oriented storage opening and reading files for each
column), even though the underlying storage could fetch column 50 on
its own with no dependency on the others, and fetch the others only
when the predicate is true.
This is not a new observation - Andres flagged the same convention as
a bottleneck for column-oriented storage back in 2016 ("Rethinking
TupleTableSlot deforming"), and it came up again when
TupleTableSlotOps itself was introduced in 2018, and again in Heikki's
Zedstore work, and again in Soumyadeep Chakraborty's 2020-2021 TableAM
column-projection proposal. Full list of prior threads below - I don't
think any of them shipped a general mechanism, so I'm not assuming
this is uncontroversial, just that the problem is a known one.
What the patch does:
--------------------
Two new optional callbacks on TupleTableSlotOps:
bool (*gettargetattr) (TupleTableSlot *slot, Bitmapset *attrs);
bool (*is_attr_valid) (TupleTableSlot *slot, int attnum);
gettargetattr() asks the slot to fetch exactly the (possibly
non-contiguous) attribute set given, instead of a dense prefix. It
returns false if the slot type declines to service this particular
call (e.g. the slot isn't yet bound to live storage), in which case
the caller falls back to the existing slot_getsomeattrs() path.
Therefore, gettargetattr is an optional optimization, and should not
break existing flows if not used. is_attr_valid() lets code that only
knows tts_nvalid as "the valid prefix length" also ask about a
specific attribute that a sparse fetch may have already populated
outside that prefix.
Both are NULL for every existing slot type (TTSOpsVirtual,
TTSOpsHeapTuple, TTSOpsMinimalTuple, TTSOpsBufferHeapTuple). That's
deliberate: this patch changes no observable behavior for any slot
type in core. slot_gettargetattr() returns false when the callback is
NULL, and slot_is_attr_valid() degenerates to exactly the existing
tts_nvalid > attnum check when is_attr_valid is NULL.
On the wiring side: ExprSetupInfo (execExpr.c) gains an all_scan_attrs
Bitmapset, populated during expr_setup_walker() from every plain
scan-level Var (same walk that already computes last_scan, just also
recording each individual attnum rather than only the maximum). It's
passed through to EEOP_SCAN_FETCHSOME via a new ExprEvalStep.d.fetch
.all_vars field. EEOP_SCAN_FETCHSOME tries slot_gettargetattr() first
and falls back to slot_getsomeattrs() exactly as before if it returns
false. EEOP_SCAN_VAR/EEOP_ASSIGN_SCAN_VAR's validity Assert()s switch
from a raw "attnum < tts_nvalid" to slot_is_attr_valid(), since a
sparse fetch can validate an attribute outside the dense prefix.
Note on the Bitmapset convention: all_scan_attrs is 0-based (member =
attnum - 1, matching tts_values[]/tts_isnull[] array indexing) rather
than offset by FirstLowInvalidHeapAttributeNumber the way
attribute-number Bitmapsets elsewhere in the tree usually are (e.g.
pull_varattnos()). That's safe here because FETCHSOME steps never
carry negative/system attribute numbers - those go through the
separate *_SYSVAR opcodes - but I'm flagging the inconsistency up
front rather than leaving it for a reviewer to notice.
What's deliberately left out of this round:
-------------------------------------------
To keep this first round reviewable, three related pieces are *not* in
this patch, on purpose:
1. JIT. The interpreter's EEOP_SCAN_FETCHSOME picks up the new
fallback; the JIT-compiled equivalent does not, so a JIT-compiled
plan will silently keep using the dense path for
this opcode even after this patch.
2. Two single-Var interpreter fast paths, ExecJustScanVar() and
ExecJustAssignScanVar(), which bypass the general opcode
interpreter entirely (and so bypass gettargetattr()) whenever an
expression collapses to exactly one bare column reference. This
may be a real gap - it means "SELECT single_col FROM wide_table"
doesn't benefit from this mechanism as it stands - but our
initial performance testing of a fix for it didn't show a benefit
(as our underlying column-storage can handle such cases on its
own), so it's held back pending better data rather than included
speculatively.
3. The equivalent change for
EEOP_INNER_FETCHSOME/EEOP_OUTER_FETCHSOME and the inner/outer
VAR/ASSIGN_VAR opcodes. Same reasoning as (2): the measured
benefit wasn't clean enough yet to bring here.
Happy to bring (2) and (3) back as follow-ups if the core shape here
is acceptable and once we have a cleaner performance case for them.
Motivation:
-----------
This is infrastructure with no consumer in core - I want to be upfront
about that rather than have it surface as a "why would we want this"
question partway through review. The motivating use case is any
external columnar table access method, where the win is avoiding I/O
for unreferenced columns entirely, not just avoiding in-memory deform
work for them.
We've tested the change on the Greengage DB, and together with the
related changes for the specific tuple table slot for column-oriented
storage, the benefit could be very good (especially for queries with
low selectivity that have filtering on a small subset of projected
columns, allowing to throw away most part of tuples without full
reading them from the file system).
Similar existing threads:
-------------------------
- "Rethinking TupleTableSlot deforming" (Andres Freund, 2016) -
https://www.postgresql.org/message-id/flat/20160722015605.hpthk7axm6sx2mur%40alap3.anarazel.de
- "TupleTableSlot abstraction" (Andres Freund, 2018) -
https://www.postgresql.org/message-id/flat/20180220224318.gw4oe5jadhpmcdnm%40alap3.anarazel.de
- Zedstore (Heikki Linnakangas et al., 2019) -
https://www.postgresql.org/message-id/flat/CALfoeiuF-m5jg51mJUPm5GN8u396o5sA2AF5N97vTRAEDYac7w%40mail.gmail.com
- "Table AM modifications to accept column projection lists"
(Soumyadeep Chakraborty, 2020-2021) -
https://www.postgresql.org/message-id/flat/CAE-ML%2B9RmTNzKCNTZPQf8O3b-UjHWGFbSoXpQa3Wvuc8YBbEQw%40mail.gmail.com
- "More speedups for tuple deformation" (David Rowley, Jan-Apr 2026) -
the part committed in PG19 speeds up the existing dense deform
loop (still walks every attribute, just faster); a subset-column
variant was floated in the same thread but never implemented,
which is closer to this patch, but at the heap/offset layer rather
than the slot vtable layer.
https://www.postgresql.org/message-id/flat/CAEG8a3KeKcZxJsH9nL%2BD1JzC4Ekx51ps7-1ZGWkwdXbPS5jTXw%40mail.gmail.com#63ab266a1f1ffff3463abdc071993ca8
Thanks for reading this far. Patch attached.
Looking forward to receiving feedback.
Best regards,
Roman Eskin
| Attachment | Content-Type | Size |
|---|---|---|
| sparse_attribute_fetch_v001.patch | text/plain | 10.8 KB |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | Alexander Korotkov | 2026-08-12 00:10:09 | Re: Vacuum statistics |
| Previous Message | Michael Paquier | 2026-08-11 23:59:53 | Re: Fetch digests explicitly for cryptohash with OpenSSL 3.0 and later |