I recently commented on Jonathan Lewis’s blog, Savepoint Funny, where I compared how PostgreSQL handles uniqueness differently: “PostgreSQL resolves uniqueness through heap tuple visibility". This deserves a more detailed explanation.
In Oracle, unique indexes store unique entries because the B-tree key is the index key, preventing duplicates. Non-unique indexes add the ROWID to ensure that all entries are physically unique, even when indexed column values are duplicated.
In PostgreSQL, all indexes, even unique ones, created explicitly by CREATE UNIQUE INDEX or implicitly to enforce a unique constraint, behave like non-unique indexes by appending the TID (tuple ID, similar to Oracle's ROWID) to the index key. This indicates that the index itself doesn't guarantee physical uniqueness, allowing multiple entries to have identical logical keys but point to different heap tuples. The actual uniqueness verification occurs at the heap level, not within the index entries.
Initially, this might seem unusual—a unique index that permits duplicates. However, PostgreSQL requires this because of its MVCC system. MVCC allows duplicate entries to coexist in an index, since they can represent different versions of the same logical row. Still, PostgreSQL must guarantee that no MVCC snapshot views two rows with the same index key. Oracle doesn't face this issue because its MVCC implementation also versions index blocks, allowing a single index version to maintain unique keys.
Let’s show that.
Page inspect
In PostgreSQL, the heap contains the table data, and index entries point to heap tuples. Visibility depends on the heap header, especially the transaction information. Index scans often visit the heap pages to check visibility, except for index-only scans, which use the heap's visibility maps as an optimization. B-tree indexes can store entries for multiple versions of the same logical row, including versions that are no longer visible to current snapshots. To ensure uniqueness, the B-tree must check the heap for matching keys to verify whether multiple entries point to visible heap tuples in the same MVCC snapshot.
I used pageinspect to look at heap and index pages. This is not application-level SQL. This is a physical page inspection, useful for debugging and understanding internals. I used it to see all tuple versions, including those not visible to my MVCC snapshot. In some ways, you can compare it to Oracle flashback query, which shows all versions of a row.
CREATE EXTENSION IF NOT EXISTS pageinspect;
DROP TABLE IF EXISTS demo_unique_mvcc;
CREATE TABLE demo_unique_mvcc
(
id bigint generated always as identity,
email text not null,
payload text not null,
marker int not null,
CONSTRAINT demo_unique_mvcc_email_key UNIQUE (email)
) WITH ( FILLFACTOR = 10 );
CREATE INDEX demo_unique_mvcc_marker_idx
ON demo_unique_mvcc(marker);
ALTER TABLE demo_unique_mvcc SET (autovacuum_enabled = false);
I disabled auto-vacuum to avoid garbage collection during my experimentation.
Unique index
I created a unique constraint on email:
postgres=# \d demo_unique_mvcc
Table "public.demo_unique_mvcc"
Column | Type | Collation | Nullable | Default
---------+---------+-----------+----------+------------------------------
id | bigint | | not null | generated always as identity
email | text | | not null |
payload | text | | not null |
marker | integer | | not null |
Indexes:
"demo_unique_mvcc_email_key" UNIQUE CONSTRAINT, btree (email)
"demo_unique_mvcc_marker_idx" btree (marker)
postgres=#
I also created another index on marker. This is deliberate. If I update only a non-indexed column, PostgreSQL may use HOT updates, so no new entries are needed in the indexes. I want a non-HOT update to show the general case, so I update an indexed column, marker, while keeping the same email. To prove my point, I've set FILLFACTOR to 10% so that HOT updates are possible.
I inserted one row:
postgres=# INSERT INTO demo_unique_mvcc(email, payload, marker)
VALUES ('a@example.com', 'first version', 1)
;
INSERT 0 1
postgres=# SELECT ctid, xmin, xmax, *
FROM demo_unique_mvcc
;
ctid | xmin | xmax | id | email | payload | marker
-------+------+------+----+---------------+---------------+--------
(0,1) | 697 | 0 | 1 | a@example.com | first version | 1
(1 row)
The precise xmin may vary as it's your transaction identifier. What's crucial is the ctid. In this case, the visible row version is (0,1), which is the first tuple on the first page.
Now let’s look at the heap page:
postgres=# SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('demo_unique_mvcc', 0))
ORDER BY lp
;
lp | t_xmin | t_xmax | t_ctid | t_infomask | t_infomask2
----+--------+--------+--------+------------+-------------
1 | 697 | 0 | (0,1) | 2306 | 4
(1 row)
This tuple was inserted by committed transaction 697, has not been deleted or updated (xmax is invalid), contains at least one variable-length column, and stores 4 attributes.
I inspect the unique index. For a B-tree index, block 0 is the metapage, so the first leaf page is usually block 1 for a tiny index:
postgres=# SELECT itemoffset, ctid, htid, dead,
data, encode(decode(replace(substr(data,4),' ',''), 'hex'),'escape')
FROM bt_page_items('demo_unique_mvcc_email_key', 1)
ORDER BY itemoffset
;
itemoffset | ctid | htid | dead | data | encode
------------+-------+-------+------+-------------------------------------------------+-----------------------
1 | (0,1) | (0,1) | f | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
(1 row)
One visible row. One index entry in block 1 of the index (ctid) addressing block 1 of the heap table (htid). Nothing surprising yet.
Multiple updates
I updated the row two times, but I did not change the unique key:
postgres=# UPDATE demo_unique_mvcc
SET payload = 'second version', marker = 2
WHERE email = 'a@example.com'
;
UPDATE 1
postgres=# UPDATE demo_unique_mvcc
SET payload = 'third version', marker = 3
WHERE email = 'a@example.com'
;
UPDATE 1
The email value did not change. Logically, there is still one row with email = 'a@example.com':
postgres=# SELECT ctid, xmin, xmax, *
FROM demo_unique_mvcc
;
ctid | xmin | xmax | id | email | payload | marker
-------+------+------+----+---------------+---------------+--------
(0,3) | 711 | 0 | 1 | a@example.com | third version | 3
(1 row)
Only one row is visible to my SQL query.
But the heap page tells a different physical story:
postgres=# SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('demo_unique_mvcc', 0))
ORDER BY lp
;
lp | t_xmin | t_xmax | t_ctid | t_infomask | t_infomask2
----+--------+--------+--------+------------+-------------
1 | 697 | 710 | (0,2) | 1282 | 4
2 | 710 | 711 | (0,3) | 9474 | 4
3 | 711 | 0 | (0,3) | 10498 | 4
(3 rows)
There are three heap tuple versions. The first is no longer current and has been superseded by transaction 710, which has set xmax. The second one is the transaction 710 change superseded by transaction 711. The third is the current version with xmax set to 0. Note that t_ctid points to the next version, so it can chain from old versions to new ones, except for the current version, which points to itself. Typically, here, an index entry pointing to (0,1) (line pointer lp=1 in block 0) can continue to (0,2) and then (0,3).
The old versions are not visible to my current query, but they are still physically present because I disabled autovacuum and have not vacuumed the table.
Now inspect the unique index:
postgres=# SELECT itemoffset, ctid, htid, dead,
data, encode(decode(replace(substr(data,4),' ',''), 'hex'),'escape')
FROM bt_page_items('demo_unique_mvcc_email_key', 1)
ORDER BY itemoffset
;
itemoffset | ctid | htid | dead | data | encode
------------+-------+-------+------+-------------------------------------------------+-----------------------
1 | (0,1) | (0,1) | t | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
2 | (0,2) | (0,2) | f | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
3 | (0,3) | (0,3) | f | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
(3 rows)
This is the important point.
The unique index can physically hold multiple entries for the same logical key (data) because these entries point to different versions of heap tuples (htid).
Duplicate key violations
I tried to insert the same email into a new row:
postgres=# INSERT INTO demo_unique_mvcc(email, payload, marker)
VALUES ('a@example.com', 'another logical row', 10)
;
ERROR: duplicate key value violates unique constraint "demo_unique_mvcc_email_key"
DETAIL: Key (email)=(a@example.com) already exists.
postgres=#
This fails. The B-tree found matching key entries in the index. Then PostgreSQL checked the heap tuple visibility of the referenced tuples. At least one conflicting tuple is visible, so this is a uniqueness violation. The index alone did not decide everything. The heap visibility check decides whether the duplicate index entry is a real conflict.
The duplicate tuple was inserted into the heap before the statement was aborted, and this is visible from the physical history:
postgres=# SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('demo_unique_mvcc', 0))
ORDER BY lp
;
lp | t_xmin | t_xmax | t_ctid | t_infomask | t_infomask2
----+--------+--------+--------+------------+-------------
1 | 697 | 710 | (0,2) | 1282 | 4
2 | 710 | 711 | (0,3) | 9474 | 4
3 | 711 | 0 | (0,3) | 10498 | 4
4 | 712 | 0 | (0,4) | 2050 | 4
(4 rows)
PostgreSQL does not determine visibility from xmin/xmax alone. The infomask bits tell whether the transaction status is already known (committed, aborted, invalid, updated, locked, etc.). When the bits are not set, PostgreSQL consults the transaction status and may later set hint bits in the tuple header:
postgres=# SELECT txid_status('744')
;
txid_status
-------------
aborted
(1 row)
postgres=# SELECT txid_status('742')
;
txid_status
-------------
committed
(1 row)
Nothing changed in the index. It was used to find the multiple versions, but a duplicate key was detected before updating it:
postgres=# SELECT itemoffset, ctid, htid, dead,
data, encode(decode(replace(substr(data,4),' ',''), 'hex'),'escape')
FROM bt_page_items('demo_unique_mvcc_email_key', 1)
ORDER BY itemoffset
;
itemoffset | ctid | htid | dead | data | encode
------------+-------+-------+------+-------------------------------------------------+-----------------------
1 | (0,1) | (0,1) | t | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
2 | (0,2) | (0,2) | t | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
3 | (0,3) | (0,3) | f | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
(3 rows)
It is not visible here, but even if uniqueness is detected on the heap tuples, it is still part of the index access methods because concurrency control must synchronize transactions by locking the index leaf page, which is the only place where all transactions updating the same key must go, since heap tuples could be in different blocks. This explains why a unique index is required to enforce unique constraints, even though duplicate checking itself is done on the heap.
Heap-Only Tuples
I mentioned HOT updates. As I've set a low fill factor and still have lots of free space in the block (as we confirm with ctid, all updates are within the same block), an update of a single indexed column may not add a new entry:
postgres=# UPDATE demo_unique_mvcc
SET payload = 'fourth version' --, marker = 2
WHERE email = 'a@example.com'
;
UPDATE 1
postgres=# SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('demo_unique_mvcc', 0))
ORDER BY lp
;
lp | t_xmin | t_xmax | t_ctid | t_infomask | t_infomask2
----+--------+--------+--------+------------+-------------
1 | 697 | 710 | (0,2) | 1282 | 4
2 | 710 | 711 | (0,3) | 9474 | 4
3 | 711 | 713 | (0,5) | 8450 | 16388
4 | 712 | 0 | (0,4) | 2050 | 4
5 | 713 | 0 | (0,5) | 10242 | 32772
(5 rows)
postgres=# SELECT itemoffset, ctid, htid, dead,
data, encode(decode(replace(substr(data,4),' ',''), 'hex'),'escape')
FROM bt_page_items('demo_unique_mvcc_email_key', 1)
ORDER BY itemoffset
;
itemoffset | ctid | htid | dead | data | encode
------------+-------+-------+------+-------------------------------------------------+-----------------------
1 | (0,1) | (0,1) | t | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
2 | (0,2) | (0,2) | t | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
3 | (0,3) | (0,3) | f | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
(3 rows)
The t_infomask2 added HEAP_ONLY_TUPLE (32768) to the number of attributes (4), indicating it has no index entry of its own. The entry pointing to (0,3) will follow the HOT chain to find the other version within the same page.
Deferred unique constraint
One last test. We have seen that a duplicate key insert leaves a tuple in the heap but no new entry in the index because the duplicate is detected during index update. However, if the unique constraint checking is deferred, the entry will be stored, and the detection happens at commit:
postgres=# ALTER TABLE demo_unique_mvcc
DROP CONSTRAINT demo_unique_mvcc_email_key
;
postgres=# ALTER TABLE demo_unique_mvcc
ADD CONSTRAINT demo_unique_mvcc_email_key
UNIQUE (email) DEFERRABLE;
postgres=# SELECT itemoffset, ctid, htid, dead,
data, encode(decode(replace(substr(data,4),' ',''), 'hex'),'escape')
FROM bt_page_items('demo_unique_mvcc_email_key', 1)
ORDER BY itemoffset
;
itemoffset | ctid | htid | dead | data | encode
------------+-------+-------+------+-------------------------------------------------+-----------------------
1 | (0,3) | (0,3) | f | 1d 61 40 65 78 61 6d 70 6c 65 2e 63 6f 6d 00 00 | a@example.com\000\000
(1 row)
postgres=# BEGIN;
BEGIN
postgres=# SET CONSTRAINTS demo_unique_mvcc_email_key DEFERRED
;
SET CONSTRAINTS
postgres=# INSERT INTO demo_unique_mvcc(email, payload, marker)
VALUES ('a@example.com', 'a duplicate row', 10);
;
INSERT 0 1
postgres=*# SELECT lp, t_xmin, t_xmax, t_ctid, t_infomask, t_infomask2
FROM heap_page_items(get_raw_page('demo_unique_mvcc', 0))
ORDER BY lp
;
lp | t_xmin | t_xmax | t_ctid | t_infomask | t_infomask2
----+--------+--------+--------+------------+-------------
1 | 697 | 710 | (0,2) | 1282 | 4
2 | 710 | 711 | (0,3) | 9474 | 4
3 | 711 | 713 | (0,5) | 9474 | 16388
4 | 712 | 0 | (0,4) | 2562 | 4
5 | 713 | 0 | (0,5) | 10498 | 32772
6 | 718 | 0 | (
by Franck Pachot
Supabase Blog
Add field-level encryption to your Supabase project with CipherStash. Searchable ciphertext, zero-knowledge key management, and no schema changes required.
July 08, 2026
PlanetScale Blog
Deadlocks happen when transactions block each other. Learn how they escalate into downtime, how to reduce them through better queries and retry logic, and how Traffic Control can protect your database from your application.
by Simeon Griggs
July 07, 2026
AWS Database Blog - Amazon Aurora
In this post, we demonstrate how to use the PostgreSQL 18 logical replication improvements on RDS for PostgreSQL: replicating STORED generated columns with the publish_generated_columns parameter, monitoring conflicts through the new counters in pg_stat_subscription_stats, verifying that parallel streaming is enabled by default, toggling two-phase commit on a running subscription, and configuring idle_replication_slot_timeout for automatic slot cleanup. These features are available on RDS for PostgreSQL 18.0 and later and Aurora PostgreSQL.
by Ramdas Gutlapalli
AWS Database Blog - Amazon Aurora
In this post, we show you how to deploy an automated pipeline that extracts PostgreSQL audit logs from CloudWatch Logs, converts them into structured comma-separated values (CSV) format, and stores them in Amazon S3 for long-term analysis. The solution processes log entries in near real time after generation.
by Bhavini Bhatia
Franck Pachot
Your connection to PostgreSQL is handled by a dedicated backend process. If that process crashes, you might think only your session is affected. However, because backend processes share memory, PostgreSQL assumes the shared state may have been corrupted and immediately terminates all other connections. Recovery is then performed before new connections are accepted.
Here is a short test to demonstrate it and to see what is visible in the PostgreSQL server log and what is received in the application. I run an ephemeral Docker container, show the log file, start ten pgbench clients, and kill my own session:
docker exec -it $(
docker run -d --rm -e POSTGRES_PASSWORD=xxx postgres -c logging_collector=on
sleep 3
) psql -U postgres <<'SQL'
\! sleep 1 ; echo '\n 🐘 Start pgbench in the background...\n' ; sleep 1
\! pgbench -i -U postgres postgres
\! pgbench -c 5 -T 60 -P 1 -U postgres postgres & sleep 10
\! sleep 1 ; echo '\n 🐘 Tail the logfile...\n' ; sleep 1
select current_setting('data_directory')||'/'||pg_current_logfile() log
\gset
\setenv log :log
\! tail -f "$log" | sed -e "s/^/📜 /" &
\! sleep 1 ; echo '\n 🐘 Crash the current process...\n' ; sleep 1
select pg_backend_pid() as pid
\gset
\setenv pid :pid
\! kill -9 $pid
\! sleep 30
\q
SQL
Here is the output. I've deliberately set it to run in an ephemeral Docker container because I don't want you to do the same on an existing database.
The container started, psql connected, and pgbench started:
psql (18.4 (Debian 18.4-1.pgdg13+1))
Type "help" for help.
postgres=#
postgres=# \! sleep 1 ; echo '\n 🐘 Start pgbench in the background...\n' ; sleep 1
🐘 Start pgbench in the background...
postgres=#
postgres=# \! pgbench -i -U postgres postgres
dropping old tables...
NOTICE: table "pgbench_accounts" does not exist, skipping
NOTICE: table "pgbench_branches" does not exist, skipping
NOTICE: table "pgbench_history" does not exist, skipping
NOTICE: table "pgbench_tellers" does not exist, skipping
creating tables...
generating data (client-side)...
vacuuming...
creating primary keys...
done in 0.21 s (drop tables 0.00 s, create tables 0.01 s, client-side generate 0.14 s, vacuum 0.03 s, primary keys 0.03 s).
postgres=# \! pgbench -c 5 -T 60 -P 1 -U postgres postgres & sleep 10
pgbench (18.4 (Debian 18.4-1.pgdg13+1))
starting vacuum...end.
progress: 1.0 s, 504.0 tps, lat 9.719 ms stddev 7.949, 0 failed
progress: 2.0 s, 338.0 tps, lat 14.826 ms stddev 9.621, 0 failed
progress: 3.0 s, 348.0 tps, lat 14.335 ms stddev 9.640, 0 failed
progress: 4.0 s, 346.0 tps, lat 14.456 ms stddev 9.288, 0 failed
progress: 5.0 s, 357.0 tps, lat 13.976 ms stddev 9.178, 0 failed
progress: 6.0 s, 349.0 tps, lat 14.371 ms stddev 10.674, 0 failed
progress: 7.0 s, 357.0 tps, lat 14.010 ms stddev 9.035, 0 failed
progress: 8.0 s, 350.0 tps, lat 14.235 ms stddev 9.087, 0 failed
progress: 9.0 s, 341.0 tps, lat 14.788 ms stddev 9.204, 0 failed
progress: 10.0 s, 277.0 tps, lat 18.002 ms stddev 9.095, 0 failed
Displaying the PostgreSQL log file while pgbench is still running:
postgres=# \! sleep 1 ; echo '\n 🐘 Tail the logfile...\n' ; sleep 1
🐘 Tail the logfile...
progress: 11.0 s, 353.0 tps, lat 14.131 ms stddev 10.032, 0 failed
postgres=#
postgres=# select current_setting('data_directory')||'/'||pg_current_logfile() log
postgres-# \gset
postgres=# \setenv log :log
postgres=# \! tail -f "$log" | sed -e "s/^/📜 /" &
postgres=#
📜 2026-07-06 17:06:18.098 UTC [1] LOG: starting PostgreSQL 18.4 (Debian 18.4-1.pgdg13+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0, 64-bit
📜 2026-07-06 17:06:18.099 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432
📜 2026-07-06 17:06:18.099 UTC [1] LOG: listening on IPv6 address "::", port 5432
📜 2026-07-06 17:06:18.101 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
📜 2026-07-06 17:06:18.106 UTC [75] LOG: database system was shut down at 2026-07-06 17:06:17 UTC
📜 2026-07-06 17:06:18.109 UTC [1] LOG: database system is ready to accept connections
progress: 12.0 s, 332.0 tps, lat 15.064 ms stddev 10.867, 0 failed
Crashing the current backend with kill -9 while pgbench is still running and the log file is being tailed:
postgres=# \! sleep 1 ; echo '\n 🐘 Crash the current process...\n' ; sleep 1
🐘 Crash the current process...
progress: 13.0 s, 357.0 tps, lat 13.976 ms stddev 9.172, 0 failed
postgres=#
postgres=# select pg_backend_pid() as pid
postgres-# \gset
postgres=# \setenv pid :pid
postgres=# \! kill -9 $pid
postgres=#
postgres=# \! sleep 30
📜 2026-07-06 17:06:36.241 UTC [1] LOG: client backend (PID 85) was terminated by signal 9: Killed
📜 2026-07-06 17:06:36.241 UTC [1] DETAIL: Failed process was running: select pg_backend_pid() as pid
📜 2026-07-06 17:06:36.241 UTC [1] LOG: terminating any other active server processes
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
pgbench: error: client 0 aborted in command 10 (SQL) of script 0; perhaps the backend died while processing
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
pgbench: error: client 2 aborted in command 8 (SQL) of script 0; perhaps the backend died while processing
pgbench: error: client 3 aborted in command 8 (SQL) of script 0; perhaps the backend died while processing
📜 2026-07-06 17:06:36.243 UTC [1] LOG: all server processes terminated; reinitializing
pgbench: error: client 4 aborted in command 8 (SQL) of script 0; perhaps the backend died while processing
pgbench: error: client 1 aborted in command 7 (SQL) of script 0; perhaps the backend died while processing
transaction type: <builtin: TPC-B (sort of)>
scaling factor: 1
query mode: simple
number of clients: 5
number of threads: 1
maximum number of tries: 1
duration: 60 s
number of transactions actually processed: 4948
number of failed transactions: 0 (0.000%)
latency average = 14.106 ms
latency stddev = 9.783 ms
initial connection time = 6.526 ms
tps = 353.948496 (without initial connection time)
pgbench: error: Run was aborted; the above results are incomplete.
📜 2026-07-06 17:06:36.250 UTC [117] LOG: database system was interrupted; last known up at 2026-07-06 17:06:18 UTC
📜 2026-07-06 17:06:36.390 UTC [117] LOG: database system was not properly shut down; automatic recovery in progress
📜 2026-07-06 17:06:36.392 UTC [117] LOG: redo starts at 0/175F960
📜 2026-07-06 17:06:36.433 UTC [117] LOG: invalid record length at 0/2687F00: expected at least 24, got 0
📜 2026-07-06 17:06:36.433 UTC [117] LOG: redo done at 0/2687ED8 system usage: CPU: user: 0.03 s, system: 0.00 s, elapsed: 0.04 s
📜 2026-07-06 17:06:36.435 UTC [118] LOG: checkpoint starting: end-of-recovery immediate wait
📜 2026-07-06 17:06:37.205 UTC [118] LOG: checkpoint complete: wrote 2054 buffers (12.5%), wrote 3 SLRU buffers; 0 WAL file(s) added, 0 removed, 1 recycled; write=0.625 s, sync=0.138 s, total=0.771 s; sync files=51, longest=0.108 s, average=0.003 s; distance=15521 kB, estimate=15521 kB; lsn=0/2687F00, redo lsn=0/2687F00
📜 2026-07-06 17:06:37.207 UTC [1] LOG: database system is ready to accept connections
postgres=#
postgres=# \q
This indicates that PostgreSQL intentionally terminated all other connections (as seen in the error messages from the five pgbench connections) when it detected a crash signal, and that it completed recovery before allowing new connections:
The logfile indicates the cause:
LOG: client backend (PID 85) was terminated by signal 9: Killed
LOG: terminating any other active server processes
The application encountered the error:
WARNING: terminating connection because of crash of another server process
DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory.
HINT: In a moment you should be able to reconnect to the database and repeat your command.
Unlike database engines that keep sessions separate, PostgreSQL backends share key memory areas like shared buffers, lock tables, and transaction status data. If a backend crashes unexpectedly, PostgreSQL cannot ensure these structures stay consistent. As a result, it adopts a cautious strategy: shutting down all backends and restarting from a reliable, known state.
This behavior is deliberate and is a fundamental safety feature of PostgreSQL. When a backend crashes, it is considered a possible sign of shared-memory corruption. To prevent serving inconsistent data, PostgreSQL shuts down all sessions, replays WAL during recovery, and only resumes accepting connections afterward.
by Franck Pachot
Percona Database Performance Blog
Percona Operator for MySQL 1.2.0 is out, and it closes three gaps that platform teams hit once a MySQL deployment grows past a single cluster. Picture a fleet that has outgrown one region: you want a warm replica cluster in a second data center, backups in object storage that pass an auditor’s encryption check, … Continued
The post Percona Operator for MySQL 1.2.0: Cross-Site Replication, Encrypted Backups, and Automatic Storage Scaling appeared first on Percona.
by Slava Sarzhan
Percona Database Performance Blog
Migrating a production PostgreSQL database on Kubernetes is not only about moving data from one operator to another. It is also about choosing the right trade-off between downtime, operational complexity, rollback safety, cost, and business risk. Practical migration paths from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL are described here. 1. … Continued
The post Comparing Migration Methods from the Crunchy Data PostgreSQL Operator to the Percona Operator for PostgreSQL appeared first on Percona.
by Chetan Shivashankar
ParadeDB Blog
We got curious about Turbopuffer's alyze tokenizer, ported it into ParadeDB to see how it compared, and ended up finding a position-reset bug that was quietly bloating the indexes of our default tokenizer.
by James Blackwood-Sewell
July 06, 2026
AWS Database Blog - Amazon Aurora
In this post, we show how Dynata simplified database cost optimization and accelerated modernization to AWS Graviton processors by adopting Database Savings Plans. Rather than managing Reserved Instances across multiple database services, Dynata consolidated their cost commitment into a single, flexible pricing model. This reduced operational overhead by 70%, extended cost coverage to Amazon Aurora serverless, and lowered total cost of ownership as their infrastructure evolved.
by Satish Bhonsle