From 963d41b98b41bd5c055c2d894a82dc93ee9d61b2 Mon Sep 17 00:00:00 2001 From: Hannu Krosing Date: Thu, 3 Sep 2026 14:43:11 +0000 Subject: [PATCH v2 8/8] 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. --- src/backend/access/common/README.toast | 115 ++++++++++++++++++++ src/backend/access/common/detoast.c | 25 ++++- src/backend/access/common/toast_internals.c | 14 +++ src/backend/catalog/toasting.c | 10 ++ src/backend/utils/fmgr/README | 4 +- src/include/varatt.h | 13 +++ 6 files changed, 175 insertions(+), 6 deletions(-) create mode 100644 src/backend/access/common/README.toast diff --git a/src/backend/access/common/README.toast b/src/backend/access/common/README.toast new file mode 100644 index 00000000000..9351faf4415 --- /dev/null +++ b/src/backend/access/common/README.toast @@ -0,0 +1,115 @@ +src/backend/access/common/README.toast + +The Oversized-Attribute Storage Technique (TOAST) +================================================= + +PostgreSQL stores table rows inside fixed-size pages (typically 8KB). When a +row exceeds the target threshold (typically BLCKSZ / 4, or ~2KB), the storage +engine uses TOAST to compress and/or move variable-length attributes (varlenas) +out-of-line into a separate auxiliary relation, known as a TOAST table +(cataloged as pg_toast_). + +PostgreSQL supports two on-disk external TOAST formats: +1. Plain TOAST (Traditional format, VARTAG_ONDISK = 18) +2. Direct TOAST (Tree/TID direct format, VARTAG_DIRECT = 19) + +Whether a table uses Plain or Direct TOAST is controlled by the relation +storage parameter 'toast_flavour' (or the GUC 'default_toast_flavour'). + + +1. Plain TOAST Format +--------------------- + +A Plain TOAST table has three columns: + (chunk_id OID, chunk_seq INT4, chunk_data BYTEA) + +Each out-of-line value is assigned a unique 32-bit OID (va_valueid). The +value's data is broken into sequential chunks of up to TOAST_MAX_CHUNK_SIZE +(typically ~2KB). An associated B-Tree index on (chunk_id, chunk_seq) +indexes every chunk. + +The pointer stored in the main table's tuple is struct varatt_external (18 bytes): + int32 va_rawsize; /* Original data size (includes 4-byte header) */ + uint32 va_extinfo; /* External saved size and 2 compression bits */ + Oid va_valueid; /* Unique ID of value within TOAST table */ + Oid va_toastrelid; /* RelID of TOAST table containing it */ + +Detoasting requires opening an index scan on (chunk_id, chunk_seq). To read the +entire datum or a partial slice, the executor searches the index for chunk 0, +then iterates through consecutive chunks until the requested byte range is +satisfied. + + +2. Direct TOAST Format +---------------------- + +Direct TOAST eliminates index lookups during detoasting by embedding direct +physical tuple identifiers (ItemPointerData / TID) into the pointer and chunk +tuples. + +A Direct TOAST table has five columns: + (chunk_id OID, chunk_seq INT4, chunk_data BYTEA, + chunk_tids TID[], chunk_tid_offsets INT8[]) + +The pointer stored in the main table's tuple is struct varatt_direct (18 bytes): + int32 va_rawsize; /* Original data size (includes 4-byte header) */ + uint32 va_extinfo; /* External saved size and 2 compression bits */ + Oid va_toastrelid; /* RelID of TOAST table containing it */ + ItemPointerData va_tid; /* Physical TID of root/terminal chunk */ + +Notice that sizeof(varatt_direct) == sizeof(varatt_external) == 18 bytes. +Because both pointer types have identical byte lengths, switching between +Plain and Direct TOAST requires zero tuple header or layout adjustments in the +parent table. + +2.1 Three-Tier Storage Hierarchy + +Depending on the size of the stored value, Direct TOAST structures chunks in one +of three tiers: + +a) Single Chunk (datum size <= TOAST_MAX_CHUNK_SIZE, ~2KB): + The data fits into a single chunk tuple. The main tuple's va_tid points + directly to this chunk. + Detoasting: exactly 1 buffer page fetch, 0 index scans. + +b) Flat Multi-Chunk (<= DIRECT_TOAST_TREE_THRESHOLD = 100 chunks, up to ~200KB): + The data chunks (leaf chunks) are inserted first. A terminal "root" chunk is + then inserted which contains the last slice of data as chunk_data, plus a + chunk_tids array containing the ItemPointerData of all leaf chunks in sequence. + The main tuple's va_tid points to this root chunk. + Detoasting: fetches the root chunk, reads chunk_tids, and directly fetches each + referenced leaf chunk by TID without consulting any index. + +c) Hierarchical Tree / DAG (> DIRECT_TOAST_TREE_THRESHOLD = 100 chunks): + For very large values, chunks are organized into a multi-level balanced tree + with a fanout of DIRECT_TOAST_FANOUT (50). + Internal (non-leaf) chunks store: + - chunk_tids: array of ItemPointers to child chunks. + - chunk_tid_offsets: array of int64 cumulative start offsets for each child, + terminated by the total subtree size. + Detoasting: random-access partial slices (detoast_attr_slice) perform a binary + search over chunk_tid_offsets at each tree level, pruning subtrees that do not + overlap the requested range [sliceoffset, sliceoffset + slicelength). Slicing + runs in O(log N) buffer accesses without touching an index. + +2.2 Deletion & Memory Management + +When a tuple containing a Direct TOAST pointer is deleted or updated, +toast_delete_datum_direct_recursive() traverses the chunk DAG by following +chunk_tids recursively and deleting every referenced chunk tuple. Because all +chunk locations are known from the embedded TIDs, no index lookups or table scans +are required during cascaded deletion. + +2.3 Maintenance and Upgrades + +Direct TOAST tables still create and maintain the standard B-Tree index on +(chunk_id, chunk_seq) to support VACUUM cleanup, sequential table scans, and +fallback verification (such as amcheck). + +Existing tables can be upgraded in place from Plain to Direct TOAST: + ALTER TABLE my_table SET (toast_flavour = direct); +or programmatically via pg_ensure_direct_toast(reloid). +This operation adds chunk_tids and chunk_tid_offsets to the TOAST table with +fast-default NULLs without rewriting table rows or requiring exclusive locks on +large data sets. Existing Plain TOAST pointers continue to be read via the +traditional path, while new out-of-line writes generate Direct TOAST pointers. diff --git a/src/backend/access/common/detoast.c b/src/backend/access/common/detoast.c index 4a7d7801c51..44c35496184 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -55,8 +55,11 @@ typedef struct ToastExternalMetadata Oid toastrelid; bool is_direct; Oid valueid; - struct varatt_direct direct_tp; - varatt_external tp; + union + { + varatt_direct direct_tp; + varatt_external tp; + }; } ToastExternalMetadata; static inline void @@ -757,7 +760,20 @@ toast_slice_copy_chunk(struct varlena *result, const char *chunk_data, } /* - * Recursively traverse and fetch slices from a direct TOAST tree/DAG. + * toast_fetch_datum_direct_slice_recursive - + * + * Traverse a Direct TOAST tree/DAG to retrieve full datums or partial slices. + * + * Direct TOAST organizes chunks either as a single chunk, a flat multi-chunk + * list (chunk_tids populated, chunk_tid_offsets NULL), or a multi-level tree + * (both chunk_tids and chunk_tid_offsets populated). + * + * - Leaf chunks (chunk_tids IS NULL) copy their slice payload directly into + * the result buffer at *logical_offset. + * - Flat multi-chunk roots iterate through all child TIDs in chunk_tids. + * - Interior tree chunks inspect chunk_tid_offsets to prune any subtrees that + * do not overlap the requested range [sliceoffset, sliceoffset + slicelength), + * achieving O(log N) slice fetching without consulting an index. */ static void toast_fetch_datum_direct_slice_recursive(Relation toastrel, ItemPointer tid, @@ -803,8 +819,7 @@ toast_fetch_datum_direct_slice_recursive(Relation toastrel, ItemPointer tid, int nelems; int i; - if (slot->tts_tupleDescriptor->natts >= 5) - offsets_datum = slot_getattr(slot, 5, &is_null_offsets); + offsets_datum = slot_getattr(slot, 5, &is_null_offsets); deconstruct_array_builtin(arr, TIDOID, &elems, &nulls, &nelems); diff --git a/src/backend/access/common/toast_internals.c b/src/backend/access/common/toast_internals.c index 45a9878ade0..2f701a4ab6c 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -30,6 +30,20 @@ int toast_flavour = TOAST_FLAVOUR_PLAIN; +/* + * Direct TOAST chunk organization parameters: + * + * - DIRECT_TOAST_TREE_THRESHOLD (100): + * Values requiring up to 100 chunks (~200KB) are structured with a flat + * root chunk containing a single chunk_tids array of all leaf chunk TIDs. + * This avoids tree depth overhead for small to medium values. + * + * - DIRECT_TOAST_FANOUT (50): + * When a value exceeds DIRECT_TOAST_TREE_THRESHOLD, a multi-level balanced + * tree is constructed where intermediate index chunks hold up to 50 child + * TIDs (chunk_tids) and cumulative byte boundaries (chunk_tid_offsets). + * This enables O(log N) slice retrieval without index scans. + */ #define DIRECT_TOAST_TREE_THRESHOLD 100 #define DIRECT_TOAST_FANOUT 50 diff --git a/src/backend/catalog/toasting.c b/src/backend/catalog/toasting.c index 22442715993..c0f561c6485 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -220,6 +220,12 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, "chunk_data", BYTEAOID, -1, 0); + /* + * Direct TOAST columns: + * chunk_tids stores child chunk TIDs for flat multi-chunk roots and interior DAG nodes. + * chunk_tid_offsets stores byte offsets within chunk_tids for binary-search slicing. + * Both columns are NULL for simple leaf chunks or legacy plain TOAST rows. + */ TupleDescInitEntry(tupdesc, (AttrNumber) 4, "chunk_tids", TIDARRAYOID, @@ -309,6 +315,10 @@ create_toast_table(Relation rel, Oid toastOid, Oid toastIndexOid, * duplicate TOAST chunk OIDs. The index might also be a little more * efficient this way, since btree isn't all that happy with large numbers * of equal keys. + * + * Even when Direct TOAST is active (which fetches chunks directly by TID), + * this index is still created and maintained for backward compatibility, + * sequential scan fallback, and VACUUM validation. */ indexInfo = makeNode(IndexInfo); diff --git a/src/backend/utils/fmgr/README b/src/backend/utils/fmgr/README index 9958d38992b..da42e97036d 100644 --- a/src/backend/utils/fmgr/README +++ b/src/backend/utils/fmgr/README @@ -206,7 +206,9 @@ For TOAST-able data types, the PG_GETARG macro will deliver a de-TOASTed data value. There might be a few cases where the still-toasted value is wanted, but the vast majority of cases want the de-toasted result, so that will be the default. To get the argument value without causing -de-toasting, use PG_GETARG_RAW_VARLENA_P(n). +de-toasting, use PG_GETARG_RAW_VARLENA_P(n). Whether the out-of-line +value is stored using Plain TOAST or Direct TOAST is an internal storage +detail handled transparently by detoast_attr(). Some functions require a modifiable copy of their input values. In these cases, it's silly to do an extra copy step if we copied the data anyway diff --git a/src/include/varatt.h b/src/include/varatt.h index 7041e47754a..fa257253c4b 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -40,6 +40,19 @@ typedef struct varatt_external Oid va_toastrelid; /* RelID of TOAST table containing it */ } varatt_external; +/* + * varatt_direct is a "Direct TOAST pointer". + * + * Instead of identifying chunks via an OID (va_valueid) which requires a + * B-Tree index scan on (chunk_id, chunk_seq), va_tid points directly to the + * root/terminal chunk tuple on disk. + * + * Notice that sizeof(varatt_direct) == sizeof(varatt_external) == 18 bytes. + * This ensures that switching to Direct TOAST causes no size difference or + * alignment change in parent table tuples. + * + * Like varatt_external, this struct is stored unaligned within actual tuples. + */ typedef struct varatt_direct { int32 va_rawsize; /* Original data size (includes header) */ -- 2.55.0.970.g62bdec98f9-goog