PostgreSQL Upsert使用系统列XMIN,XMAX和其他

时间:2016-08-20 20:01:45

标签: postgresql upsert postgresql-9.5

免责声明:理论问题。

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

这是一个简单的例子:

nd@postgres=# create table t(i int primary key, x int);
nd@postgres=# insert into t values(1,1);
nd@postgres=# 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对xminxmaxhere的含义进行了解释并不太清楚。

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

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

1 个答案:

答案 0 :(得分:31)

我认为这是一个值得深入回答的有趣问题;如果它有点冗长,请耐心等待。

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

RETURNING (xmax = 0) AS inserted

现在详细说明:

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

xmax有两个不同的目的:

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

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

  3. 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.h中找到。 lp_off是页面中元组数据的偏移量,t_ctid当前元组ID ,它由页码中的页码和元组号组成。由于新创建了表,因此所有数据都在第0页。

    让我讨论heap_page_items返回的三行。

    1. 行指针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. 在行指针2处,我们看到由事务INSERT ... ON CONFLICT创建的新的更新元组(事务102508)。

      t_infomask表示此元组是更新的结果,xmin有效,而xmax包含KEY SHARE行锁(由于交易已完成)。在INSERT ... ON CONFLICT处理期间执行了此行锁定。 t_infomask2表明这是一个HOT元组。

    3. 在第3行,我们看到新插入的行。

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

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