pg_stat_io: I/O Breakdown

Break disk I/O down to the exact operation — heap writes, WAL, and VACUUM — instead of one undifferentiated number

Every earlier lab in this block measured I/O indirectly — cache hit ratios from pg_statio_user_tables, file sizes from pg_total_relation_size. pg_stat_io, introduced in PostgreSQL 16, breaks I/O down to the operation level: which backend type performed it, what kind of object it touched — relation data or WAL — and in what context: an ordinary read, a VACUUM pass, a bulk read, or a bulk write.\n\nThis lab resets the view for a clean baseline, generates a real workload, and reads the breakdown after each phase — INSERT, then VACUUM — to see exactly which context each one shows up under, and why the WAL and the table's own heap are tracked as two completely separate objects even though a single INSERT writes to both.

Turning On Timing (Optional, Session-Scoped)

SET track_io_timing = on;

Without this, pg_stat_io still reports accurate read/write/extend counts — it just leaves the timing columns (read_time, write_time) at zero. track_io_timing is superuser-context, so a single SET is enough for the current session.

A Clean Baseline

SELECT pg_stat_reset_shared('io');

Unlike the per-table resets in Lab 2.4.1, this resets the shared, cluster-wide pg_stat_io view in one call.

Reading the Breakdown

SELECT backend_type, object, context, reads, writes, extends, hits
FROM pg_stat_io
WHERE backend_type = 'client backend' AND object = 'relation'
ORDER BY context;

object = 'relation' is heap and index data; object = 'wal' is the write-ahead log — a single INSERT touches both, but they are tracked as entirely separate rows. context further splits relation I/O into normal (ordinary access), vacuum (VACUUM's own reads/writes), bulkread/bulkwrite (the ring-buffer-bounded operations from Lab 2.4.6), and init (extending a relation for the first time).

object: relation vs wal

pg_stat_io tracks I/O against relation data (table and index heap pages) completely separately from I/O against the write-ahead log, even within the same statement. An INSERT generates both — a relation write for the new row's page, and a WAL write to make that change durable — and pg_stat_io reports them as two independent rows rather than one combined number.

context: normal vs vacuum vs bulkread/bulkwrite

Within relation I/O, context distinguishes ordinary query access (normal) from VACUUM's own reads and writes (vacuum) from the ring-buffer-bounded bulk operations covered in Lab 2.4.6 (bulkread, bulkwrite) from a relation's very first extension (init). The same physical disk, four different reasons something touched it.

⏱️ Enable I/O Timing

Turn on track_io_timing for this session so pg_stat_io's timing columns are populated, not just its counts.

psql -U postgres -d beer_db -c "SET track_io_timing = on;" -c "SELECT current_setting('track_io_timing');"

student@lab:~$ psql -U postgres -d beer_db -c "SET track_io_timing = on;" -c "SELECT current_setting('track_io_timing');" SET SET current_setting ------------------ on (1 row)

🔄 Reset for a Clean Baseline

Reset the shared I/O statistics view before generating any workload.

psql -U postgres -d beer_db -c "SELECT pg_stat_reset_shared('io');"

student@lab:~$ psql -U postgres -d beer_db -c "SELECT pg_stat_reset_shared('io');" SET pg_stat_reset_shared ----------------------- (1 row)

✍️ Generate a Write Workload

Insert 50,000 rows, then check the I/O breakdown by object — relation data versus WAL are tracked separately.

psql -U postgres -d beer_db -c "INSERT INTO monitoring.io_demo (payload) SELECT repeat('z',100) FROM generate_series(1,50000) g;"
psql -U postgres -d beer_db -c "SELECT backend_type, object, context, reads, writes, extends, hits FROM pg_stat_io WHERE backend_type = 'client backend' AND (reads > 0 OR writes > 0 OR extends > 0) ORDER BY object, context;"

student@lab:~$ psql -U postgres -d beer_db -c "INSERT INTO monitoring.io_demo (payload) SELECT repeat('z',100) FROM generate_series(1,50000) g;" SET INSERT 0 50000 student@lab:~$ psql -U postgres -d beer_db -c "SELECT backend_type, object, context, reads, writes, extends, hits FROM pg_stat_io WHERE backend_type = 'client backend' AND (reads > 0 OR writes > 0 OR extends > 0) ORDER BY object, context;" SET backend_type | object | context | reads | writes | extends | hits ----------------+----------+---------+-------+--------+---------+-------- client backend | relation | normal | 116 | 0 | 950 | 202152 client backend | wal | normal | 0 | 3 | | (2 rows)

🧹 Run VACUUM and Check the Context Breakdown

VACUUM the table and see a new context appear in the relation I/O breakdown.

psql -U postgres -d beer_db -c "VACUUM monitoring.io_demo;"
psql -U postgres -d beer_db -c "SELECT backend_type, object, context, reads, writes, extends, hits FROM pg_stat_io WHERE backend_type = 'client backend' AND object = 'relation' ORDER BY context;"

student@lab:~$ psql -U postgres -d beer_db -c "VACUUM monitoring.io_demo;" SET VACUUM student@lab:~$ psql -U postgres -d beer_db -c "SELECT backend_type, object, context, reads, writes, extends, hits FROM pg_stat_io WHERE backend_type = 'client backend' AND object = 'relation' ORDER BY context;" SET backend_type | object | context | reads | writes | extends | hits ----------------+----------+-----------+-------+--------+---------+-------- client backend | relation | bulkread | 0 | 0 | | 0 client backend | relation | bulkwrite | 0 | 0 | 0 | 0 client backend | relation | init | 0 | 0 | 0 | 0 client backend | relation | normal | 150 | 0 | 951 | 203588 client backend | relation | vacuum | 0 | 0 | 0 | 834 (5 rows)

🔄 Reset Again — and See It Does Not Stay at Zero

Reset the I/O stats one more time, then immediately check whether anything is already non-zero again.

psql -U postgres -d beer_db -c "SELECT pg_stat_reset_shared('io');"
psql -U postgres -d beer_db -c "SELECT count(*) FROM pg_stat_io WHERE reads > 0 OR writes > 0;"

student@lab:~$ psql -U postgres -d beer_db -c "SELECT pg_stat_reset_shared('io');" SET pg_stat_reset_shared ----------------------- (1 row) student@lab:~$ psql -U postgres -d beer_db -c "SELECT count(*) FROM pg_stat_io WHERE reads > 0 OR writes > 0;" SET count ------- 3 (1 row)

Lab 2.4.7 complete — and Block 2.4 complete. You can now attribute disk I/O to the exact operation that caused it:\n\n\n track_io_timing : ✅ enabled at session level\n pg_stat_reset_shared('io') : ✅ clean measurement windows, twice\n relation vs wal : ✅ told apart on the same INSERT\n normal vs vacuum context : ✅ watched VACUUM's own footprint appear\n

Enable JavaScript to run the live terminal and track your progress.