Purge
Drop old versions as soon as no open view can need them, and see what a long reader holds back
Add a purge command that drops every committed undo record all open views can do without, takes out the rows and index entries those records had marked, and frees undo pages left empty; undo status shows the oldest view and how much purge could drop.
- An old version is needed only by views that do not see the change after it
- The oldest open view is the horizon: everything older than what it sees can go
- One long-running transaction holds back every cleanup after it
- Purge trims a log; it does not sweep the table
- Purge is not VACUUM: one drops versions, the other shortens the file
Why this stage exists
Since read views, every change leaves an old version behind, and a DELETE leaves a marked row. They go when a transaction ends and no view at all is open. On a busy database that moment may never come: as long as any reader is running, nothing is dropped, even versions that no running reader could ever ask for.
Look at what a version is for. The undo record of a change made by transaction t holds the row as it was before t. Only a view that does not see t's change can need it. Once every open view sees t, the record is dead weight.
The oldest view is the horizon
So the question is not "is any view open?" but "is any view old enough to need this?". The answer depends only on the oldest open view: whatever it sees, every newer view sees too.
undo status shows both halves: who holds the horizon, and how much is waiting behind it.
>undo status[UNDO] records=15 pages=0 oldest_view=16 purgeable=10>purge[PURGE] records=10 rows=0>undo status[UNDO] records=5 pages=0 oldest_view=16 purgeable=0
purge drops what is purgeable, takes the rows and index entries those records had marked out of their trees, and frees undo pages left empty. By itself, purge still runs only when a transaction ends with no view open, as before; the command is how a database with readers always open keeps up.
The long-running transaction
The lesson of this stage is a villain: the transaction that stays open. A report that runs for an hour keeps its view for an hour, and every version every other transaction makes in that hour waits behind it. Undo grows, marked rows keep their space, and every reader that walks a version chain walks further. Ending that one transaction lets all of it go.
Purge, PostgreSQL's VACUUM and ours
PostgreSQL solves the same problem the other way round. It keeps every version as a row in the table, so dead rows sit among live ones, and something must sweep the table: its VACUUM finds rows no snapshot can see and frees their space. InnoDB keeps old versions in undo and trims a log, which is what purge does here. Same problem, same villain: in both, one open transaction holds back every cleanup after it.
This engine's own VACUUM, from Memory and Space, is a third thing: it moves pages to make the file shorter, and has nothing to do with versions.
The algorithm in words
What is purgeable
Ask every open view.
- 1.Take each undo record of a committed transaction t.
- 2.It is purgeable when every open view sees t's change: no view could need the version it holds.
- 3.With no view open, every committed record is purgeable.
purge
Drop, take out, free.
- 1.Drop every purgeable record.
- 2.For each dropped record, take out what its change marked: a DELETE's row from its leaf, and the index entries a DELETE or an UPDATE marked, in the order they were marked, merging as a DELETE would.
- 3.Free every undo page left holding no record.
- 4.Log it as a commit: before purging, write the open transactions' undo records that are in no undo page yet; after, log every page changed since it was last logged, then a commit record.
- 5.Print [PURGE] records=<r> rows=<m>.
Output contract for this stage
These are the exact lines the tests check. The full definition is on the output contract page, section 90.
| Input | Output |
|---|---|
purge | [PURGE] records=<r> rows=<m>, m the marked table rows taken out |
purge x | [ERROR:00104] ... |
undo status | [UNDO] records=<n> pages=<p> oldest_view=<t> purgeable=<k> |
What you should see when done
$droid --db p.dbWelcome to droid!>create table t (id int primary key, v varchar(20));Create table statement executed successfully>insert into t (id, v) values (1, 'v1');Insert statement executed successfully (1 rows)>insert into t (id, v) values (2, 'v2');Insert statement executed successfully (1 rows)>@1 begin;@1| Begin statement executed successfully>@1 select count(*) from t;@1| COUNT(*)@1| 2@1| (1 rows)@1| [EXEC] rows_examined=2>@2 delete from t where id = 2;@2| Delete statement executed successfully (1 rows)@2| [EXEC] rows_examined=1>@3 begin;@3| Begin statement executed successfully>@3 select count(*) from t;@3| COUNT(*)@3| 1@3| (1 rows)@3| [EXEC] rows_examined=1>@1 commit;@1| Commit statement executed successfully>undo status[UNDO] records=1 pages=0 oldest_view=6 purgeable=1>purge[PURGE] records=1 rows=1>.exitexiting.. good bye!
Common bugs in this stage
- Purging what the oldest view needs. A record is purgeable only when every open view sees its change.
- Taking out a marked row whose delete is still needed. The row goes with its record, not before.
- Leaving index entries behind. Purge takes out every entry a dropped record's change marked: a deleted row's entries and an updated row's old ones.
- Leaving empty undo pages allocated. They go back to the free list.
Language notes
The page above is the same for every language. These are the only language-specific hints.
- C: keep committed records in commit order; the purgeable ones are a prefix of that list, so purge walks from the front and stops at the first one some view needs.
- C++: a
std::dequeof committed transactions with their records;pop_frontwhile the oldest view sees the front one. - Rust: a
VecDequeof committed transactions;while let Some(t) = q.front()with the view check. - Zig: an
ArrayListused as a queue with a head index, compacted after each purge.
Checklist
One line per thing the tests check. The tags name the test cases.
- purge prints what it dropped; with nothing purgeable it drops nothingpurge-format
- The oldest view holds back purge, and purge drops what newer views do not needlong-reader-blocks-purge
- purge takes marked rows out of their leaf and the space comes backmarked-rows-removed
- purge frees undo pages it leaves emptyundo-pages-freed
- purge takes marked index entries out with their rowsmarked-index-entries-removed
- A crash after a purge recovers the same rowscrash-after-purge