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

标签 sql postgresql merge sql-insert upsert

我发现使用可写 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

所以我想例如 legacy第二列采用其默认值 VALUES排。

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

最佳答案

Postgres 9.5 已实现 UPSERT .见下文。

Postgres 9.4 或更高版本

这是一个棘手的问题。您遇到了此限制( per documentation ):

In a VALUES list appearing at the top level of an INSERT, an expression can be replaced by DEFAULT to indicate that the destination column's default value should be inserted. DEFAULT cannot be used when VALUES appears in other contexts.



大胆强调我的。如果没有要插入的表,则不会定义默认值。所以你的问题没有直接的解决方案,但有很多可能的替代路线,具体取决于具体要求 .

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

您可以从系统目录 pg_attrdef 中获取这些信息。 like @Patrick commented或来自 information_schema.columns .在这里完成说明:
  • Get the default values of table columns in Postgres?

  • 但是,您仍然只有一个行列表,其中包含表达式的文本表示来 cooking 默认值。您必须动态地构建和执行语句以获取要使用的值。乏味而凌乱。相反,我们可以让内置的 Postgres 功能为我们做这件事:

    简单快捷

    插入一个虚拟行并让它返回以使用生成的默认值:
    INSERT INTO playlist_items DEFAULT VALUES RETURNING *;
    

    问题/解决方案范围
  • 这仅保证适用于 STABLE or IMMUTABLE default expressions .最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 *;
    ...
    

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

    归功于 Igor 的想法:
  • Postgresql, select a "fake" row

  • 删除 NOT NULL约束

    您必须为 NOT NULL 提供虚拟值。列,因为( per documentation ):

    Not-null constraints are always copied to the new table.



    要么容纳那些在INSERT声明或(更好)消除约束:
    ALTER TABLE tmp_playlist_items
       ALTER COLUMN foo DROP NOT NULL
     , ALTER COLUMN bar DROP NOT NULL;
    

    有一个快速而肮脏的方式具有 super 用户权限:
    UPDATE pg_attribute
    SET    attnotnull = FALSE
    WHERE  attrelid = 'tmp_playlist_items'::regclass
    AND    attnotnull
    AND    attnum > 0;
    

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

    那么,让我们来看看 清洁方式 :
    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$$
    

    更干净,仍然非常快。小心执行动态命令并警惕 SQL 注入(inject)。这个说法是安全的。我已发帖 several related answers with more explanation.

    通用解决方案(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;

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

    根据要求,这仅替换列 legacy 中的 NULL 值在 INSERT 的输入行中案件。可以轻松扩展到其他列或在 UPDATE 中工作情况也是如此。例如,您可以 UPDATE也有条件:仅当输入值为 NOT NULL .我在 UPDATE 中添加了注释行以上。

    旁白:除了 VALUES 中的第一个之外,您不需要在任何行中转换值。表达式,因为类型是从第一行派生的。

    Postgres 9.5

    工具 UPSERT INSERT .. 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;
    

    我们可以附上 VALUES条款到 INSERT直接,这允许 DEFAULT关键词。在 (id) 上发生独特违规的情况下, Postgres 更新代替。我们可以在 UPDATE 中使用排除的行. The manual:

    The SET and WHERE clauses in ON CONFLICT DO UPDATE have access to the existing row using the table's name (or an alias), and to rows proposed for insertion using the special excluded table.



    和:

    Note that the effects of all per-row BEFORE INSERT triggers are reflected in excluded values, since those effects may have contributed to the row being excluded from insertion.



    剩下的角落案例

    您有多种选择 UPDATE : 你可以 ...
  • ...根本不更新:添加 WHERE条款给 UPDATE只写入选定的行。
  • ...只更新选定的列。
  • ...仅当列当前为 NULL 时才更新:COALESCE(l.legacy, EXCLUDED.legacy)
  • ...仅当新值是 NOT NULL 时才更新:COALESCE(EXCLUDED.legacy, l.legacy)

  • 但是没有办法辨别DEFAULT INSERT中实际提供的值和值.只有结果 EXCLUDED行是可见的。如果您需要区别,请回到之前的解决方案,我们可以为您提供两者。

    关于sql - 使用 PostgreSQL 9.3 在 CTE UPSERT 中生成 DEFAULT 值,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23794405/

    相关文章:

    mysql - 数据库中所有表的字段总数

    mysql - 当一个表不是 'main' 时如何连接 3 个表

    python - 如何更新odoo中所有产品的字段中的不同记录?

    sql - 如何在 PostgreSql 中插入一条记录并返回新的 id 作为输出参数

    git - 如何维护 TFS 中可用的 git 物理分支文件夹?

    php - MySQL ON DUPLICATE KEY UPDATE语法错误

    sql - 为什么这两个带有否定 WHERE 子句的 SELECT COUNT(*) 总结不正确?

    Pandas 将切割中的列添加到 DataFrame

    git - 如何在 git 中一次提取一系列提交?

    mysql - 如何在mysql数据库中保存所有版本的帖子