协慌网

登录 贡献 社区

插入,在 PostgreSQL 中重复更新吗?

几个月前,我从关于 Stack Overflow 的答案中学到了如何使用以下语法在 MySQL 中一次执行多个更新:

INSERT INTO table (id, field, field2) VALUES (1, A, X), (2, B, Y), (3, C, Z)
ON DUPLICATE KEY UPDATE field=VALUES(Col1), field2=VALUES(Col2);

我现在已经切换到 PostgreSQL,显然这是不正确的。它指的是所有正确的表,因此我认为这是使用不同关键字的问题,但是我不确定在 PostgreSQL 文档的哪个地方覆盖了这个问题。

为了澄清,我想插入几件事,如果它们已经存在,请对其进行更新。

答案

自 9.5 版起的 PostgreSQL 具有UPSERT语法,带有ON CONFLICT子句。使用以下语法(类似于 MySQL)

INSERT INTO the_table (id, column_1, column_2) 
VALUES (1, 'A', 'X'), (2, 'B', 'Y'), (3, 'C', 'Z')
ON CONFLICT (id) DO UPDATE 
  SET column_1 = excluded.column_1, 
      column_2 = excluded.column_2;

在 postgresql 的电子邮件组归档中搜索 “upsert” ,可以在手册中找到一个示例,说明您可能想要做的事情

示例 38-2 UPDATE / INSERT 的例外

本示例根据需要使用异常处理来执行 UPDATE 或 INSERT:

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        -- note that "a" must be unique
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- do nothing, and loop to try the UPDATE again
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

黑客邮件列表中可能有一个示例,说明如何使用 9.1 及更高版本中的 CTE 批量执行此操作:

WITH foos AS (SELECT (UNNEST(%foo[])).*)
updated as (UPDATE foo SET foo.a = foos.a ... RETURNING foo.id)
INSERT INTO foo SELECT foos.* FROM foos LEFT JOIN updated USING(id)
WHERE updated.id IS NULL;

有关更清楚的示例,请参见a_horse_with_no_name 的答案。

警告:如果同时在多个会话中执行,则这是不安全的(请参见下面的注意事项)。


在 postgresql 中执行 “UPSERT” 的另一种巧妙方法是执行两个顺序的 UPDATE / INSERT 语句,每个语句被设计为成功或无效。

UPDATE table SET field='C', field2='Z' WHERE id=3;
INSERT INTO table (id, field, field2)
       SELECT 3, 'C', 'Z'
       WHERE NOT EXISTS (SELECT 1 FROM table WHERE id=3);

如果已经存在 “id = 3” 的行,则 UPDATE 将成功,否则将无效。

仅当 “id = 3” 行不存在时,INSERT 才会成功。

您可以将这两个字符串组合成一个字符串,并使用从您的应用程序执行的单个 SQL 语句来运行它们。强烈建议在单个事务中一起运行它们。

在单独运行或在锁定表上运行时,此方法效果很好,但会受到竞争条件的影响,这意味着如果同时插入一行,它可能仍然会因重复的键错误而失败,或者当同时删除一行时,如果没有插入行,它可能会终止。 PostgreSQL 9.1 或更高版本上的SERIALIZABLE事务将以非常高的序列化失败率为代价可靠地处理它,这意味着您必须重试很多。看看为什么 upsert 如此复杂,它会更详细地讨论这种情况。

除非应用程序检查受影响的行数并验证insertupdate影响了行,否则此方法还可能会丢失read committed隔离的更新。

在 PostgreSQL 9.1 中,可以使用可写的 CTE( 通用表表达式)来实现:

WITH new_values (id, field1, field2) as (
  values 
     (1, 'A', 'X'),
     (2, 'B', 'Y'),
     (3, 'C', 'Z')

),
upsert as
( 
    update mytable m 
        set field1 = nv.field1,
            field2 = nv.field2
    FROM new_values nv
    WHERE m.id = nv.id
    RETURNING m.*
)
INSERT INTO mytable (id, field1, field2)
SELECT id, field1, field2
FROM new_values
WHERE NOT EXISTS (SELECT 1 
                  FROM upsert up 
                  WHERE up.id = new_values.id)

请参阅以下博客条目:


请注意,此解决方案不能防止发生唯一的密钥冲突,但是它不容易遭受丢失的更新的影响。
请参阅dba.stackexchange.com 上 Craig Ringer 的后续报道