关于sql:insert,postgresql中的重复更新?

Insert, on duplicate update in PostgreSQL?

几个月前,我从一个关于堆栈溢出的答案中了解到如何使用以下语法在MySQL中同时执行多个更新:

1
2
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)

1
2
3
4
5
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",可以找到一个例子,说明您可能想做什么,在手册中:

Example 38-2. Exceptions with UPDATE/INSERT

This example uses exception handling to perform either UPDATE or INSERT, as appropriate:

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
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批量执行此操作的示例:

1
2
3
4
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;

请看一个没有名字的回答的"马"来获得更清晰的例子。


警告:如果同时从多个会话执行,则这是不安全的(请参阅下面的警告)。

在PostgreSQL中执行"upsert"的另一个聪明方法是执行两个顺序更新/插入语句,每个语句都设计为成功或无效。

1
2
3
4
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"的行已经存在,则更新将成功,否则将无效。

只有当"id=3"的行不存在时,插入才会成功。

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

这在隔离运行或在锁定的表上运行时非常有效,但受制于争用条件,这意味着如果同时插入一行,它可能仍然失败,并出现重复键错误,或者在同时删除一行时终止而不插入任何行。PostgreSQL 9.1或更高版本上的SERIALIZABLE事务将以非常高的序列化失败率为代价可靠地处理它,这意味着您将不得不重试很多次。看看为什么厄普斯特如此复杂,这就更详细地讨论了这个案例。

除非应用程序检查受影响的行计数并验证INSERTupdate是否影响了行,否则此方法也会在read committed隔离中丢失更新。


使用PostgreSQL 9.1,可以使用可写CTE(公共表表达式)实现这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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)

查看这些日志:

  • 通过可写CTE升迁
  • 等待9.1–可写CTE
  • 为什么厄普塞特这么复杂?

请注意,此解决方案不会阻止唯一的密钥冲突,但它不容易丢失更新。请参阅dba.stackexchange.com上的craig ringer跟进。


在PostgreSQL 9.5和更新版本中,可以使用INSERT ... ON CONFLICT UPDATE

请参阅文档。

mysql INSERT ... ON DUPLICATE KEY UPDATE可以直接改为ON CONFLICT UPDATE。SQL标准语法也不是,它们都是特定于数据库的扩展。有很好的理由说明没有使用MERGE,一个新的语法不是为了好玩而创建的。(MySQL的语法也存在一些问题,这意味着它没有被直接采用)。

例如,给定设置:

1
2
CREATE TABLE tablename (a INTEGER PRIMARY KEY, b INTEGER, c INTEGER);
INSERT INTO tablename (a, b, c) VALUES (1, 2, 3);

MySQL查询:

1
2
INSERT INTO tablename (a,b,c) VALUES (1,2,3)
  ON DUPLICATE KEY UPDATE c=c+1;

变成:

1
2
INSERT INTO tablename (a, b, c) VALUES (1, 2, 10)
ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;

差异:

  • 必须指定用于唯一性检查的列名(或唯一约束名)。那是江户十一〔四〕号

  • 必须使用关键字SET,就像这是一个普通的UPDATE语句一样。

它也有一些很好的特点:

  • 你可以在你的UPDATE上有一个WHERE条款(让你有效地把ON CONFLICT UPDATE变成ON CONFLICT IGNORE以获得某些值)

  • 建议的插入值可用作行变量EXCLUDED,它与目标表具有相同的结构。您可以使用表名来获取表中的原始值。因此,在这种情况下,EXCLUDED.c将是10(因为这是我们试图插入的内容),"table".c将是3,因为这是表中的当前值。您可以在SET表达式和WHERE子句中使用其中一种或两者。

有关upsert的背景,请参阅如何upsert(合并、插入…在PostgreSQL中?


我来这里的时候也在找同样的东西,但是缺少一个通用的"upsert"函数让我有点困扰,所以我想您可以通过更新并在手册的函数中插入SQL作为参数。

就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE FUNCTION upsert (sql_update TEXT, sql_insert TEXT)
    RETURNS VOID
    LANGUAGE plpgsql
AS $$
BEGIN
    LOOP
        -- first try to update
        EXECUTE sql_update;
        -- check if the row is found
        IF FOUND THEN
            RETURN;
        END IF;
        -- not found so insert the row
        BEGIN
            EXECUTE sql_insert;
            RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- do nothing and loop
        END;
    END LOOP;
END;
$$;

也许要做你最初想做的,批处理"upsert",你可以使用tcl来分割SQL_更新并循环单个更新,执行命中将非常小,参见http://archives.postgresql.org/pgsql-performance/2006-04/msg00557.php

最高的成本是从代码执行查询,在数据库方面,执行成本要小得多。


没有简单的命令可以做到这一点。

最正确的方法是使用函数,比如docs中的函数。

另一个解决方案(虽然不是很安全)是通过返回进行更新,检查哪些行是更新的,并插入其余的行

沿着这条线的东西:

1
2
3
4
5
UPDATE TABLE
SET COLUMN = x.column
FROM (VALUES (1,'aa'),(2,'bb'),(3,'cc')) AS x (id, COLUMN)
WHERE TABLE.id = x.id
returning id;

假设返回ID:2:

1
INSERT INTO TABLE (id, COLUMN) VALUES (1, 'aa'), (3, 'cc');

当然,它迟早会脱离(在并行环境中),因为这里有明确的竞争条件,但通常它会起作用。

这是一篇关于这个主题的更长更全面的文章。


就我个人而言,我在insert语句中设置了一个"规则"。假设您有一个"dns"表,记录每个客户每次的dns点击量:

1
2
3
4
5
CREATE TABLE dns (
   "time" TIMESTAMP WITHOUT TIME zone NOT NULL,
    customer_id INTEGER NOT NULL,
    hits INTEGER
);

您希望能够重新插入具有更新值的行,或者在不存在的情况下创建这些行。输入客户ID和时间。像这样:

1
2
3
4
5
6
7
CREATE RULE replace_dns AS
    ON INSERT TO dns
    WHERE (EXISTS (SELECT 1 FROM dns WHERE ((dns."time" = NEW."time")
            AND (dns.customer_id = NEW.customer_id))))
    DO INSTEAD UPDATE dns
        SET hits = NEW.hits
        WHERE ((dns."time" = NEW."time") AND (dns.customer_id = NEW.customer_id));

更新:如果同时进行插入,这可能会失败,因为它将生成唯一的违规异常。但是,未终止的事务将继续并成功,您只需要重复已终止的事务。

但是,如果总是有大量的插入发生,您将希望在insert语句周围放置一个表锁:share row exclusive locking将阻止任何可能插入、删除或更新目标表中的行的操作。但是,不更新唯一密钥的更新是安全的,因此如果您不执行任何操作,请改用顾问锁。

另外,copy命令不使用规则,因此如果使用copy插入,则需要使用触发器。


如果要插入和替换,我自定义上面的"upsert"功能:

`

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 CREATE OR REPLACE FUNCTION upsert(sql_insert text, sql_update text)

 RETURNS void AS
 $BODY$
 BEGIN
    -- first try to insert and after to update. Note : insert has pk and update not...

    EXECUTE sql_insert;
    RETURN;
    EXCEPTION WHEN unique_violation THEN
    EXECUTE sql_update;
    IF FOUND THEN
        RETURN;
    END IF;
 END;
 $BODY$
 LANGUAGE plpgsql VOLATILE
 COST 100;
 ALTER FUNCTION upsert(text, text)
 OWNER TO postgres;`

在执行之后,执行如下操作:

1
SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)

为避免编译器错误,请务必使用双美元逗号。

  • 检查速度…

类似于最喜欢的答案,但工作速度稍快:

1
2
WITH upsert AS (UPDATE spider_count SET tally=1 WHERE DATE='today' RETURNING *)
INSERT INTO spider_count (spider, tally) SELECT 'Googlebot', 1 WHERE NOT EXISTS (SELECT * FROM upsert)

(来源:http://www.the-art-of-web.com/sql/upsert/)


我在管理帐户设置时遇到了与名称-值对相同的问题。设计标准是不同的客户机可以有不同的设置集。

我的解决方案,类似于jwp,是批量删除和替换,在应用程序中生成合并记录。

这是非常防弹的,平台独立的,而且由于每个客户端的设置从来没有超过20个,所以这只是3个相当低负载的DB调用——可能是最快的方法。

更新单个行(检查异常,然后插入)或某些组合的替代方法是可怕的代码,速度慢,而且经常中断,因为(如上所述)非标准SQL异常处理从DB更改为DB,甚至从发布更改为发布。

1
2
3
4
5
6
7
8
 #This IS pseudo-code - WITHIN the application:
 BEGIN TRANSACTION - GET TRANSACTION LOCK
 SELECT ALL CURRENT name VALUE pairs WHERE id = $id INTO a hash record
 CREATE a MERGE record FROM the CURRENT AND UPDATE record
  (SET INTERSECTION WHERE shared KEYS IN NEW win, AND empty VALUES IN NEW are deleted).
 DELETE ALL name VALUE pairs WHERE id = $id
 COPY/INSERT merged records
 END TRANSACTION


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE OR REPLACE FUNCTION save_user(_id INTEGER, _name CHARACTER VARYING)
  RETURNS BOOLEAN AS
$BODY$
BEGIN
    UPDATE users SET name = _name WHERE id = _id;
    IF FOUND THEN
        RETURN TRUE;
    END IF;
    BEGIN
        INSERT INTO users (id, name) VALUES (_id, _name);
    EXCEPTION WHEN OTHERS THEN
            UPDATE users SET name = _name WHERE id = _id;
        END;
    RETURN TRUE;
END;

$BODY$
  LANGUAGE plpgsql VOLATILE STRICT


更新将返回修改的行数。如果使用JDBC(Java),则可以在0处检查此值,如果没有影响行,则使用FINK插入。如果使用其他编程语言,可能仍然可以获得修改行的数目,请查看文档。

这可能不那么优雅,但您有更简单的SQL,从调用代码中使用它更为简单。不同的是,如果您用pl/psql编写十行脚本,那么您可能只需要为它进行一种或另一种类型的单元测试。


我使用这个函数merge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE OR REPLACE FUNCTION merge_tabla(KEY INT, DATA TEXT)
  RETURNS void AS
$BODY$
BEGIN
    IF EXISTS(SELECT a FROM tabla WHERE a = KEY)
        THEN
            UPDATE tabla SET b = DATA WHERE a = KEY;
        RETURN;
    ELSE
        INSERT INTO tabla(a,b) VALUES (KEY, DATA);
        RETURN;
    END IF;
END;
$BODY$
LANGUAGE plpgsql


根据INSERT语句的PostgreSQL文档,不支持处理ON DUPLICATE KEY案例。这部分语法是一个专有的MySQL扩展。


对于合并小集合,可以使用上面的函数。但是,如果您正在合并大量数据,我建议您查看http://mbk.projects.postgresql.org。

我了解的当前最佳实践是:

  • 将新的/更新的数据复制到临时表中(当然可以,如果成本正常,也可以插入)
  • 获取锁[可选](建议优先于表锁,IMO)
  • 合并。(有趣的部分)

  • 编辑:无法按预期工作。与公认的答案不同,当两个进程同时重复调用upsert_foo时,这会产生唯一的密钥冲突。

    尤里卡!我在一个查询中找到了一种方法:使用UPDATE ... RETURNING测试是否有任何行受到影响:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CREATE TABLE foo (k INT PRIMARY KEY, v TEXT);

    CREATE FUNCTION update_foo(k INT, v TEXT)
    RETURNS SETOF INT AS $$
        UPDATE foo SET v = $2 WHERE k = $1 RETURNING $1
    $$ LANGUAGE SQL;

    CREATE FUNCTION upsert_foo(k INT, v TEXT)
    RETURNS VOID AS $$
        INSERT INTO foo
            SELECT $1, $2
            WHERE NOT EXISTS (SELECT update_foo($1, $2))
    $$ LANGUAGE SQL;

    UPDATE必须在单独的过程中完成,因为不幸的是,这是一个语法错误:

    1
    ... WHERE NOT EXISTS (UPDATE ...)

    现在它按需工作:

    1
    2
    3
    4
    SELECT upsert_foo(1, 'hi');
    SELECT upsert_foo(1, 'bye');
    SELECT upsert_foo(3, 'hi');
    SELECT upsert_foo(3, 'bye');