Online VACUUM, REINDEX, and Table Rewrites
Over time tables and indexes accumulate bloat — dead tuples and empty space left by MVCC. There are two fundamentally different ways to deal with it: reclaiming space for reuse within the object (online, cheap) versus physically shrinking the object and returning space to the OS (expensive, traditionally blocking). Choosing the wrong one causes outages.
The one thing to understand first
Over time tables and indexes accumulate bloat — dead tuples and empty space left by MVCC. There are two fundamentally different ways to deal with it: reclaiming space for reuse within the object (online, cheap) versus physically shrinking the object and returning space to the OS (expensive, traditionally blocking). Choosing the wrong one causes outages.
*Routine VACUUM reclaims space for reuse without shrinking the file; returning space to the OS is a separate, far more expensive operation — and there is an online tool for every job, so you should never need the blocking one.* Match the tool to the goal and bloat cleanup is invisible to users.
Plain VACUUM: online, but doesn't shrink files
Regular VACUUM (and autovacuum) marks dead tuple space reusable and updates the free space and visibility maps. It runs with a weak lock that allows concurrent reads and writes. What it does not do is return space to the operating system — the file stays the same size, with free space inside it for future rows. For steady-state churn this is exactly what you want, and it should be handled automatically by a well-tuned autovacuum.
VACUUM FULL: shrinks, but locks everything
VACUUM FULL rewrites the entire table into a new compact file and swaps it in, returning space to the OS. The catch: it holds an ACCESS EXCLUSIVE lock for the whole rewrite — no reads, no writes — and needs disk space for a full second copy. On a large production table this is an outage. Reserve it for cases where you must reclaim disk and can afford a maintenance window.
REINDEX CONCURRENTLY: online index rebuild
Index bloat is reclaimed by rebuilding the index. The online form builds a fresh copy alongside the old one under a weak lock and swaps them:
REINDEX INDEX CONCURRENTLY idx_orders_customer;
REINDEX TABLE CONCURRENTLY orders; -- every index on the tableThis is the standard, safe remedy for index bloat. Like CIC, a failure can leave an INVALID index to drop, and it cannot run inside a transaction block.
pg_repack: online table compaction
To compact a table (not just its indexes) without the lock of VACUUM FULL, the pg_repack extension is the standard tool. It creates a shadow copy of the table, uses triggers to capture changes during the copy, builds the indexes, and swaps the new table in — holding the strong lock only for the brief final swap. The result is a fully compacted table with seconds of locking instead of minutes or hours:
-- Compact a bloated table online
pg_repack -d app --table orders
-- Reads and writes continue throughout; only a momentary lock at swap.Layer 3 — Watch it happen on your own database
Do not rebuild blindly — measure. The pgstattuple extension reports actual dead space:
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('orders'); -- table bloat: dead_tuple_percent
SELECT * FROM pgstatindex('idx_orders_pk'); -- index bloat: leaf_fragmentation
-- Cross-check the file size before/after a rebuild to prove space returned:
SELECT pg_size_pretty(pg_total_relation_size('orders'));The number that decides everything is dead_tuple_percent (tables) or leaf_fragmentation (indexes). A high value justifies a rebuild; a low one means autovacuum is already keeping up and a VACUUM FULL would be a pointless outage. Re-check pg_total_relation_size after a pg_repack/REINDEX CONCURRENTLY to confirm space actually returned to the OS.
Layer 4 — The levers this hands you
There is an online tool for every cleanup goal — match the tool to the job:
- Prevention beats cure. Tune autovacuum per-table on high-churn tables so bloat never accumulates in the first place.
- Eliminate long transactions that hold back the vacuum horizon and let dead tuples pile up.
- **Use appropriate
fillfactor** to enable HOT updates and cut index bloat at the source. - **Avoid
VACUUM FULLin production** — its ACCESS EXCLUSIVE lock and need for a full second copy make it an outage; reach forpg_repackinstead.
Layer 5 — What an Oracle DBA should expect vs what they get
Bloat is the cost of PostgreSQL's MVCC model, and the tooling differs sharply from Oracle:
- Bloat itself is more visible. Oracle's in-place updates + UNDO mean tables don't accumulate dead row versions the way Postgres heaps do — managing bloat is a uniquely Postgres discipline with no exact Oracle counterpart.
- **
VACUUM FULL≈ALTER TABLE ... MOVE/ online redefinition,** but the plain form is fully blocking. The online equivalent of Oracle's segment shrink (ALTER TABLE ... SHRINK SPACE) ispg_repack— an external extension, not built in. - **
REINDEX CONCURRENTLY≈ALTER INDEX ... REBUILD ONLINE,** with the same INVALID-on-failure caveat as CIC. - **Routine
VACUUMhas no real Oracle analogue** — there is no background process Oracle DBAs tune to reclaim dead-tuple space, because UNDO handles old versions differently. Autovacuum tuning is the new core skill.
Key takeaway
Routine VACUUM/autovacuum reclaims dead space online for reuse but does not shrink files; to return space to the OS use REINDEX CONCURRENTLY for indexes and pg_repack for tables — both online — and reserve the blocking VACUUM FULL for a maintenance window. Always measure first with pgstattuple, and prevent bloat at the source by tuning autovacuum, killing long transactions, and using fillfactor for HOT updates.
Interview lens
How this shows up in interviews
What an interviewer is actually testing, the same mechanism seen from three roles, and the gap between a shallow answer and one that shows real depth.
What the interviewer is testing
Whether you distinguish reclaim-in-place (plain VACUUM, cheap) from shrink-and-return-to-OS (VACUUM FULL/rewrite, blocking) and choose correctly.
Questions you should be able to answer
- ▸Does plain VACUUM return disk space to the OS? If not, what does?
- ▸VACUUM vs VACUUM FULL vs pg_repack — the tradeoffs?
- ▸How do you rebuild a bloated index without downtime?
Database engineer
You use plain VACUUM for reuse, REINDEX CONCURRENTLY for index bloat, and pg_repack (not VACUUM FULL) when you genuinely must shrink online.
Software engineer
You never casually run VACUUM FULL on a live hot table — it takes ACCESS EXCLUSIVE and rewrites the whole relation.
Feature builder
You prefer designs that avoid mass churn so a rewrite is rarely needed at all.
Shallow answer
'VACUUM FULL reclaims space, so run it to shrink.' Ignores that it's a blocking full rewrite.
Answer that shows depth
Plain VACUUM marks dead-tuple space reusable within the object but usually neither shrinks the file nor returns space to the OS. Returning space requires a rewrite: VACUUM FULL and CLUSTER take ACCESS EXCLUSIVE and rebuild the relation — an outage on hot tables — while pg_repack performs an online rebuild using triggers plus a final swap. Index bloat is addressed with REINDEX CONCURRENTLY. Choosing a blocking rewrite when in-place reuse would have sufficed is a classic self-inflicted outage.
Pro lab notebook
The captured run behind every query
Every SQL query above is real and the source citations are verified. Pro unlocks the captured psql output — each query re-run against a real PostgreSQL 17.10 lab, with its verbatim result.
1 real lab transcript for this lesson
The explanation and the SQL above are free — so are the verified source citations. What Pro unlocks is the proof: every query in Online VACUUM, REINDEX, and Table Rewrites re-run against a real PostgreSQL 17.10 instance, with its verbatim psql output captured line for line.
What Pro unlocks here
- Every query in this lesson re-run against a real PostgreSQL 17.10 lab
- The verbatim psql output — page headers, row versions, plan nodes, catalog rows
- The corrected form of any query the source lesson got wrong, re-captured
- A copy-ready notebook you can replay yourself to confirm every claim
How this was verified
This explanation is anchored to PostgreSQL's source tree — the files linked above, on the REL_17_STABLE branch — and cross-checked against the official documentation for versions 15 through 18. Every SQL query shown was executed and its output captured in a throwaway PostgreSQL 17.10 lab; where a query needed correcting to run, that correction is noted inline.
Official documentation: §25.1 Routine Reindexing