From 5904f0fc9a93aef667f4ec83c185099e90c2d66f Mon Sep 17 00:00:00 2001 From: Hannu Krosing Date: Thu, 3 Sep 2026 14:43:11 +0000 Subject: [PATCH v12 08/10] Add backend TOAST architecture documentation and clean up detoast access Add src/backend/access/common/README.toast documenting the on-disk layout, pointer tags, compression header formats, and detoasting architecture for both Plain TOAST (OID and OID8) and Direct TOAST (single-chunk, flat array, and multi-level DAG). Also update src/backend/utils/fmgr/README and clean up detoast.c header comments and array access. --- src/backend/access/common/README.toast | 124 ++++++++++++++++++++ src/backend/access/common/detoast.c | 30 +++-- src/backend/access/common/toast_internals.c | 14 +++ src/backend/catalog/toasting.c | 11 ++ src/backend/utils/fmgr/README | 4 +- src/include/varatt.h | 16 +++ 6 files changed, 190 insertions(+), 9 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..6c3ff307c12 --- /dev/null +++ b/src/backend/access/common/README.toast @@ -0,0 +1,124 @@ +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 index-backed format, VARTAG_ONDISK_OID = 18 / VARTAG_ONDISK_OID8 = 4) +2. Direct TOAST (Tree/TID direct format, VARTAG_DIRECT = 19) + +Whether a table writes Plain or Direct TOAST datums is controlled by the +relation storage parameter 'toast_flavour', while the default TOAST table +format created for new tables is controlled by the GUC 'toast_default_flavour' +(or 'toast_flavour = direct' on the table). + + +1. Plain TOAST Format +--------------------- + +Plain TOAST uses three logical columns in the TOAST table: + (chunk_id OID|OID8, chunk_seq INT4, chunk_data BYTEA) + +Each out-of-line value is assigned a unique 32-bit OID (toast_value_type = 'oid') +or 64-bit OID8 (toast_value_type = 'oid8') identifier. 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 Plain TOAST +chunk. + +The pointer stored in the main table's tuple is struct varatt_external_oid +(16-byte payload, 18 bytes on disk with 2-byte header) or struct +varatt_external_oid8 (20-byte payload, 22 bytes on disk with 2-byte header): + int32 va_rawsize; /* Original data size (includes 4-byte header) */ + uint32 va_extinfo; /* External saved size and 2 compression bits */ + Oid/Oid8 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. + +Direct TOAST tables (created when toast_default_flavour = 'direct' or when +the parent table has toast_flavour = 'direct', or upgraded in-place via +ensure_direct_toast) use a five-column schema: + (chunk_id OID|OID8, chunk_seq INT4, chunk_data BYTEA, + chunk_tids TID[], chunk_tid_offsets INT8[]) + +For Direct TOAST tuples, chunk_id is NULL. The pointer stored in the main +table's tuple is struct varatt_direct (20 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_oid8) == 20 bytes +(including 2 bytes of trailing compiler padding for 4-byte struct alignment). +Total on-disk varlena size with 2-byte header is 22 bytes. + +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 + +TOAST tables define a partial unique B-Tree index on (chunk_id, chunk_seq) +WHERE chunk_id IS NOT NULL. Because Direct TOAST chunks have chunk_id IS NULL, +they bypass index insertion and maintenance entirely, while allowing Plain TOAST +tuples to coexist in the same TOAST table. + +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 legacy 3-column TOAST +tables with fast-default NULLs and marks the index partial (WHERE chunk_id IS +NOT NULL) without rewriting table rows. Existing Plain TOAST pointers continue +to be read via the index-based 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 7381e682873..eb9a5da95f2 100644 --- a/src/backend/access/common/detoast.c +++ b/src/backend/access/common/detoast.c @@ -45,8 +45,8 @@ static void toast_fetch_datum_direct_slice_recursive(Relation toastrel, ItemPoin TupleTableSlot *slot); /* - * Unpacked metadata from either plain (varatt_external) or direct (varatt_direct) - * on-disk TOAST pointers. + * Unpacked metadata from either plain (varatt_external_oid / + * varatt_external_oid8) or direct (varatt_direct) on-disk TOAST pointers. */ typedef struct ToastExternalMetadata { @@ -55,8 +55,11 @@ typedef struct ToastExternalMetadata bool is_compressed; Oid toastrelid; bool is_direct; - Oid8 valueid; - struct varatt_direct direct_tp; + union + { + Oid8 valueid; + struct varatt_direct direct_tp; + }; } ToastExternalMetadata; static inline void @@ -70,7 +73,6 @@ toast_get_external_metadata(varlena *attr, ToastExternalMetadata *meta) meta->is_compressed = VARATT_DIRECT_IS_COMPRESSED(meta->direct_tp); meta->toastrelid = meta->direct_tp.va_toastrelid; meta->is_direct = true; - meta->valueid = 0; } else if (VARATT_IS_EXTERNAL_ONDISK(attr)) { @@ -673,7 +675,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, @@ -721,8 +736,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 edb196d690f..f4ecf42b094 100644 --- a/src/backend/access/common/toast_internals.c +++ b/src/backend/access/common/toast_internals.c @@ -31,6 +31,20 @@ int toast_default_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 6a87007ece8..44f06c022f1 100644 --- a/src/backend/catalog/toasting.c +++ b/src/backend/catalog/toasting.c @@ -282,6 +282,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 plain TOAST rows. + */ if (is_direct) { TupleDescInitEntry(tupdesc, (AttrNumber) 4, @@ -383,6 +389,11 @@ 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. + * + * When Direct TOAST format is used (which fetches chunks directly by TID + * with chunk_id IS NULL), this index is created as a partial unique index + * (WHERE chunk_id IS NOT NULL) so that Plain TOAST tuples can coexist in + * the same TOAST table without indexing Direct TOAST chunks. */ 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 956b9bc27ad..54b0c3e4804 100644 --- a/src/include/varatt.h +++ b/src/include/varatt.h @@ -82,6 +82,18 @@ VARATT_EXTERNAL_OID8_SET_VALUEID(varatt_external_oid8 *toast_pointer, Oid8 id) toast_pointer->va_valueid_hi = (uint32) (id >> 32); } +/* + * 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_oid8) == 20 bytes + * (including 2 bytes of trailing compiler padding for 4-byte struct alignment). + * + * Like varatt_external_oid8, this struct is stored unaligned within actual tuples. + */ typedef struct varatt_direct { int32 va_rawsize; /* Original data size (includes header) */ @@ -91,6 +103,10 @@ typedef struct varatt_direct ItemPointerData va_tid; /* Physical TID of the final chunk */ } varatt_direct; +StaticAssertDecl((sizeof(int32) + sizeof(uint32) + sizeof(Oid) + sizeof(ItemPointerData) + 2) == + sizeof(varatt_direct), + "varatt_direct unexpected size"); + /* * These macros define the "saved size" portion of va_extinfo. Its remaining * two high-order bits identify the compression method. -- 2.56.0.rc1.315.gc6ed9934b7-goog