Fix pg_get_multixact_stats() members_size calculation

From: Chao Li <li(dot)evan(dot)chao(at)gmail(dot)com>
To: PostgreSQL-development <pgsql-hackers(at)postgresql(dot)org>
Cc: Michael Paquier <michael(dot)paquier(at)gmail(dot)com>, nagnrik(at)gmail(dot)com
Subject: Fix pg_get_multixact_stats() members_size calculation
Date: 2026-05-22 07:02:48
Message-ID: 819AC1B2-1A71-4244-B081-3ADD85D1725D@gmail.com
Views: Whole Thread | Raw Message | Download mbox | Resend email
Thread:
Lists: pgsql-hackers

Hi,

While testing pg_get_multixact_stats(), I found that it can undercount members_size.

This is a simple repro, starting from a fresh cluster.

Session 1:
```
evantest=# create table t (i int primary key);
CREATE TABLE
evantest=# insert into t values (1);
INSERT 0 1
evantest=#
evantest=# begin;
BEGIN
evantest=*# select * from t where i = 1 for share;
i
---
1
(1 row)
```

Session 2:
```
evantest=# begin;
BEGIN
evantest=*# select * from t where i = 1 for share;
i
---
1
(1 row)

evantest=*# select * from pg_get_multixact_stats();
num_mxids | num_members | members_size | oldest_multixact
-----------+-------------+--------------+------------------
1 | 2 | 0 | 1
(1 row)
```

num_members is reported as 2, but members_size is reported as 0, which looks surprising.

The current implementation does the division before multiplying by the member-group size:
```
static inline uint64
MultiXactOffsetStorageSize(MultiXactOffset new_offset,
MultiXactOffset old_offset)
{
Assert(new_offset >= old_offset);
return (uint64) ((new_offset - old_offset) / MULTIXACT_MEMBERS_PER_MEMBERGROUP) *
MULTIXACT_MEMBERGROUP_SIZE;
}
```

Since MULTIXACT_MEMBERS_PER_MEMBERGROUP is 4, any remainder of 1 to 3 members is truncated. This is less visible with large values, but it becomes obvious with a small number of members, as in the example above.

I checked the related commits, 0e3ad4b96aedee57fc2694e28486fe0ceca8110a and 97b101776ce23dd6c4abbdae213806bc24ed6133, and I didn't see anything suggesting that this truncation was intentional. So even though this is a small issue, I think it is better to fix it before PostgreSQL 19 is released.

The fix is straightforward, just compute the per-member size first, which is MULTIXACT_MEMBERGROUP_SIZE / MULTIXACT_MEMBERS_PER_MEMBERGROUP, and then
multiply that by (new_offset - old_offset).

The doc example also seems to confirm that members_size is meant to be num_members * 5, without rounding for group alignment or accounting for the 12 bytes wasted per page:
```
<screen>
=# SELECT *, pg_size_pretty(members_size) members_size_pretty
FROM pg_catalog.pg_get_multixact_stats();
num_mxids | num_members | members_size | oldest_multixact | members_size_pretty
-----------+-------------+--------------+------------------+---------------------
311740299 | 2785241176 | 13926205880 | 2 | 13 GB
(1 row)
</screen>
```

Where 2785241176 * 5 = 13926205880.

With the fix, the same test reports members_size as 10:
```
evantest=*# select * from pg_get_multixact_stats();
num_mxids | num_members | members_size | oldest_multixact
-----------+-------------+--------------+------------------
1 | 2 | 10 | 1
(1 row)
```

The attached patch also updates the existing isolation test to cover members_size.

Best regards,
--
Chao Li (Evan)
HighGo Software Co., Ltd.
https://www.highgo.com/

Attachment Content-Type Size
v1-0001-Fix-pg_get_multixact_stats-members_size-calculati.patch application/octet-stream 9.6 KB

Browse pgsql-hackers by date

  From Date Subject
Previous Message Samil C 2026-05-22 06:53:49 Inquiry Regarding Parallel DML (Write-Side) Support in Future PostgreSQL Releases