# Tuple Layout

When you insert a new row into a table, postgreSQL creates a tuple, a contiguous chunk of memory bytes that contains both system metadata and your actual data.

A tuple starts with a 23 byte header. Every single tuple in every table has this same header structure regardless of how many columns you have or which datatype you are using. These 23 bytes are pure overhead, which is why storing lots of tiny rows (say, a two-column table with a smallint and a boolean) is inefficient—you're spending more space on metadata than on data.

### Tuple Headers

**t\_xmin**

The first 4 bytes are `t_xmin`, a transaction ID. This is the ID of the transaction that created this tuple. When you run `INSERT INTO users VALUES (1, 'Alice')` inside transaction 1000, the resulting tuple gets `t_xmin=1000`.

This field never changes. Ever. Even if the tuple is later updated or deleted, `t_xmin` remains the ID of the transaction that originally created it. This is the foundation of MVCC. To determine if a tuple is visible to your transaction, PostgreSQL looks at `t_xmin` and asks: "Was this transaction committed before my snapshot was taken? Am I allowed to see tuples it created?"

**t\_xmax**

The next 4 bytes are `t_xmax`, which is more complicated. In the simple case, if the tuple has never been deleted or locked, `t_xmax` is zero. But if a transaction deletes this tuple, `t_xmax` gets set to that transaction's ID.

Here's where it gets subtle: `t_xmax` can also mean "this tuple is locked" (as in `SELECT ... FOR UPDATE`) rather than deleted. How do you tell the difference? You have to look at the `t_infomask` flags. If `HEAP_XMAX_LOCK_ONLY` is set, then `t_xmax` is a lock, not a deletion. If `HEAP_UPDATED` is set, this was an UPDATE (so there's a newer version somewhere). If neither is set, it's a plain DELETE.

And there's yet another case: if multiple transactions lock the same tuple concurrently (for example, multiple `SELECT ... FOR SHARE` statements), `t_xmax` doesn't hold a transaction ID at all. Instead, it holds a "MultiXactId," which is an ID into a separate structure (`pg_multixact`) that stores a list of transaction IDs. The `HEAP_XMAX_IS_MULTI` flag in `t_infomask` tells you this has happened.

This overloading of `t_xmax` is a clever space optimization, but it makes the visibility logic quite complex.

**t\_cid**

The next 6 bytes are `t_ctid`, a tuple identifier consisting of a page number (4 bytes) and a line pointer number (2 bytes). This field serves a dual purpose.

First, it's the tuple's own physical address, often called the TID (tuple identifier). If you run:

```sql
SELECT ctid, * FROM users;
```

You'll see values like `(0,1)`, meaning page 0, line pointer 1. This is how indexes refer to tuples—they store the TID.

Second, `t_ctid` is used for update chains. When a tuple is the current version (hasn't been updated), its `t_ctid` points to itself: `(0,1)` points to `(0,1)`. But when a tuple is updated, the old version's `t_ctid` gets changed to point to the new version. This creates a chain:

```plaintext
Old version at (0,1): t_ctid = (0,2)
New version at (0,2): t_ctid = (0,2)  [self-pointer]
```

If you update again:

Code

```plaintext
Old v1 at (0,1): t_ctid = (0,2)
Old v2 at (0,2): t_ctid = (0,3)
Current at (0,3): t_ctid = (0,3)
```

This chain allows PostgreSQL to follow updates. An index points to `(0,1)`. When you look up that TID, you find an old version with `t_ctid=(0,2)`, so you follow the chain to `(0,2)`, then to `(0,3)`, where you find the current version.

Long update chains are a performance problem. If a tuple has been updated 100 times, you have to follow 100 hops to reach the current version. This is one reason why HOT (Heap-Only Tuple) updates are so valuable—they keep the chain short and on the same page.

**t\_infomask**

Now we come to the most complex field in the tuple header: `t_infomask`, a 2-byte bitmap containing 16 Boolean flags. This is where PostgreSQL packs a huge amount of state information.

Some flags describe the tuple's data layout. `HEAP_HASNULL` (bit 0) means at least one column is NULL, so there's a null bitmap after the tuple header. `HEAP_HASVARWIDTH` (bit 1) means there are variable-length columns. `HEAP_HASEXTERNAL` (bit 2) means at least one column is stored out-of-line in a TOAST table.

Other flags describe the tuple's MVCC state. `HEAP_XMAX_LOCK_ONLY` (bit 7) means `t_xmax` is a lock, not a delete. `HEAP_UPDATED` (bit 13) means this tuple was updated, so there's a newer version. `HEAP_XMAX_IS_MULTI` (bit 12) means `t_xmax` is a MultiXactId.

But the most important flags are the hint bits: `HEAP_XMIN_COMMITTED` (bit 8), `HEAP_XMIN_INVALID` (bit 9), `HEAP_XMAX_COMMITTED` (bit 10), and `HEAP_XMAX_INVALID` (bit 11). Understanding hint bits is essential.

Here's the problem they solve: To determine if a tuple is visible, we need to know whether `t_xmin` and `t_xmax` are committed or aborted. This information lives in the CLOG (commit log), also called `pg_xact`. The CLOG is on disk (or maybe cached in memory), and checking it requires I/O. If we had to check the CLOG for every tuple we examine during a query, performance would be terrible.

Hint bits cache this information directly in the tuple. The first time someone checks whether transaction 1000 is committed, they look it up in the CLOG. If it's committed, they set the `HEAP_XMIN_COMMITTED` bit in the tuple's `t_infomask` and mark the page dirty. From that point on, anyone who looks at this tuple sees the hint bit and knows immediately that transaction 1000 is committed, without having to touch the CLOG.

This has a fascinating consequence: a SELECT query can cause writes. If you run a big INSERT, creating millions of new tuples, and then immediately run a SELECT that scans the table, that SELECT will be the first to check the visibility of each tuple. For every tuple, it will look up the transaction in CLOG (probably finding it committed), set the hint bit, and mark the page dirty. Eventually, those dirty pages get written to disk. Your SELECT just triggered a write of the entire table.

The solution is to run VACUUM immediately after a bulk insert. VACUUM will proactively set all the hint bits, so subsequent queries won't have to.

```sql
CREATE TABLE hint_demo (id INT);
INSERT INTO hint_demo VALUES (1);

SELECT t_infomask FROM heap_page_items(get_raw_page('hint_demo', 0));
```

You might see `t_infomask = 2818`, which is `0x0B02` in hex. Let's decode that:

```txt
Binary: 0000 1011 0000 0010
Bit 1: HEAP_HASVARWIDTH (set)
Bit 8: HEAP_XMIN_COMMITTED (set)
Bit 9: HEAP_XMIN_INVALID (set)
Bit 11: HEAP_XMAX_INVALID (set)
```

Wait, both `HEAP_XMIN_COMMITTED` and `HEAP_XMIN_INVALID` are set? That seems contradictory. Actually, `HEAP_XMIN_INVALID` set means the tuple was created by an aborted transaction, which overrides the "committed" bit. This tuple is garbage and will never be visible to anyone.

**t\_infomask2**

There's a second infomask field, `t_infomask2`, which is also 2 bytes. The lower 11 bits store the number of attributes (columns) in this tuple, allowing up to 2047 columns per table. The upper bits are flags related to HOT updates: `HEAP_HOT_UPDATED` (bit 14) and `HEAP_ONLY_TUPLE` (bit 13).
