|
| 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. |
0 commit comments