Understanding PostgreSQL Page (Block) Structure
PostgreSQL Page (Block) Structure
PostgreSQL stores data on disk in fixed-size units called pages or blocks. In this post, we’ll explore how PostgreSQL organizes data within these pages and demonstrate how to inspect their contents using the pg_filedump tool.

What Is a Page?
In PostgreSQL, a page is the smallest unit of storage for reading and writing data from disk.
- Every table and index is made up of these 8 KB pages.
Page Layout Overview
A PostgreSQL page has three main sections:
1. Page Header
Contains metadata about the page, including:
- Number of tuples
- Offset of free space
- LSN (Log Sequence Number), checksum, flags, etc.
Approximate size: 24 bytes
2. Line Pointers (Item Identifiers)
Each tuple stored in the page has a corresponding line pointer, which:
- Points to the tuple’s offset within the page
- Occupies 4 bytes per pointer
- Grows downward from the top of the page
3. Tuples (Row Data)
The actual data is stored at the bottom of the page and grows upward.
This structure ensures:
- Line pointers grow from the top
- Tuples grow from the bottom
- Free space is between them and dynamically adjusted
Tuple Insertion Process
When a new row is inserted:
- PostgreSQL checks for available space on the page.
- A line pointer is added to the page header.
- The tuple is inserted at the bottom of the page.
Thus, a tuple’s location is represented by the ctid, which is a combination of:
- Block number
- Line pointer offset
Example: ctid = (0,1) refers to block 0, tuple 1.
Inspecting Physical Data with pg_filedump
pg_filedump is a powerful tool for inspecting the physical layout of PostgreSQL data files at the page level.
Installation (PostgreSQL 13 on RHEL-based systems)
rpm -ivh https://ftp.postgresql.org/pub/repos/yum/13/redhat/rhel-9.3-x86_64/pg_filedump_13-17.3-1PGDG.rhel9.x86_64.rpm
Usage Example
/usr/pgsql-13/bin/pg_filedump -f <file_path>
Practical Example
Step 1: Create a Table
CREATE TABLE test_wal (id serial, val text);
Initially, the table is empty, and pg_filedump won’t show meaningful content yet.
Step 2: Insert a Row
INSERT INTO test_wal (val) VALUES ('deneme');
The data is inserted, but PostgreSQL doesn’t immediately flush it to disk.
Step 3: Find the File Path
SELECT pg_relation_filepath('test_wal');
-- Example output: base/50654/57581
Step 4: Check the Page (Before Checkpoint)
/usr/pgsql-13/bin/pg_filedump -f /var/lib/pgsql/13/data/base/50654/57581
You may see something like:
*******************************************************************
* PostgreSQL File/Block Formatted Dump Utility
*
* File: /var/lib/pgsql/13/data/base/50654/57581
* Options used: -f
*******************************************************************
Notice: Block size determined from reading block 0 is zero, using default 8192 instead.
Hint: Use -S <size> to specify the size manually.
Block 0 ********************************************************
<Header> -----
Block Offset: 0x00000000 Offsets: Lower 0 (0x0000)
Block: Size 0 Version 0 Upper 0 (0x0000)
LSN: logid 0 recoff 0x00000000 Special 0 (0x0000)
Items: 0 Free Space: 0
Checksum: 0x0000 Prune XID: 0x00000000 Flags: 0x0000 ()
Length (including item array): 24
Error: Invalid header information.
0000: 00000000 00000000 00000000 00000000 ................
0010: 00000000 00000000 ........
<Data> -----
Empty block - no items listed
<Special Section> -----
Error: Invalid special section encountered.
Error: Special section points off page. Unable to dump contents.
This happens because the changes haven’t been flushed to disk yet.
Step 5: Force Checkpoint and Retry
CHECKPOINT;
Then run pg_filedump again:
/usr/pgsql-13/bin/pg_filedump -f /var/lib/pgsql/13/data/base/50654/57581
Sample output:
*******************************************************************
* PostgreSQL File/Block Formatted Dump Utility
*
* File: /var/lib/pgsql/13/data/base/50654/57581
* Options used: -f
*******************************************************************
Block 0 ********************************************************
<Header> -----
Block Offset: 0x00000000 Offsets: Lower 28 (0x001c)
Block: Size 8192 Version 4 Upper 8152 (0x1fd8)
LSN: logid 0 recoff 0x31c787a8 Special 8192 (0x2000)
Items: 1 Free Space: 8124
Checksum: 0x0000 Prune XID: 0x00000000 Flags: 0x0000 ()
Length (including item array): 28
0000: 00000000 a887c731 00000000 1c00d81f .......1........
0010: 00200420 00000000 d89f4600 . . ......F.
<Data> -----
Item 1 -- Length: 35 Offset: 8152 (0x1fd8) Flags: NORMAL
1fd8: 4c200000 00000000 09000000 00000000 L ..............
1fe8: 01000200 02081800 01000000 0f64656e .............den
1ff8: 656d65 eme
*** End of File Encountered. Last Block Read: 0 ***
Now you can see the actual tuple and how it’s stored physically.
Checking the ctid
SELECT ctid FROM test_wal WHERE id=1;
-- Output: (0,1)
0: Block number1: Line pointer index
Additional Notes
- PostgreSQL doesn’t immediately flush data to disk upon insert.
- You can influence write behavior using parameters like:
checkpoint_timeoutbgwriter_delaymax_wal_size
Summary
Understanding PostgreSQL’s internal page structure is essential for performance tuning, debugging, and in-depth database analysis. In this article, we covered:
- How data is organized on disk in 8 KB pages
- Tuple layout within a page
- How to inspect physical storage using
pg_filedump
This low-level insight gives you the tools to better understand what’s really happening inside your PostgreSQL instance.
← PostgreSQL Blog