使用PostgreSQL 9.3在CTE UPSERT中生成DEFAULT值

Akt*_*tau 10 sql postgresql merge upsert sql-insert

我发现使用可写CTE模拟PostgreSQL中的upsert是一个非常优雅的解决方案,直到我们在Postgres中获得实际的upsert/merge.(见:https://stackoverflow.com/a/8702291/558819)

但是,有一个问题:如何插入默认值?NULL当然,使用将无助于NULL显式插入NULL,与MySQL不同.一个例子:

WITH new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
    VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
    ,      (NULL::int, 21, 1, 'b', 34, 2, NULL::boolean)
    ,      (668, 21, 30012, 'c', 30, 3, FALSE)
    ,      (7428, 21, 23068, 'd', 0, 4, FALSE)
), upsert AS (
    UPDATE playlist_items m
    SET    (playlist, item, group_name, duration, sort, legacy)
       = (nv.playlist, nv.item, nv.group_name, nv.duration, nv.sort, nv.legacy)
    FROM   new_values nv
    WHERE  nv.id = m.id
    RETURNING m.id
)
INSERT INTO playlist_items (playlist, item, group_name, duration, sort, legacy)
SELECT playlist, item, group_name, duration, sort, legacy
FROM   new_values nv
WHERE  NOT EXISTS (SELECT 1
                   FROM   upsert m
                   WHERE  nv.id = m.id)
RETURNING id
Run Code Online (Sandbox Code Playgroud)

所以我希望例如legacy列可以采用第二VALUES行的默认值.

我已经尝试了一些东西,比如DEFAULT在VALUES列表中明确使用,这不起作用,因为CTE不知道它插入了什么.我也尝试coalesce(col, DEFAULT)过插入语句似乎无法工作.那么,有可能做我想要的吗?

Erw*_*ter 15

Postgres 9.5已实施UPSERT.见下文.

Postgres 9.4或更高版本

这是一个棘手的问题.您遇到此限制(每个文档):

VALUES出现在a 的顶层的列表中INSERT,表达式可以替换DEFAULT为表示应插入目标列的默认值.在其他情境中出现DEFAULT时无法使用 VALUES.

大胆强调我的.如果没有要插入的表,则不会定义缺省值.因此,您的问题没有直接解决方案,但根据具体要求,有许多可能的替代路线.

从系统目录中获取默认值?

可以@Patrick评论的系统目录中获取或从中获取.完整说明如下:pg_attrdef information_schema.columns

但是,您仍然只有一个列表,其中包含表达式的文本表示以烹饪默认值.您必须动态构建和执行语句以获取要使用的值.单调乏味.相反,我们可以让内置的Postgres功能为我们做到这一点:

简单快捷方式

插入一个虚拟行并让它返回使用生成的默认值:

INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
Run Code Online (Sandbox Code Playgroud)

解决方案的问题/范围

  • 这是只保证为工作STABLEIMMUTABLE默认表情.大多数VOLATILE功能都可以正常工作,但没有任何保证.该current_timestamp系列函数都是稳定的,因为它们的值不会在事务中改变.
    特别是,这会对serial列(或从序列中绘制的任何其他默认值)产生副作用.但这应该不是问题,因为您通常不serial直接写入列.这些不应该在INSERT声明中列出.列的
    剩余缺陷serial:序列仍然通过单次调用来提前获取默认行,从而在编号中产生间隙.同样,这应该不是问题,因为通常serial列中预期存在差距.

还有两个问题可以解决:

  • 如果已定义列NOT NULL,则必须插入虚拟值并NULL在结果中替换.

  • 我们实际上并不想插入虚拟行.我们可以稍后删除(在同一个事务中),但可能会有更多的副作用,比如触发器ON DELETE.有一个更好的方法:

避免虚拟行

克隆一个临时表,包括列缺省值,并插入到:

BEGIN;
CREATE TEMP TABLE tmp_playlist_items (LIKE playlist_items INCLUDING DEFAULTS)
   ON COMMIT DROP;  -- drop at end of transaction

INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *;
...
Run Code Online (Sandbox Code Playgroud)

结果相同,副作用更少.由于默认表达式是逐字复制的,因此克隆从相同的序列中提取(如果有的话).但完全避免了不需要的行或触发器的其他副作用.

感谢Igor的想法:

删除NOT NULL约束

您必须为NOT NULL列提供虚拟值,因为(每个文档):

始终将非空约束复制到新表.

要么适应INSERT声明中的那些,要么(更好地)消除约束:

ALTER TABLE tmp_playlist_items
   ALTER COLUMN foo DROP NOT NULL
 , ALTER COLUMN bar DROP NOT NULL;
Run Code Online (Sandbox Code Playgroud)

有超级用户权限的快速和脏的方式:

UPDATE pg_attribute
SET    attnotnull = FALSE
WHERE  attrelid = 'tmp_playlist_items'::regclass
AND    attnotnull
AND    attnum > 0;
Run Code Online (Sandbox Code Playgroud)

它只是一个没有数据而没有其他目的的临时表,它在事务结束时被删除.所以快捷方式很诱人.不过,基本规则是:永远不要直接篡改系统目录.

所以,让我们看一个干净的方法:在DO语句中自动化动态SQL .您只需要保证您拥有的常规权限,因为相同的角色创建了临时表.

DO $$BEGIN
EXECUTE (
   SELECT 'ALTER TABLE tmp_playlist_items ALTER '
       || string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
       || ' DROP NOT NULL'
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = 'tmp_playlist_items'::regclass
   AND    attnotnull
   AND    attnum > 0
   );
END$$
Run Code Online (Sandbox Code Playgroud)

更清洁,仍然非常快.使用动态命令执行注意,并警惕SQL注入.这句话很安全.我已经发布了几个相关的答案和更多的解释.

一般解决方案(9.4及以上)

BEGIN;

CREATE TEMP TABLE tmp_playlist_items
   (LIKE playlist_items INCLUDING DEFAULTS) ON COMMIT DROP;

DO $$BEGIN
EXECUTE (
   SELECT 'ALTER TABLE tmp_playlist_items ALTER '
       || string_agg(quote_ident(attname), ' DROP NOT NULL, ALTER ')
       || ' DROP NOT NULL'
   FROM   pg_catalog.pg_attribute
   WHERE  attrelid = 'tmp_playlist_items'::regclass
   AND    attnotnull
   AND    attnum > 0
   );
END$$;

LOCK TABLE playlist_items IN EXCLUSIVE MODE;  -- forbid concurrent writes

WITH default_row AS (
   INSERT INTO tmp_playlist_items DEFAULT VALUES RETURNING *
   )
, new_values (id, playlist, item, group_name, duration, sort, legacy) AS (
   VALUES
      (651, 21, 30012, 'a', 30, 1, FALSE)
    , (NULL, 21, 1, 'b', 34, 2, NULL)
    , (668, 21, 30012, 'c', 30, 3, FALSE)
    , (7428, 21, 23068, 'd', 0, 4, FALSE)
   )
, upsert AS (  -- *not* replacing existing values in UPDATE (?)
   UPDATE playlist_items m
   SET   (  playlist,   item,   group_name,   duration,   sort,   legacy)
       = (n.playlist, n.item, n.group_name, n.duration, n.sort, n.legacy)
   --                                   ..., COALESCE(n.legacy, m.legacy)  -- see below
   FROM   new_values n
   WHERE  n.id = m.id
   RETURNING m.id
   )
INSERT INTO playlist_items
        (playlist,   item,   group_name,   duration,   sort, legacy)
SELECT n.playlist, n.item, n.group_name, n.duration, n.sort
                                   , COALESCE(n.legacy, d.legacy)
FROM   new_values n, default_row d   -- single row can be cross-joined
WHERE  NOT EXISTS (SELECT 1 FROM upsert u WHERE u.id = n.id)
RETURNING id;

COMMIT;
Run Code Online (Sandbox Code Playgroud)

LOCK如果您有并发事务尝试写入同一个表,则只需要.

根据请求,这仅替换案例legacy的输入行的列中的NULL值INSERT.可以很容易地扩展到其他列或在这种UPDATE情况下工作.例如,您也可以UPDATE有条件地:仅当输入值为时NOT NULL.我在UPDATE上面添加了注释行.

旁白:你并不需要在任何行,但在第一个值VALUES表达式,因为类型从派生的第一排.

Postgres 9.5

实现UPSERTINSERT .. ON CONFLICT .. DO NOTHING | UPDATE.这大大简化了操作:

INSERT INTO playlist_items AS m (id, playlist, item, group_name, duration, sort, legacy)
VALUES (651, 21, 30012, 'a', 30, 1, FALSE)
,      (DEFAULT, 21, 1, 'b', 34, 2, DEFAULT)  -- !
,      (668, 21, 30012, 'c', 30, 3, FALSE)
,      (7428, 21, 23068, 'd', 0, 4, FALSE)
ON CONFLICT (id) DO UPDATE
SET (playlist, item, group_name, duration, sort, legacy)
 = (EXCLUDED.playlist, EXCLUDED.item, EXCLUDED.group_name
  , EXCLUDED.duration, EXCLUDED.sort, EXCLUDED.legacy)
-- (...,  COALESCE(l.legacy, EXCLUDED.legacy))  -- see below
RETURNING m.id;
Run Code Online (Sandbox Code Playgroud)

我们可以直接附加VALUES子句INSERT,它允许DEFAULT关键字.在发生独特违规的情况下(id),Postgres会更新.我们可以使用排除的行UPDATE.手册:

SETWHERE条款在ON CONFLICT DO UPDATE访问使用表的名称(或别名)现有行,并提出了使用特殊插入行excluded表.

和:

请注意,所有每行BEFORE INSERT触发器的效果都会反映在排除值中,因为这些效果可能会导致排除插入行.

剩下的角落案例

你有各种各样的选择UPDATE:你可以......

  • ...根本不更新:添加一个WHERE子句UPDATE只能写入选定的行.
  • ...仅更新选定的列.
  • ...仅在列当前为NULL时才更新: COALESCE(l.legacy, EXCLUDED.legacy)
  • ...仅在新值为时才更新NOT NULL:COALESCE(EXCLUDED.legacy, l.legacy)

但是没有办法辨别DEFAULT实际提供的价值观和价值观INSERT.只有结果EXCLUDED行可见.如果您需要区分,请回到之前的解决方案,您可以随意使用.