The docs for cmax say:[0]

> The command identifier within the deleting transaction, or zero.

This was true once upon a time, I think. But nowadays cmax and cmin
are the same physical field, and the user-facing system columns don't
seem to be trying to interpret it. For example:

[v19devel:5432][334102] regression=# create table pj (a int);
CREATE TABLE
[v19devel:5432][334102] regression=# begin; insert into pj values (1);
insert into pj values (2); commit;
BEGIN
INSERT 0 1
INSERT 0 1
COMMIT
[v19devel:5432][334102] regression=# select ctid, xmin, xmax, cmin,
cmax, * from pj;
 ctid  | xmin  | xmax | cmin | cmax | a
-------+-------+------+------+------+---
 (0,1) | 22424 |    0 |    0 |    0 | 1
 (0,2) | 22424 |    0 |    1 |    1 | 2

So here you have a non-zero cmax for a not-deleted row.

The converse isn't true either. "Or zero" hints that deleted rows
might always have a non-zero value, but 0 is also just the first
command in the transaction. (Null would be a meaningful signal, but I
assume we don't want to do that.)

As far as I can tell, it is impossible to observe cmin <> cmax. From
heap_getsysattr (access/common/heaptuple.c):

        case MinCommandIdAttributeNumber:
        case MaxCommandIdAttributeNumber:

            /*
             * cmin and cmax are now both aliases for the same field, which
             * can in fact also be a combo command id.  XXX perhaps we should
             * return the "real" cmin or cmax if possible, that is if we are
             * inside the originating transaction?
             */
            result =
CommandIdGetDatum(HeapTupleHeaderGetRawCommandId(tup->t_data));
            break;

So it looks like these system columns also don't look up combocids.

I'm not interested in changing any of this, but I think we could clean
up the docs a little. The description for cmin is questionable too:

> The command identifier (starting at zero) within the inserting transaction.

That's true if the row hasn't been deleted yet, but then we overwrite the field.

Here is a patch to make both of these fields a little clearer, I
think. It could be improved further by some glossary entries
explaining what a command id is (and a combocid). Or maybe that's too
much information? And maybe we should be more drastic: combine cmin &
cmax into one entry, and explain that they are two names for the same
value, which might signify the insert cid, the delete cid, or a
combocid.

[0] 
https://www.postgresql.org/docs/current/ddl-system-columns.html#DDL-SYSTEM-COLUMNS-CMAX

Yours,

-- 
Paul              ~{:-)
[email protected]
From 5cfa05ee9c6f075875b4eb9ad6d64e81c9cbdcbb Mon Sep 17 00:00:00 2001
From: "Paul A. Jungwirth" <[email protected]>
Date: Tue, 20 Jan 2026 10:33:31 -0800
Subject: [PATCH v1] docs: Clarify cmin and cmax system columns

These are two names for the same physical field. The docs suggested that they
were distinct, and the cmax description was simply wrong for not-yet-deleted
rows.
---
 doc/src/sgml/ddl.sgml | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/doc/src/sgml/ddl.sgml b/doc/src/sgml/ddl.sgml
index 9070aaa5a7c..7fdfc8da1b8 100644
--- a/doc/src/sgml/ddl.sgml
+++ b/doc/src/sgml/ddl.sgml
@@ -1510,7 +1510,10 @@ CREATE TABLE circles (
 
      <para>
       The command identifier (starting at zero) within the inserting
-      transaction.
+      transaction.  Once the row has been deleted, this is replaced by the
+      identifier of the delete command (or a combo identifier, if still in the
+      same transaction as the insert). <structfield>cmin</structfield> always
+      has the same value as <structfield>cmax</structfield>.
      </para>
     </listitem>
    </varlistentry>
@@ -1540,7 +1543,12 @@ CREATE TABLE circles (
      </indexterm>
 
      <para>
-      The command identifier within the deleting transaction, or zero.
+      The command identifier (starting at zero) within the deleting
+      transaction.  If the row has not yet been deleted, this is the identifier
+      of the insert command. If the row was inserted and deleted in the same
+      transaction, it is a combo identifier instead, whose meaning is private
+      to that transaction. <structfield>cmax</structfield> always has the same
+      value as <structfield>cmin</structfield>.
      </para>
     </listitem>
    </varlistentry>
-- 
2.47.3

Reply via email to