Thread

  1. Fix pg_get_multixact_stats() members_size calculation

    Chao Li <li.evan.chao@gmail.com> — 2026-05-22T07:02:48Z

    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/