如何查看upsert是否是PostgreSQL 9.5+ UPSERT的更新?

How to find out if an upsert was an update with PostgreSQL 9.5+ UPSERT?

可写CTE被认为是9.5之前的UPSERT的解决方案,如插入,PostgreSQL中的重复更新中所述?

可以执行带有以下信息的UPSERT,无论它是以UPDATE还是INSERT与以下可写CTE惯用语:

1
2
3
4
5
6
7
8
9
WITH
    update_cte AS (
        UPDATE t SET v = $1 WHERE id = $2 RETURNING 'updated'::text STATUS
    ),
    insert_cte AS (
        INSERT INTO t(id, v) SELECT $2, $1 WHERE NOT EXISTS
            (SELECT 1 FROM update_cte) RETURNING 'inserted'::text STATUS
    )
 (SELECT STATUS FROM update_cte) UNION (SELECT STATUS FROM insert_cte)

此查询将返回"已更新"或"已插入",或者可能(很少)因违反约束而失败,如https://dba.stackexchange.com/questions/78510/why-is-cte-open-to中所述-lost-更新

使用PostgreSQL 9.5+新的"UPSERT"语法可以实现类似的功能,从优化中获益并避免可能的约束违规吗?


我相信xmax::text::int > 0将是最简单的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
so=# DROP TABLE IF EXISTS tab;
NOTICE:  TABLE"tab" does NOT exist, skipping
DROP TABLE
so=# CREATE TABLE tab(id INT PRIMARY KEY, col text);
CREATE TABLE
so=# INSERT INTO tab(id, col) VALUES (1,'a'), (2, 'b');
INSERT 0 2
so=# INSERT INTO tab(id, col)
VALUES (3, 'c'), (4, 'd'), (1,'aaaa')
ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col
returning *,CASE WHEN xmax::text::INT > 0 THEN 'updated' ELSE 'inserted' END,ctid;
 id | col  |   CASE   | ctid
----+------+----------+-------
  3 | c    | inserted | (0,3)
  4 | d    | inserted | (0,4)
  1 | aaaa | updated  | (0,5)
(3 ROWS)

INSERT 0 3
so=# INSERT INTO tab(id, col)
VALUES (3, 'c'), (4, 'd'), (1,'aaaa')
ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col
returning *,CASE WHEN xmax::text::INT > 0 THEN 'updated' ELSE 'inserted' END,ctid;
 id | col  |  CASE   | ctid
----+------+---------+-------
  3 | c    | updated | (0,6)
  4 | d    | updated | (0,7)
  1 | aaaa | updated | (0,8)
(3 ROWS)

INSERT 0 3


根据@ lad2025的答案,可以通过滥用WHERE子句中的相关函数的设置和自定义选项来实现结果,以获得所需的副作用。

1
2
3
4
5
6
7
8
9
CREATE TABLE t(id INT PRIMARY KEY, v TEXT);

INSERT INTO t (id, v)
    SELECT $1, $2
    WHERE 'inserted' = set_config('upsert.action', 'inserted', TRUE)
    ON CONFLICT (id) DO UPDATE
        SET v = EXCLUDED.v
        WHERE 'updated' = set_config('upsert.action', 'updated', TRUE)
RETURNING current_setting('upsert.action') AS"upsert.action";

set_config的第三个参数是is_localtrue表示设置将在交易结束时消失。 更确切地说,current_setting('upsert.action')将返回NULL(并且不会抛出错误),直到会话结束。


SQL Server MERGE语句中,$action返回字符串'INSERT', 'UPDATE', or 'DELETE'

对于Postgresql,我找不到与RETURNING类似的函数/变量。

解决方法的一种方法是将列is_updated添加到表中:

1
2
3
4
5
6
7
8
9
10
11
12
13
DROP TABLE IF EXISTS tab;

CREATE TABLE tab(id INT PRIMARY KEY, col VARCHAR(100),
                 is_updated BOOLEAN DEFAULT FALSE);
INSERT INTO tab(id, col) VALUES (1,'a'), (2, 'b');


-- main query
INSERT INTO tab(id, col)
VALUES (3, 'c'), (4, 'd'), (1,'aaaa')
ON CONFLICT (id) DO UPDATE SET col = EXCLUDED.col, is_updated = TRUE
RETURNING id,col,
          CASE WHEN is_updated THEN 'UPDATED' ELSE 'INSERTED' END AS action;

Rextester演示

输出:

1
2
3
4
5
6
7
╔════╦══════╦══════════╗
║ id ║ col  ║  action  ║
╠════╬══════╬══════════╣
║  3 ║ c    ║ INSERTED ║
║  4 ║ d    ║ INSERTED ║
║  1 ║ aaaa ║ UPDATED  ║
╚════╩══════╩══════════╝


(xmax::text::bigint > 0)(NOT xmax = 0)。 一旦事务计数达到整数溢出,Typecast到整数就会中断。