RonDB training · module 07 of 25 · 20 min
Data structure layout
From the memory a data node takes at startup down to the bytes of one row, and what happens to them during an update.
Memory · allocated once
A data node takes all its memory at startup and never calls malloc again
- No allocation or freeing in the hot path: no surprises under load, no fragmentation of the process heap.
- A global memory manager hands out 32 KB pages to 12 resource groups.
- Each group has a reserved size, a maximum and a priority, so memory moves to where it is needed.
- By default the size of everything is computed from the machine (module 13).
| Resource group | Holds |
|---|---|
| DataMemory | Rows, hash indexes, ordered indexes |
| TransactionMemory | Operations, copy tuples |
| DiskPageBuffer | The page cache for disk data |
| JobBuffer · Transporter | Signals between threads and nodes |
| Schema · SchemaTrans | Table definitions, schema changes |
| Query · Replication · … | Query workers, replication buffers |
docs: automatic_memory_config.md (DataMemory) · code: src/kernel/blocks/record_types.hpp (RG_TRANSACTION_MEMORY 1 … RG_REPLICATION_MEMORY 12) · src/kernel/vm/ndbd_malloc_impl.hpp (MM_RG_COUNT 12)
Ownership · from table to thread
Each fragment replica belongs to one ldm thread, which owns its pages
A fragment replica is the part of one partition stored on one node. One ldm thread owns it: its row pages, hash index and ordered indexes.
The other replica lives on the other node of the node group. Writes go to the owning ldm thread; reads can also run in query workers (module 11).
docs: table_options.md (Partitioning of tables) · rondb_vm_arch.md (ldm threads) · module 02 (partitions and replicas)
PART 1
Row ids
Every row has an address that never changes.
Row id · logical page and index
A row id is a logical page number and an index in that page
The fixed part of a row never moves. A page map per fragment turns the logical page into a physical 32 KB page; the index is the row's word offset in that page.
The primary replica picks the row id at insert and sends it along: every replica uses the same row id, on its own physical pages.
docs: row_data_structure.md (Row format) · code: dbtup/DbtupPageMap.cpp (logical to physical pages) · tuppage.hpp (32-word page header) · ndb_limits.h (MAX_TUPLES_PER_PAGE 8191) · page numbers and row size are illustrations
Row ids · what depends on them
Recovery, indexes and checkpoints all rely on stable row ids
Node restart
A starting node and a live node compare rows by row id to synchronise, page by page (module 16).
Indexes
The hash index stores row ids; ordered indexes store the physical page and offset. A moved row would break both.
OPTIMIZE TABLE
Compacts variable-size parts, never the fixed parts: those are the row ids.
The fixed part is the row's anchor. Everything that can change size lives elsewhere and is reached through a reference from it.
docs: row_data_structure.md · basic_sql.md (Index sizes, Optimize/Analyze Table)
PART 2
Anatomy of a row
A fixed part, a variable part, a disk part.
Row parts · up to three
Row parts: a fixed part, plus variable and disk parts when the columns need them
The fixed part is the entry point: header and fixed-size columns, at the row id.
Variable-size and dynamic columns live in the variable part. When it moves to a bigger slot, RonDB updates the reference in the fixed part.
Disk columns live in the disk part, variable-size since RonDB 22.10, cached in memory by PGMAN.
docs: row_data_structure.md (Row format) · code: Dbtup.hpp (Var_part_ref; m_base_record_page_no/idx: the disk part's reference back) · DbtupVarAlloc.cpp (realloc_var_part)
Row header · word by word
The row header grows with what the table needs: 16, 20, 28 or 36 bytes
Four words always: the operation owning the row, header bits, checksum and GCI. Node restart uses the GCI to find rows to resend.
A NULL bit per nullable column, and an 8-byte reference to the variable part with the variable-size columns. MySQL reserves it in other tables too, unless ROW_FORMAT=FIXED.
Tables with disk columns add an 8-byte disk reference. It comes first, then the variable part reference, then the NULL bits.
code: dbtup/Dbtup.hpp (Tuple_header) · dbtup/DbtupMeta.cpp (checksum, GCI, disk ref, var ref, NULL offset) · ha_ndbcluster.cc (ForceVarPart) · docs: row_data_structure.md (16/20/28/36 bytes)
Header bits · word 1
Header bits: sixteen bits of version, sixteen bits of state
| Bits | Name | Meaning |
|---|---|---|
| 0–15 | version | Row version; ordered index scans use it to see which version they read |
| 16 | COPY_TUPLE | This is a copy made by an update, not the row itself |
| 17 · 30 | DISK_PART · DISK_VAR_PART | The row has a disk part · a variable-size one |
| 20 · 23 | ALLOC · FREE | Inserted by a transaction not yet committed · free slot |
| 22 | MM_GROWN | The variable part was grown for an update not yet committed |
| 24 · 28 | LCP_SKIP · LCP_DELETE | Bookkeeping for the checkpoint scan running now |
| 26 | VAR_PART | A variable part exists |
| 29 | DELETE_WAIT | Marked for delete, waiting for its disk page |
code: storage/ndb/src/kernel/blocks/dbtup/Dbtup.hpp (Tuple_header: TUP_VERSION_MASK 0xFFFF, COPY_TUPLE 0x00010000 … DISK_VAR_PART 0x40000000; a few flags left out)
Fixed-size page · one row size per fragment
A fixed-size page is a header and an array of equal slots
Every slot on a page has the same size: header plus fixed columns of this table. The index in the row id is the slot's word offset: with 20-word slots, row 3 is at index 60.
The page header tracks free slots and which 4 KB parts changed since the last checkpoint, so a checkpoint can skip what did not (module 14).
code: dbtup/tuppage.hpp (Tup_fixsize_page: HEADER_WORDS 32, DATA_WORDS = 8192 − 32, change map bits 24–31 of m_flags, 4 kB per bit)
Variable-size page · data from the front, directory from the back
A variable-size page finds each entry through a directory at its end
Entries grow from the front, the directory from the back. A row's variable-part reference names the page and its directory slot.
One directory word per entry: length and position in words, plus two flags. Pages sit in free lists by free space: 0–199, 200–499, 500–999, 1000–4079, 4080–7783 and 7784–8159 words.
code: dbtup/tuppage.hpp (Tup_varsize_page: CHAIN, FREE, LEN_MASK, POS_MASK) · tuppage.cpp (free_record) · dbtup/DbtupVarAlloc.cpp (alloc_record with CHAIN, init_list_sizes) · DbtupDiskAlloc.cpp (disk rows without CHAIN)
Variable part · what one entry holds
The variable part: column offsets, column data, then the dynamic part
A column's length is the next offset minus its own, so any column is found with two reads. To grow an entry, RonDB moves it and updates the reference in the fixed part.
code: DbtupExecQuery.cpp (shrink_tuple: num_vars + 1 offsets, data, ALIGN_WORD, dynamic part) · DbtupVarAlloc.cpp (realloc_var_part, move_var_part update the fixed part's Var_part_ref) · tuppage.hpp (32-bit directory entry) · the column values are an illustration
Column format · COLUMN_FORMAT FIXED or DYNAMIC
FIXED or DYNAMIC columns: dynamic ones cost CPU and buy flexibility
FIXED · the default
- Fixed-size columns in the fixed part, variable ones in the variable part.
- Space is always there, even for NULL.
- Fastest to read and write.
DYNAMIC
- In the dynamic part: NULL takes no space.
- Added online (module 05) and compacted by OPTIMIZE TABLE.
- About 40% more CPU to read and write the row: under 5% of a key lookup, close to 40% in a full table scan.
-- the default comes from ndb-default-column-format, FIXED unless set CREATE TABLE t1 (a INT COLUMN_FORMAT FIXED, b INT COLUMN_FORMAT DYNAMIC) ENGINE=NDB;
docs: row_data_structure.md (Fixed row format, Dynamic row format) · code: ha_ndbcluster.cc (ndb_default_column_format, default FIXED)
Sizing · memory per row, from the parts
What one row of a typical table costs in DataMemory
| Part | Bytes per row | Source |
|---|---|---|
| Header with NULL bits and var reference | 28 | row_data_structure.md |
| Fixed-size columns | their sizes | a BIGINT is 8, an INT 4 |
| Variable part: directory entry and end offset, + 2 per var column | 6 | tuppage.hpp, shrink_tuple |
| Variable-size values | their lengths | utf8mb4: up to 4 per character |
| Primary key hash index | ≈15 | basic_sql.md |
| Each ordered index, PRIMARY included unless USING HASH | ≈10 | basic_sql.md |
Then multiply by the number of replicas. Words are 4 bytes, so parts are rounded up to whole words.
docs: row_data_structure.md · basic_sql.md (Index sizes) · module 13 sizes DataMemory for a whole cluster
PART 3
Changing a row
Updates work on a copy until commit.
Copy tuple · the uncommitted version
Copy tuples: an update writes a copy of the row in TransactionMemory
The row's first header word points to the operation that owns it; RNIL means no transaction has it.
The first write copies the whole row into TransactionMemory and changes the copy. Every column gets its full size there, variable-size and disk columns too. The change mask records which columns changed.
code: dbtup/Dbtup.hpp (alloc_copy_tuple from RG_TRANSACTION_MEMORY, total_rec_size, COPY_TUPLE_HEADER32 4, ChangeMask) · DbtupMeta.cpp (total_rec_size: every column at its maximum size) · docs: row_data_structure.md (row header)
Concurrency · readers never wait for the writer
Concurrency: readers see the committed row; commit installs the copy, abort drops it
Read committed
Reads the committed version in DataMemory while the update works on its copy. No lock, no wait.
Commit
Copies the new values and version into the row at its row id. The copy is freed.
Abort
Frees the copy. The row in DataMemory was never touched.
If the update makes the variable part bigger, RonDB grows it at once and marks the row MM_GROWN: commit can then never run out of memory. An abort shrinks it back.
code: dbtup/Dbtup.hpp (Tuple_header comment on MM_GROWN) · module 09 covers locks and the commit protocol
Recap · module 07
What to take away
- All memory is taken at startup, as 32 KB pages in 12 resource groups.
- A row id is logical page + index; it never changes and is the same on every replica.
- A row is a fixed part (header 16–36 bytes), a variable part and maybe a disk part.
- Variable-size pages find entries through a directory at the end of the page.
- Updates work on a copy in TransactionMemory; readers keep reading the committed row.
RonDB training · 25 modules · you are at 07