Manual VACUUM in Postgres
Introduction
- As mentioned in the previous article, we explored
AutovacuumandAutoanalyze, but they have limitations in that they do not automatically run after changing just a few rows of data, requiring a specific threshold to be met. - Causes
- Because operations to check for
Dead Tuplesand update statistics data require significant processing costs, handling it this way helps Postgres saveCPUandDisk I/O. - If data changes have not reached the threshold, meaning the volume of changes is insignificant compared to the entire table, the
Query Plannercan still provide an effective solution.
- Because operations to check for
Why Manual VACUUM Is Needed
When operating in production, many factors can prevent Autovacuum from working effectively, including the following causes:
Long-Running Transactions- This is the most common cause because the
MVCCmechanism of Postgres operates on the principle thatVacuumcannot reclaim anyDead Tupleif it remains visible to an active Transaction. - Specific scenario:
- If you start a
Transaction (Tx1)with an isolation level ofRepeatable Read/Serializable, it will operate with the current data snapshot. - When another
Transactionexecutes anUPDATE/DELETEon data and successfully commits, it generatesDead Tuples. - However, within
Tx1, you can stillSELECTrow data from before thatUPDATE/DELETE, even though those rows are nowDead Tuples. - Consequently, running
VACUUMwill not reclaimDead Tuplessnapshot byTx1, as that data might still be used byTx1.
- If you start a
- This is the most common cause because the
- Database Configuration Falling Behind the
Write Rate- The default configuration of
Autovacuumin Postgres is designed safely to avoid overloading systemCPU/Disk I/O. - For
High-throughputsystems with highUPDATE/DELETEfrequencies, this configuration is too slow. - Rate-limited cleanup (
Throttling): Postgres uses the parameterautovacuum_vacuum_cost_limit. WhenAutovacuumreaches this cost threshold, it is forced to pause for a duration defined byautovacuum_vacuum_cost_delay. - As a result, the rate at which the application produces
Dead Tuples(such as 10,000 rows/sec) is significantly faster than the rate at whichAutovacuumcleans them (such as 1,000 rows/sec).
- The default configuration of
- Default Scale Factor Formula Unsuited for Ultra-Large Tables
- The default threshold to trigger
Autovacuumon a table is:{Threshold} = 50 + (0.2 x {Total Rows}). - For a table with 1,000 rows, it requires 250 Dead Tuples for
Autovacuumto run immediately, which is highly effective. - For a table with 50 million rows,
{Threshold} = 50 + (0.2 x 50,000,000) = 10,000,050 Dead Tuples.- The result is that the table must accumulate over 10 million Dead Tuples before
Autovacuumtriggers. - Throughout the accumulation of these 10 million rows, the system suffers severe performance degradation from consuming storage and reading useless
Dead Tuples.
- The result is that the table must accumulate over 10 million Dead Tuples before
- The default threshold to trigger
These causes lead to Dead Tuples accumulating over time to excessive levels without being reclaimed, resulting in severe Table Bloat.
Manual Execution Commands
When excessive Dead Tuples accumulate without cleanup, it leads to Table Bloat. You must proactively run commands to clean up and update statistics manually:
ANALYZE table_name: Updates statistics so the Planner has sufficient information to execute queries efficiently.VACUUM table_name: Checks for Dead Tuples no longer used anywhere, then updates theFree Space Mapto allow subsequentINSERTrows to override thoseDead Tuples.- Running this command does not reduce disk volume immediately, as the disk allocation remains unchanged.
- If you wonder why
INSERToperations do not automatically overwriteDead Tupleswithout running this command first:- During
INSERT, Postgres does not inspect every row to verify if it is aDead Tupleto overwrite, as that would severely impact performance. - This command processes
Dead Tuplesin theHeapandIndex Entry, removing theTIDpointing from theIndexto theHeapsoIndexscans avoid retrievingDead Tuples. Dead Tuplesin theHeapare logged in theFree Space Mapfor futureINSERToverrides.- Unused
Indexentries can then be merged as nodes in theB-Tree.
- During
VACUUM ANALYZE table_name: Combines both operations above.VACUUM (INDEX_CLEANUP ON) table_name: The explicit form ofVACUUM table_name, cleaning upDead Tuplesin bothHeapandIndex Entries.VACUUM (INDEX_CLEANUP OFF) table_name: Skips index cleanup to run faster.- Useful when dataset size is extremely large and you want to clean
Dead Tuplesfirst. - Or when you plan to
ReIndexlater, making index cleanup redundant.
- Useful when dataset size is extremely large and you want to clean
VACUUM FULL table_name: Reconstructs the table entirely.- Creates a brand new table file on disk, copies all
Live Tuplesto the new file and completely deletes the old file. - Benefit: Reclaims 100% of unused disk space back to the OS.
- Drawback: Takes an
Exclusive Lockon the table, blocking all queries.
- Creates a brand new table file on disk, copies all
VACUUM FREEZE table_name: Freezes row data to preventTransaction ID Wraparound.- When modifying data, the
xminof a row records theXIDof the activeTransaction. However,XIDis limited to a32-bitinteger (maximum2^32), wrapping around to zero when exceeded. - At this point, Postgres cannot determine whether a newly created
XID(smaller number) comes before or after an olderXID(larger number) to display data (since default visibility checks requirexmin=XIDto be smaller than the currentXID). - Freezing data sets the
HEAPTUPLE_XMIN_FROZENflag on old rows so Postgres treats them as older than any activeXIDduringSELECTqueries. - Two types of Freeze operations:
Lazy Freeze: Runs alongsideAutovacuum, freezing rows older than thevacuum_freeze_min_agesetting.Full Table Freeze: Executed manually viaVACUUM FREEZE table_name, scanning the entire table to freeze allLive Tuples, which runs slower thanLazy Freeze.
- When modifying data, the
VACUUM (VERBOSE) table_name: Outputs detailed execution logs (number ofDead Tuplesremoved, pages freed, etc.). Used for debugging and monitoring.
Detail
VACUUM config
To view VACUUM configuration details, run:
SELECT
name,
setting,
unit,
short_desc
FROM pg_settings
WHERE name IN (
'autovacuum_vacuum_cost_limit',
'autovacuum_vacuum_cost_delay',
'vacuum_cost_page_hit',
'vacuum_cost_page_miss',
'vacuum_cost_page_dirty'
);
SHOW vacuum_cost_limit;
vacuum_cost_page_hit: Default = 1 point, finding a data page directly in RAM (Shared Buffers).vacuum_cost_page_miss: Default = 2 points, reading a data page from disk.vacuum_cost_page_dirty: Default = 20 points, modifying a data page and writing it back to disk.autovacuum_vacuum_cost_delay: Whenever theAutovacuumprocess reaches the cost limit, it pauses for2msbefore continuing cleanup.- This is the default PostgreSQL setting, where
2msenables reasonably fast execution and reduces bloat accumulation.
- This is the default PostgreSQL setting, where
autovacuum_vacuum_cost_limit: The maximum I/O cost limit (resource contention threshold) thatVACUUM(orAUTOVACUUM) can consume in a single cycle before pausing.- This acts as a throttle, preventing cleanup processes from exhausting Disk I/O and slowing down active application traffic.
- If the value shows
-1, it inherits the Global configuration, which can be verified using theSHOWcommand. - During execution, PostgreSQL accumulates cost points:
{vacuum_cost_page_hit + vacuum_cost_page_miss + vacuum_cost_page_dirty = Total Accumulated Points} >= {autovacuum_vacuum_cost_limit}.- Once points reach the
vacuum_cost_limit(such as 200 points): - The
VACUUMprocess pauses for the duration specified byvacuum_cost_delay(such as 2 ms). - After the delay, accumulated points reset to
0andVACUUMresumes.
- Once points reach the
- Configured impacts:
- A LOW value (such as 200) cleans slowly and takes longer, but remains safe with minimal impact on application performance.
- A HIGH value (such as 1000 - 2000) cleans quickly and reduces Table Bloat, but demands high I/O and may cause latency on slower disks.
Long-Running Transactions
-- Tx1
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT * FROM test
-- Statement 2
DELETE FROM test
This example illustrates why Autovacuum fails to proceed:
- You execute
Tx1, followed byStatement 2. Statement 2deletes all rows in thetesttable.- Because
Tx1usesTRANSACTION ISOLATION LEVEL REPEATABLE READ, it continues to read data as it existed before deletion. These rows becomeDead TuplesandVACUUMcannot remove them. - Checking the table state shows
Dead Tuplesat100%becauseAutovacuumcannot run:
- You must identify the blocking
Transactionand perform aCOMMIT/ROLLBACK. Running manual Vacuum commands will not reclaim dead space while the transaction remains active. - Running
VACUUMwith an open transaction yields no reclaimedDead Tuples:
Closing the Transaction allows Dead Tuples to be cleaned up successfully:
Manual VACUUM
-- Statement 1
DELETE FROM test WHERE col_smallint = 1
-- Query 2
SELECT count(*) FROM test
SELECT reltuples AS estimated_row_count FROM pg_class WHERE relname = 'test'
-- Query 3
SELECT
relname AS table_name,
n_live_tup AS live_tuples,
n_dead_tup AS dead_tuples,
ROUND(
(n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0)) * 100, 2
) AS dead_pct_ratio,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
WHERE relname = 'test';
-- Statement 4
ANALYZE test
VACUUM test
VACUUM ANALYZE test
VACUUM (INDEX_CLEANUP ON) test
VACUUM (INDEX_CLEANUP OFF) test
VACUUM FREEZE test
VACUUM FULL test
VACUUM (VERBOSE) test
- Execute Statement 1 to
DELETErow data, generatingDead Tuples. - Run Query 2 to check remaining rows, observing the discrepancy between actual row counts and the estimate in
pg_class. - Because
ANALYZEhas not run, statistics inpg_classremain unupdated, differing from exactCOUNTresults.
Query 3 displays generated Dead Tuples that remain uncollected due to missing the threshold:
Statement 4 provides commands to trigger ANALYZE and VACUUM, or a combined command to analyze and clean up simultaneously:
Happy coding!
Comments
Post a Comment