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或更高版本上的
除非应用程序检查受影响的行计数并验证
使用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和更新版本中,可以使用
请参阅文档。
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 |
根据
对于合并小集合,可以使用上面的函数。但是,如果您正在合并大量数据,我建议您查看http://mbk.projects.postgresql.org。
我了解的当前最佳实践是:
编辑:无法按预期工作。与公认的答案不同,当两个进程同时重复调用
尤里卡!我在一个查询中找到了一种方法:使用
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; |
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'); |