Fix pg_get_multixact_stats() members_size calculation

Started by Chao Li3 days ago2 messageshackers
Jump to latest
#1Chao Li
li.evan.chao@gmail.com

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/

Attachments:

v1-0001-Fix-pg_get_multixact_stats-members_size-calculati.patchapplication/octet-stream; name=v1-0001-Fix-pg_get_multixact_stats-members_size-calculati.patch; x-unix-mode=0644Download+49-28
#2Michael Paquier
michael@paquier.xyz
In reply to: Chao Li (#1)
Re: Fix pg_get_multixact_stats() members_size calculation

On Fri, May 22, 2026 at 03:02:48PM +0800, Chao Li wrote:

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

Surprising it is. I'll double-check tomorrow. Thanks.
--
Michael