PostgreSQL Upsert 使用系统列 XMIN、XMAX 等来区分插入和更新的行

Posted

技术标签:

【中文标题】PostgreSQL Upsert 使用系统列 XMIN、XMAX 等来区分插入和更新的行【英文标题】:PostgreSQL Upsert differentiate inserted and updated rows using system columns XMIN, XMAX and others 【发布时间】:2016-12-27 17:54:54 【问题描述】:

免责声明:理论问题。

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

这是一个简单的例子:

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 ║
╠═══╪════╪══════╪══════╣
║ 1 │ 11 │ 7696 │ 7696 ║
║ 2 │ 22 │ 7696 │    0 ║
╚═══╧════╧══════╧══════╝

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

IMO 不太清楚xminxmax 列here 的含义。

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

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

【问题讨论】:

为什么不为您在更新期间设置的元数据创建另一列? @vol7ron 因为它会减慢整个查询的速度。我感觉现有的列(包括系统列)就足够了。 有趣。我的直觉说它应该起作用,但是它是无证行为,并且不能保证这在某天不会改变。我宁愿在专业项目中使用它。 说服 Postgres 开发人员将这个问题正式化(记录)可能是值得的。 是的,我不建议依赖旧元组的 xmax,尽管它适用于当前实现。我认为我们可能应该有一个关键字或伪函数来请求插入与更新的决定。请发帖给 pgsql-hackers 指出这一点。 【参考方案1】:

我认为这是一个值得深入回答的有趣问题;如果有点长,请多多包涵。

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

RETURNING (xmax = 0) AS inserted

现在详细解释:

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

xmax 有两个不同的用途:

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

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

xmax 的文档并不完整,因为该字段的确切含义被认为是一个实现细节,不知道元组的t_infomask 就无法理解,通过 SQL 无法立即看到。

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

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

SELECT *, ctid, xmin, xmax FROM t;

┌───┬────┬───────┬────────┬────────┐
│ i │ x  │ ctid  │  xmin  │  xmax  │
├───┼────┼───────┼────────┼────────┤
│ 1 │ 11 │ (0,2) │ 102508 │ 102508 │
│ 2 │ 22 │ (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 │   8160 │ 102507 │ 102508 │ (0,2)  │ 500        │ 4002        │
│  2 │   8128 │ 102508 │ 102508 │ (0,2)  │ 2190       │ 8002        │
│  3 │   8096 │ 102508 │      0 │ (0,3)  │ 900        │ 2           │
└────┴────────┴────────┴────────┴────────┴────────────┴─────────────┘
(3 rows)

t_infomaskt_infomask2的含义参见src/include/access/htup_details.hlp_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(heap only tuple)更新进行更新,这意味着更新后的元组与原始元组在同一页面中,并且没有修改索引列(参见 @ 987654349@)。

    在第 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 有一天会重新实现,从而改变这种行为,但我认为这不太可能。

【讨论】:

感谢系统列的详细说明。了解它的内部工作原理非常有用。 这是一个出色而有趣的答案,值得更多的支持。 这是否意味着我们可以依赖这种行为?因为如果 Postgres 出于某种原因决定不实施删除方面的更新,这也可能会改变。 它没有被记录并且是实现的副作用,所以它可能会改变。但几率不是很高。

以上是关于PostgreSQL Upsert 使用系统列 XMIN、XMAX 等来区分插入和更新的行的主要内容,如果未能解决你的问题,请参考以下文章

如何确定 upsert 是不是是 PostgreSQL 9.5+ UPSERT 的更新?

PostgreSQL INSERT ON CONFLICT UPDATE (upsert) 使用所有排除值

如何在 flask_sqlalchemy 中使用 PostgreSQL 的“INSERT...ON CONFLICT”(UPSERT)功能?

SQLAlchemy - 在 postgresql 中执行批量 upsert(如果存在,更新,否则插入)

PostgreSQL Upsert 用于几乎相似的值

如何在 PostgreSQL 中进行 UPSERT(合并、插入……重复更新)?