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 不太清楚xmin
和xmax
列here 的含义。
是否可以将逻辑基于这些列?系统列有没有更重要的解释(源代码除外)?
最后,我对更新/插入的行的猜测是否正确?
【问题讨论】:
为什么不为您在更新期间设置的元数据创建另一列? @vol7ron 因为它会减慢整个查询的速度。我感觉现有的列(包括系统列)就足够了。 有趣。我的直觉说它应该起作用,但是它是无证行为,并且不能保证这在某天不会改变。我宁愿在专业项目中使用它。 说服 Postgres 开发人员将这个问题正式化(记录)可能是值得的。 是的,我不建议依赖旧元组的 xmax,尽管它适用于当前实现。我认为我们可能应该有一个关键字或伪函数来请求插入与更新的决定。请发帖给 pgsql-hackers 指出这一点。 【参考方案1】:我认为这是一个值得深入回答的有趣问题;如果有点长,请多多包涵。
简而言之:您的猜测是正确的,您可以使用以下RETURNING
子句来确定该行是否已插入且未更新:
RETURNING (xmax = 0) AS inserted
现在详细解释:
当一行更新时,PostgreSQL 不会修改数据,而是创建该行的一个新版本; autovacuum 将在不再需要旧版本时将其删除。行的一个版本称为 tuple,因此在 PostgreSQL 中,每行可以有多个元组。
xmax
有两个不同的用途:
如文档中所述,它可以是删除(或更新)元组的事务的事务 ID(“元组”是“行”的另一个词)。只有事务 ID 介于 xmin
和 xmax
之间的事务才能看到元组。如果没有事务 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_infomask
和t_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
表明xmin
和xmax
都属于已提交的事务,因此会显示创建和删除元组的时间。 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)功能?