Skip to content

Commit 41696ab

Browse files
committed
Add backend TOAST architecture documentation and clean up detoast access
- Add src/backend/access/common/README.toast documenting Plain TOAST and Direct TOAST formats, pointer structures (varatt_external vs. varatt_direct), catalog layouts, three-tier storage hierarchy (single chunk, flat multi-chunk, and hierarchical DAG), slicing traversal, and in-place upgrades. - Add explanatory architectural comments to struct varatt_direct in varatt.h, DAG parameters in toast_internals.c, Direct TOAST catalog columns in toasting.c, and transparency notes in fmgr/README. - Clean up redundant tuple descriptor natts check when fetching offsets_datum in toast_fetch_datum_direct_slice_recursive() in detoast.c, and expand the function documentation. - Use an anonymous union for direct_tp and tp in ToastExternalMetadata to eliminate redundant memory and reflect the mutual exclusivity of external pointer formats.
1 parent b280ca0 commit 41696ab

6 files changed

Lines changed: 170 additions & 4 deletions

File tree

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
src/backend/access/common/README.toast
2+
3+
The Oversized-Attribute Storage Technique (TOAST)
4+
=================================================
5+
6+
PostgreSQL stores table rows inside fixed-size pages (typically 8KB). When a
7+
row exceeds the target threshold (typically BLCKSZ / 4, or ~2KB), the storage
8+
engine uses TOAST to compress and/or move variable-length attributes (varlenas)
9+
out-of-line into a separate auxiliary relation, known as a TOAST table
10+
(cataloged as pg_toast_<relfilenumber>).
11+
12+
PostgreSQL supports two on-disk external TOAST formats:
13+
1. Plain TOAST (Traditional format, VARTAG_ONDISK = 18)
14+
2. Direct TOAST (Tree/TID direct format, VARTAG_DIRECT = 19)
15+
16+
Whether a table uses Plain or Direct TOAST is controlled by the relation
17+
storage parameter 'toast_flavour' (or the GUC 'default_toast_flavour').
18+
19+
20+
1. Plain TOAST Format
21+
---------------------
22+
23+
A Plain TOAST table has three columns:
24+
(chunk_id OID, chunk_seq INT4, chunk_data BYTEA)
25+
26+
Each out-of-line value is assigned a unique 32-bit OID (va_valueid). The
27+
value's data is broken into sequential chunks of up to TOAST_MAX_CHUNK_SIZE
28+
(typically ~2KB). An associated B-Tree index on (chunk_id, chunk_seq)
29+
indexes every chunk.
30+
31+
The pointer stored in the main table's tuple is struct varatt_external (18 bytes):
32+
int32 va_rawsize; /* Original data size (includes 4-byte header) */
33+
uint32 va_extinfo; /* External saved size and 2 compression bits */
34+
Oid va_valueid; /* Unique ID of value within TOAST table */
35+
Oid va_toastrelid; /* RelID of TOAST table containing it */
36+
37+
Detoasting requires opening an index scan on (chunk_id, chunk_seq). To read the
38+
entire datum or a partial slice, the executor searches the index for chunk 0,
39+
then iterates through consecutive chunks until the requested byte range is
40+
satisfied.
41+
42+
43+
2. Direct TOAST Format
44+
----------------------
45+
46+
Direct TOAST eliminates index lookups during detoasting by embedding direct
47+
physical tuple identifiers (ItemPointerData / TID) into the pointer and chunk
48+
tuples.
49+
50+
A Direct TOAST table has five columns:
51+
(chunk_id OID, chunk_seq INT4, chunk_data BYTEA,
52+
chunk_tids TID[], chunk_tid_offsets INT8[])
53+
54+
The pointer stored in the main table's tuple is struct varatt_direct (18 bytes):
55+
int32 va_rawsize; /* Original data size (includes 4-byte header) */
56+
uint32 va_extinfo; /* External saved size and 2 compression bits */
57+
Oid va_toastrelid; /* RelID of TOAST table containing it */
58+
ItemPointerData va_tid; /* Physical TID of root/terminal chunk */
59+
60+
Notice that sizeof(varatt_direct) == sizeof(varatt_external) == 18 bytes.
61+
Because both pointer types have identical byte lengths, switching between
62+
Plain and Direct TOAST requires zero tuple header or layout adjustments in the
63+
parent table.
64+
65+
2.1 Three-Tier Storage Hierarchy
66+
67+
Depending on the size of the stored value, Direct TOAST structures chunks in one
68+
of three tiers:
69+
70+
a) Single Chunk (datum size <= TOAST_MAX_CHUNK_SIZE, ~2KB):
71+
The data fits into a single chunk tuple. The main tuple's va_tid points
72+
directly to this chunk.
73+
Detoasting: exactly 1 buffer page fetch, 0 index scans.
74+
75+
b) Flat Multi-Chunk (<= DIRECT_TOAST_TREE_THRESHOLD = 100 chunks, up to ~200KB):
76+
The data chunks (leaf chunks) are inserted first. A terminal "root" chunk is
77+
then inserted which contains the last slice of data as chunk_data, plus a
78+
chunk_tids array containing the ItemPointerData of all leaf chunks in sequence.
79+
The main tuple's va_tid points to this root chunk.
80+
Detoasting: fetches the root chunk, reads chunk_tids, and directly fetches each
81+
referenced leaf chunk by TID without consulting any index.
82+
83+
c) Hierarchical Tree / DAG (> DIRECT_TOAST_TREE_THRESHOLD = 100 chunks):
84+
For very large values, chunks are organized into a multi-level balanced tree
85+
with a fanout of DIRECT_TOAST_FANOUT (50).
86+
Internal (non-leaf) chunks store:
87+
- chunk_tids: array of ItemPointers to child chunks.
88+
- chunk_tid_offsets: array of int64 cumulative start offsets for each child,
89+
terminated by the total subtree size.
90+
Detoasting: random-access partial slices (detoast_attr_slice) perform a binary
91+
search over chunk_tid_offsets at each tree level, pruning subtrees that do not
92+
overlap the requested range [sliceoffset, sliceoffset + slicelength). Slicing
93+
runs in O(log N) buffer accesses without touching an index.
94+
95+
2.2 Deletion & Memory Management
96+
97+
When a tuple containing a Direct TOAST pointer is deleted or updated,
98+
toast_delete_datum_direct_recursive() traverses the chunk DAG by following
99+
chunk_tids recursively and deleting every referenced chunk tuple. Because all
100+
chunk locations are known from the embedded TIDs, no index lookups or table scans
101+
are required during cascaded deletion.
102+
103+
2.3 Maintenance and Upgrades
104+
105+
Direct TOAST tables still create and maintain the standard B-Tree index on
106+
(chunk_id, chunk_seq) to support VACUUM cleanup, sequential table scans, and
107+
fallback verification (such as amcheck).
108+
109+
Existing tables can be upgraded in place from Plain to Direct TOAST:
110+
ALTER TABLE my_table SET (toast_flavour = direct);
111+
or programmatically via pg_ensure_direct_toast(reloid).
112+
This operation adds chunk_tids and chunk_tid_offsets to the TOAST table with
113+
fast-default NULLs without rewriting table rows or requiring exclusive locks on
114+
large data sets. Existing Plain TOAST pointers continue to be read via the
115+
traditional path, while new out-of-line writes generate Direct TOAST pointers.

‎src/backend/access/common/detoast.c‎

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,20 @@ toast_slice_copy_chunk(struct varlena *result, const char *chunk_data,
665665
}
666666

667667
/*
668-
* Recursively traverse and fetch slices from a direct TOAST tree/DAG.
668+
* toast_fetch_datum_direct_slice_recursive -
669+
*
670+
* Traverse a Direct TOAST tree/DAG to retrieve full datums or partial slices.
671+
*
672+
* Direct TOAST organizes chunks either as a single chunk, a flat multi-chunk
673+
* list (chunk_tids populated, chunk_tid_offsets NULL), or a multi-level tree
674+
* (both chunk_tids and chunk_tid_offsets populated).
675+
*
676+
* - Leaf chunks (chunk_tids IS NULL) copy their slice payload directly into
677+
* the result buffer at *logical_offset.
678+
* - Flat multi-chunk roots iterate through all child TIDs in chunk_tids.
679+
* - Interior tree chunks inspect chunk_tid_offsets to prune any subtrees that
680+
* do not overlap the requested range [sliceoffset, sliceoffset + slicelength),
681+
* achieving O(log N) slice fetching without consulting an index.
669682
*/
670683
static void
671684
toast_fetch_datum_direct_slice_recursive(Relation toastrel, ItemPointer tid,
@@ -711,8 +724,7 @@ toast_fetch_datum_direct_slice_recursive(Relation toastrel, ItemPointer tid,
711724
int nelems;
712725
int i;
713726

714-
if (slot->tts_tupleDescriptor->natts >= 5)
715-
offsets_datum = slot_getattr(slot, 5, &is_null_offsets);
727+
offsets_datum = slot_getattr(slot, 5, &is_null_offsets);
716728

717729
deconstruct_array_builtin(arr, TIDOID, &elems, &nulls, &nelems);
718730

‎src/backend/access/common/toast_internals.c‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,20 @@
3030

3131
int toast_flavour = TOAST_FLAVOUR_PLAIN;
3232

33+
/*
34+
* Direct TOAST chunk organization parameters:
35+
*
36+
* - DIRECT_TOAST_TREE_THRESHOLD (100):
37+
* Values requiring up to 100 chunks (~200KB) are structured with a flat
38+
* root chunk containing a single chunk_tids array of all leaf chunk TIDs.
39+
* This avoids tree depth overhead for small to medium values.
40+
*
41+
* - DIRECT_TOAST_FANOUT (50):
42+
* When a value exceeds DIRECT_TOAST_TREE_THRESHOLD, a multi-level balanced
43+
* tree is constructed where intermediate index chunks hold up to 50 child
44+
* TIDs (chunk_tids) and cumulative byte boundaries (chunk_tid_offsets).
45+
* This enables O(log N) slice retrieval without index scans.
46+
*/
3347
#define DIRECT_TOAST_TREE_THRESHOLD 100
3448
#define DIRECT_TOAST_FANOUT 50
3549

‎src/backend/catalog/toasting.c‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,12 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
272272
"chunk_data",
273273
BYTEAOID,
274274
-1, 0);
275+
/*
276+
* Direct TOAST columns:
277+
* chunk_tids stores child chunk TIDs for flat multi-chunk roots and interior DAG nodes.
278+
* chunk_tid_offsets stores byte offsets within chunk_tids for binary-search slicing.
279+
* Both columns are NULL for simple leaf chunks or legacy plain TOAST rows.
280+
*/
275281
TupleDescInitEntry(tupdesc, (AttrNumber) 4,
276282
"chunk_tids",
277283
TIDARRAYOID,
@@ -361,6 +367,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid,
361367
* duplicate TOAST chunk OIDs. The index might also be a little more
362368
* efficient this way, since btree isn't all that happy with large numbers
363369
* of equal keys.
370+
*
371+
* Even when Direct TOAST is active (which fetches chunks directly by TID),
372+
* this index is still created and maintained for backward compatibility,
373+
* sequential scan fallback, and VACUUM validation.
364374
*/
365375

366376
indexInfo = makeNode(IndexInfo);

‎src/backend/utils/fmgr/README‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ For TOAST-able data types, the PG_GETARG macro will deliver a de-TOASTed
206206
data value. There might be a few cases where the still-toasted value is
207207
wanted, but the vast majority of cases want the de-toasted result, so
208208
that will be the default. To get the argument value without causing
209-
de-toasting, use PG_GETARG_RAW_VARLENA_P(n).
209+
de-toasting, use PG_GETARG_RAW_VARLENA_P(n). Whether the out-of-line
210+
value is stored using Plain TOAST or Direct TOAST is an internal storage
211+
detail handled transparently by detoast_attr().
210212

211213
Some functions require a modifiable copy of their input values. In these
212214
cases, it's silly to do an extra copy step if we copied the data anyway

‎src/include/varatt.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ VARATT_EXTERNAL_OID8_SET_VALUEID(varatt_external_oid8 *toast_pointer, Oid8 id)
8282
toast_pointer->va_valueid_hi = (uint32) (id >> 32);
8383
}
8484

85+
/*
86+
* varatt_direct is a "Direct TOAST pointer".
87+
*
88+
* Instead of identifying chunks via an OID (va_valueid) which requires a
89+
* B-Tree index scan on (chunk_id, chunk_seq), va_tid points directly to the
90+
* root/terminal chunk tuple on disk.
91+
*
92+
* Notice that sizeof(varatt_direct) == sizeof(varatt_external) == 18 bytes.
93+
* This ensures that switching to Direct TOAST causes no size difference or
94+
* alignment change in parent table tuples.
95+
*
96+
* Like varatt_external, this struct is stored unaligned within actual tuples.
97+
*/
8598
typedef struct varatt_direct
8699
{
87100
int32 va_rawsize; /* Original data size (includes header) */

0 commit comments

Comments
 (0)