PostgreSQL Upsert使用系统列XMIN,XMAX和其他来区分插入和更新的行

PostgreSQL Upsert differentiate inserted and updated rows using system columns XMIN, XMAX and others

免责声明:理论问题。

这里询问了几个有关如何区分PostgreSQL upsert语句中插入和更新的行的问题。

这是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE t(i INT PRIMARY KEY, x INT);
INSERT INTO t VALUES(1,1);
INSERT INTO t VALUES(1,11),(2,22)
    ON conflict(i) do UPDATE SET x = excluded.i*11
    returning *, xmin, xmax;

╔═══╤════╤══════╤══════╗
║ i │ x  │ xmin │ xmax ║
╠═══╪════╪══════╪══════╣
11176967696
2227696 │    0
╚═══╧════╧══════╧══════╝

因此,xmax> 0(或xmax = xmin)-行已更新; xmax = 0-已插入行。

IMO此处不太清楚解释xminxmax列的含义。

是否可以将逻辑基于这些列?关于系统列(源代码除外),还有什么更重要的解释吗?

最后我对更新/插入行的猜测正确吗?


我认为这是一个有趣的问题,值得深入回答。如果它有点长,请忍受。

简而言之:您的猜测是正确的,您可以使用以下RETURNING子句来确定是否插入了该行且未更新该行:

1
RETURNING (xmax = 0) AS inserted

现在详细说明:

更新一行时,PostgreSQL不会修改数据,而是创建该行的新版本;当不再需要旧版本时,将通过autovacuum删除。行的一个版本称为元组,因此在PostgreSQL中每行可以有多个元组。

xmax有两个不同的用途:

  • 如文档中所述,它可以是删除(或更新)元组的事务的事务ID(" tuple"是" row"的另一个词)。只有交易ID在xminxmax之间的交易才能看到该元组。如果没有事务ID小于xmax的事务,则可以安全地删除旧的元组。

  • xmax也用于存储行锁。在PostgreSQL中,行锁不存储在锁表中,而是存储在元组中,以避免锁表溢出。
    如果只有一个事务在该行上具有锁,则xmax将包含锁定事务的事务ID。如果该行上有多个事务锁定,则xmax包含一个所谓的multixact的编号,该数据结构又包含锁定事务的事务ID。

  • xmax的文档尚不完整,因为此字段的确切含义被认为是实现细节,并且在不知道元组的t_infomask的情况下无法理解,而元组的t_infomask不能通过SQL立即看到。

    您可以安装contrib模块pageinspect来查看元组的此字段和其他字段。

    我运行了您的示例,这是我使用heap_page_items函数检查详细信息时看到的(在我的情况下,事务ID号当然是不同的):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    SELECT *, ctid, xmin, xmax FROM t;

    ┌───┬────┬───────┬────────┬────────┐
    │ i │ x  │ ctid  │  xmin  │  xmax  │
    ├───┼────┼───────┼────────┼────────┤
    111(0,2)102508102508
    222(0,3)102508 │      0
    └───┴────┴───────┴────────┴────────┘
    (2 ROWS)

    SELECT lp, lp_off, t_xmin, t_xmax, t_ctid,
           to_hex(t_infomask) AS t_infomask, to_hex(t_infomask2) AS t_infomask2
    FROM heap_page_items(get_raw_page('laurenz.t', 0));

    ┌────┬────────┬────────┬────────┬────────┬────────────┬─────────────┐
    │ lp │ lp_off │ t_xmin │ t_xmax │ t_ctid │ t_infomask │ t_infomask2 │
    ├────┼────────┼────────┼────────┼────────┼────────────┼─────────────┤
    │  1 │   8160102507102508(0,2)  │ 500        │ 4002        │
    │  2 │   8128102508102508(0,2)  │ 2190       │ 8002        │
    │  3 │   8096102508 │      0(0,3)  │ 900        │ 2           │
    └────┴────────┴────────┴────────┴────────┴────────────┴─────────────┘
    (3 ROWS)

    t_infomaskt_infomask2的含义可以在src/include/access/htup_details.h中找到。 lp_off是页面中元组数据的偏移量,而t_ctid是当前的元组ID,它由页面号和页面中的元组号组成。由于表是新创建的,因此所有数据都在页面0中。

    让我讨论heap_page_items返回的三行。

  • 在行指针(lp)1处,我们找到了旧的,更新的元组。它最初具有ctid = (0,1),但是经过修改以包含更新期间当前版本的元组ID。元组由事务102507创建,并由事务102508(发布INSERT ... ON CONFLICT的事务)使之无效。该元组不再可见,在VACUUM期间将被删除。

    t_infomask显示xminxmax都属于已提交的事务,因此显示创建和删除元组的时间。 t_infomask2显示该元组已通过HOT(仅堆元组)更新进行了更新,这意味着更新后的元组与原始元组在同一页面中,并且没有修改索引列(请参见src/backend/access/heap/README.HOT)。

  • 在行指针2处,我们看到由事务INSERT ... ON CONFLICT创建的新的,更新的元组(事务102508)。

    t_infomask显示此元组是更新的结果,xmin有效,并且xmax包含KEY SHARE行锁(由于事务完成,该锁不再相关)。此行锁是在INSERT ... ON CONFLICT处理期间获取的。 t_infomask2表示这是一个HOT元组。

  • 在行指针3处,我们看到新插入的行。

    t_infomask显示xmin有效,而xmax无效。 xmax设置为0,因为此值始终用于新插入的元组。

  • 因此,更新行的非零xmax是由行锁引起的实现工件。可以想象有一天会重新实现INSERT ... ON CONFLICT,以便这种行为发生变化,但是我认为这不太可能。