By John Doe June 18, 2026
Summary: In this article, we will explore the impact of long transactions on the performance of hot row updates in PostgreSQL.
Table of Contents
Hot Row Update Scenarios
Hot rows refer to data rows in a database that undergo frequent modification operations. Under high-concurrency scenarios, updating hot rows generates excessively long data row version chains in PostgreSQL tables, severely impacting system performance.
In scenarios such as major e-commerce promotions and high-frequency financial trading, the same record (e.g., account balance, product inventory) is rapidly and repeatedly modified. This generates an ultra-long data row version chain, causing a massive number of dead tuples to quickly accumulate in tables and indexes. This leads to a drastic bloat in file size, affecting subsequent update and query performance. If the execution time of an update operation increases, it will have a significant negative impact on the business level.
To address these types of problems, simply upgrading computer hardware configurations can no longer meet such low-latency demands. Below, we will construct a test case for this scenario to conduct testing and analysis.
Preparing the Base Data
Adjust the database configuration parameters (modify the postgresql.conf configuration file):
shared_buffers = 1GB
max_wal_size = 4GB
synchronous_commit = off
Note: We set the
synchronous_commitparameter tooffhere to simulate a business workload supported by high-speed storage devices. It is not recommended to do this in an actual production environment.
Next, in a PostgreSQL 18.4 database, we will create a table and insert 100 rows of data.
CREATE TABLE pgbench_accounts (
aid int PRIMARY KEY,
bid int,
filler char(128),
abalance int
);
INSERT INTO pgbench_accounts(aid, bid, filler, abalance)
SELECT aid, 1, '', 0
FROM generate_series(1, 100) as aid;
Create a new test.sql file with a custom SQL script to define the business workload for frequently updating hot rows:
-- test.sql
UPDATE pgbench_accounts SET bid = bid + 1 WHERE aid = 100;
After preparing the base table and initial data, let’s start a session, begin a transaction within that session, and keep it in an idle state:
BEGIN ISOLATION LEVEL REPEATABLE READ;
SELECT filler FROM pgbench_accounts WHERE aid = 1;
Next, we will test the performance of frequently updating hot rows under two scenarios: with and without the idle transaction started above.
Running Benchmark Pressure Test
Run the test using pgbench combined with the -f parameter to specify the custom script:
pgbench --no-vacuum -f test.sql --time=600 --client=8 --jobs=8
pgbench (18.4)
transaction type: test.sql
scaling factor: 1
query mode: simple
number of clients: 8
number of threads: 8
maximum number of tries: 1
duration: 600 s
number of transactions actually processed: 300144
number of failed transactions: 0 (0.000%)
latency average = 15.993 ms
initial connection time = 5.147 ms
tps = 500.216495 (without initial connection time)
Finally, we commit and end the idle transaction from earlier.
COMMIT;
Test Results Comparison
Summarizing and comparing the test results above, we have:
| Test Scenario | PostgreSQL | Redrock Postgres | Improvement Rate |
|---|---|---|---|
| With Long Transaction | 500.216 tps | 21429.916 tps | 4184% |
| Without Long Transaction | 14519.995 tps | 22835.284 tps | 57.3% |
The presence of long transactions has a massive impact on the performance of PostgreSQL’s hot row updates. For Redrock Postgres, however, it is generally able to maintain stable performance regardless.
Conclusion
In PostgreSQL, when UPDATE operations are frequently executed on the same row of data in the presence of long transactions, performance will continuously degrade as the number of updates increases.
This phenomenon is an inevitable result of PostgreSQL’s MVCC (Multi-Version Concurrency Control) mechanism:
- PostgreSQL’s
UPDATEis not an in-place update: EachUPDATEexecution generates a completely new row version, merely marking the old version as invalid rather than overwriting it directly. - In the presence of a long transaction, all historical row versions generated by updates can neither be reclaimed by background processes nor skipped by the update logic. Every new
UPDATErequires traversing the row’s complete version chain from start to finish to locate the latest version visible to the current transaction before writing can be executed. The cost of a single update increases linearly with the number of versions, resulting in an overall operation time complexity of $\mathcal{O}(N^2)$, which ultimately manifests as a continuous decline in performance.
In contrast, Redrock Postgres maintains consistently stable update performance because it introduces an undo log, allowing for in-place updates when modifying tuples.