5 Page Pruning and HOT Updates
PostgreSQL 14 · Translated from Russian by Liudmila Mantrova · Chinese translation by Xiong Cancan
5.1 Page Pruning
While a heap page is being read or updated, PostgreSQL can perform some quick page cleanup, or pruning.1 It happens in the following cases:
The previous
UPDATEoperation did not find enough space to place a new tuple into the same page. This event is reflected in the page header.The heap page contains more data than allowed by the fillfactor storage parameter. (default:
100)
An INSERT operation can add a new row into the page only if this page is filled for less than fillfactor percent. The rest of the space is kept for UPDATE operations (no such space is reserved by default).
Page pruning removes the tuples that cannot be visible in any snapshot anymore (that is, that are beyond the database horizon). It never goes beyond a single heap page, but in return it is performed very fast. Pointers to pruned tuples remain in place since they may be referenced from an index—which is already a different page. (p. 88)
For the same reason, neither the visibility map nor the free space map is refreshed (so the recovered space is set aside for updates, not for insertions).
Since a page can be pruned during reads, any SELECT statement can cause page modifications. This is yet another such case in addition to deferred setting of information bits. (p. 71)
Let’s take a look at how page pruning actually works. We are going to create a two-column table and build an index on each of the columns:
If the s column contains only Latin letters, each heap tuple will have a fixed size of 2004 bytes, plus 24 bytes of the header. The fillfactor storage parameter is set to 75 %. It means that the page has enough free space for four tuples, but we can insert only three.
Let’s insert a new row and update it several times:
Now the page contains four tuples:
Expectedly, we have just exceeded the fillfactor threshold. You can tell it by the difference between the pagesize and upper values—it is bigger than 75 % of the page size, which is 6144 bytes: (p. 62)
The next page access triggers page pruning that removes all the outdated tuples. Then a new tuple (0,5) is added into the freed space:
The remaining heap tuples are physically moved towards the highest addresses so that all the free space is aggregated into a single continuous chunk. The tuple pointers are also modified accordingly. As a result, there is no free space fragmentation in the page.
The pointers to the pruned tuples cannot be removed yet because they are still referenced from the indexes; PostgreSQL changes their status from normal to dead. Let’s take a look at the first page of the hot_s index (the zero page is used for metadata):
We can see the same picture in the other index too:
An index scan can return (0,1), (0,2), and (0,3) as tuple identifiers. The server tries to read the corresponding heap tuple but sees that the pointer has the dead status; it means that this tuple does not exist anymore and should be ignored. And while being at it, the server also changes the pointer status in the index page to avoid repeated heap page access.2
Let’s extend the function displaying index pages so that it also shows whether the pointer is dead: (v. 13)
All the pointers in the index page are active so far. But as soon as the first index scan occurs, their status changes:
Although the heap tuple referenced by the fourth pointer is still unpruned and has the normal status, it is already beyond the database horizon. That’s why this pointer is also marked as dead in the index.
5.2 HOT Updates
It would be very inefficient to keep references to all heap tuples in an index.
To begin with, each row modification triggers updates of all the indexes created on the table: once a new heap tuple appears, each index must include a reference to this tuple, even if the modified fields are not indexed.
Furthermore, indexes accumulate references to historic heap tuples, so they have to be pruned together with these tuples. (p. 102)
Things get worse as you create more indexes on a table.
But if the updated column is not a part of any index, there is no point in creating another index entry that contains the same key value. To avoid such redundancies, PostgreSQL provides an optimization called Heap-Only Tuple updates.3
If such an update is performed, an index page contains only one entry for each row. This entry points to the very first row version; all the subsequent versions located in the same page are bound into a chain by ctid pointers in the tuple headers.
Row versions that are not referenced from any index are tagged with the Heap-Only Tuple bit. If a version is included into the HOT chain, it is tagged with the Heap Hot Updated bit.
If an index scan accesses a heap page and finds a row version marked as Heap Hot Updated, it means that the scan should continue, so it goes further along the chain of HOT updates. Obviously, all the fetched row versions are checked for visibility before the result is returned to the client.
To take a look at how HOT updates are performed, let’s delete one of the indexes and truncate the table.
For convenience, we will redefine the heap_page function so that its output includes three more fields: ctid and the two bits related to HOT updates:
Let’s repeat the insert and update operations:
The page now contains a chain of HOT updates:
The
Heap Hot Updatedbit shows that the executor should follow the CTID chain.The
Heap Only Tuplebit indicates that this tuple is not referenced from any indexes.
As we make further updates, the chain will grow—but only within the page limits:
The index still contains only one reference, which points to the head of this chain:
A HOT update is possible if the modified fields are not a part of any index. Otherwise, some of the indexes would contain a reference to a heap tuple that appears in the middle of the chain, which contradicts the idea of this optimization. Since a HOT chain can grow only within a single page, traversing the whole chain never requires access to other pages and thus does not hamper performance.
5.3 Page Pruning for HOT Updates
A special case of page pruning—which is nevertheless important—is pruning of HOT update chains.
In the example above, the fillfactor threshold is already exceeded, so the next update should trigger page pruning. But this time the page contains a chain of HOT updates. The head of this chain must always remain in its place since it is referenced from the index, but other pointers can be released because they are sure to have no external references.
To avoid moving the head, PostgreSQL uses dual addressing: the pointer referenced from the index (which is (0,1) in this case) receives the redirect status since it points to the tuple that currently starts the chain:
The tuples (0,1), (0,2), and (0,3) have been pruned; the head pointer 1 remains for redirection purposes, while pointers 2 and 3 have been deallocated (received the unused status) since they are guaranteed to have no references from indexes. The new tuple is written into the freed space as tuple (0,2).
Let’s perform some more updates:
The next update is going to trigger page pruning:
Again, some of the tuples are pruned, and the pointer to the head of the chain is shifted accordingly.
If unindexed columns are modified frequently, it makes sense to reduce the fillfactor value, thus reserving some space in the page for updates. Obviously, you have to keep in mind that the lower the fillfactor value is, the more free space is left in the page, so the physical size of the table grows.
5.4 HOT Chain Splits
If the page has no more space to accommodate a new tuple, the chain will be cut off. PostgreSQL will have to add a separate index entry to refer to the tuple located in another page.
To observe this situation, let’s start a concurrent transaction with a snapshot that blocks page pruning:
Now we are going to perform some updates in the first session:
When the next update happens, this page will not be able to accommodate another tuple, and page pruning will not manage to free any space:
Tuple (0,5) contains the (1,1) reference that goes to page 1:
However, this reference is not used: the Heap Hot Updated bit is not set for tuple (0,5). As for tuple (1,1), it can be accessed from the index that now has two entries. Each of them points to the head of their own HOT chain:
5.5 Page Pruning for Indexes
I have declared that page pruning is confined to a single heap page and does not affect indexes. However, indexes have their own pruning,4 which also cleans up a single page— an index one in this case.
Index pruning happens when an insertion into a B-tree is about to split the page into two, as the original page does not have enough space anymore. The problem is that even if some index entries are deleted later, two separate index pages will not be merged into one. It leads to index bloating, and once bloated, the index cannot shrink even if a large part of the data is deleted. But if pruning can remove some of the tuples, a page split may be deferred.
There are two types of tuples that can be pruned from an index.
First of all, PostgreSQL prunes those tuples that have been tagged as dead.5 As I have already said, PostgreSQL sets such a tag during an index scan if it detects an index entry pointing to a tuple that is not visible in any snapshot anymore or simply does not exist.
If no tuples are known to be dead, PostgreSQL checks those index entries that reference different versions of the same table row.6 Because of MVCC, update operations may generate a large number of row versions, and many of them are soon likely to disappear behind the database horizon. HOT updates cushion this effect, but they are not always applicable: if the column to update is a part of an index, the corresponding references are propagated to all the indexes. Before splitting the page, it makes sense to search for the rows that are not tagged as dead yet but can already be pruned. To achieve this, PostgreSQL has to check visibility of heap tuples. Such checks require table access, so they are performed only for “promising” index tuples, which have been created as copies of the existing ones for MVCC purposes. It is cheaper to perform such a check than to allow an extra page split. (v. 14)
backend/access/heap/pruneheap.c, heap_page_prune_opt function ↩︎
backend/access/index/indexam.c, index_fetch_heap function ↩︎
postgresql.org/docs/14/btree-implementation.html#BTREE-DELETION ↩︎
backend/access/nbtree/README, Simple deletion section ↩︎
backend/access/nbtree/README, Bottom-Up deletion section include/access/tableam.h ↩︎