在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;
searchpostgresql的电子邮件组的档案“upsert”导致find一个你可能想要做的事情的例子,在手册中 :
例38-2。 与UPDATE / INSERTexception
本示例使用exception处理来适当地执行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语句,每个语句的devise都会成功或不起作用。
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才会成功。
您可以将这两个string合并为一个string,并使用从您的应用程序执行的单个SQL语句来运行它们。 强烈build议在单个事务中一起运行它们。
在隔离运行或locking的表上运行时,这种方式非常有效,但是如果同时插入一行,则会受到竞争条件的影响,即重复键错误,或者在同时删除一行时可能没有行插入。 PostgreSQL 9.1或更高版本上的SERIALIZABLE
事务将以非常高的序列化失败率为代价进行可靠的处理,这意味着您将不得不重试。 看看为什么upsert这么复杂 ,这将更详细地讨论这种情况。
除非应用程序检查受影响的行数并validationinsert
或update
是否影响行,否则此方法还会丢失read committed
隔离中的update
。
使用PostgreSQL 9.1,可以使用可写的CTE( 公用表expression式 )来实现:
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
- 为什么这么复杂?
请注意,此解决scheme不会阻止唯一的密钥违规,但它不容易丢失更新。
请参阅dba.stackexchange.com上Craig Ringer的后续信息
在PostgreSQL 9.5及更新版本中,您可以使用INSERT ... ON CONFLICT UPDATE
。
请参阅文档 。
MySQL INSERT ... ON DUPLICATE KEY UPDATE
可以直接改写为ON CONFLICT UPDATE
。 SQL标准语法也不是,它们都是数据库特定的扩展。 有充分的理由MERGE
没有用于这个 ,一个新的语法不是为了好玩而创build的。 (MySQL的语法也有问题,这意味着它不被直接采用)。
例如给定的设置:
CREATE TABLE tablename (a integer primary key, b integer, c integer); INSERT INTO tablename (a, b, c) values (1, 2, 3);
MySQL查询:
INSERT INTO tablename (a,b,c) VALUES (1,2,3) ON DUPLICATE KEY UPDATE c=c+1;
变为:
INSERT INTO tablename (a, b, c) values (1, 2, 10) ON CONFLICT (a) DO UPDATE SET c = tablename.c + 1;
区别:
-
您必须指定用于唯一性检查的列名称(或唯一约束名称)。 这是
ON CONFLICT (columnname) DO
-
必须使用关键字
SET
,就像这是一个普通的UPDATE
语句一样
它也有一些不错的function:
-
你可以在你的
UPDATE
上有一个WHERE
子句(让你可以有效地将ON CONFLICT UPDATE
变成ON CONFLICT IGNORE
) -
推荐的插入值可用作行variables
EXCLUDED
,它与目标表具有相同的结构。 您可以使用表名称获取表中的原始值。 所以在这种情况下,EXCLUDED.c
将是10
(因为这是我们试图插入的),"table".c
将是3
因为这是表中的当前值。 您可以在SET
expression式和WHERE
子句中使用一个或两个。
有关upsert的背景,请参阅PostgreSQL中的如何UPSERT(MERGE,INSERT … ON DUPLICATE UPDATE)?
当我来到这里的时候,我正在寻找同样的东西,但是缺less一个通用的“upsert”函数让我感到困扰,所以我认为你可以通过更新并插入sql作为参数
看起来像这样:
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_update并循环单个更新,性能命中将会很小见http://archives.postgresql.org/pgsql-性能/ 2006-04 / msg00557.php
成本最高的是从你的代码执行查询,在数据库方面执行成本要小得多
没有简单的命令去做。
最正确的方法是使用函数,就像文档中的函数一样。
另一个解决scheme(虽然不是那么安全)是更新返回,检查哪些行是更新,并插入其余的
有些东西是:
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被返回:
insert into table (id, column) values (1, 'aa'), (3, 'cc');
当然,它会迟早(在同时发生的情况下)得到救助,因为在这里有明显的竞争条件,但是通常情况下它会起作用。
这是一个关于这个话题的更长和更全面的文章 。
就个人而言,我已经build立了一个附加到插入语句的“规则”。 假设你有一个“dns”表,logging每个客户的dns点击次数:
CREATE TABLE dns ( "time" timestamp without time zone NOT NULL, customer_id integer NOT NULL, hits integer );
您希望能够重新插入具有更新值的行,或者如果它们不存在,则可以创build它们。 键入customer_id和时间。 像这样的东西:
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));
更新:如果发生同时插入,这可能会失败,因为它会产生unique_violationexception。 但是,未终止的交易将继续并成功,您只需重复终止的交易。
但是,如果总是有大量的插入操作发生,那么您将希望在插入语句中放置一个表锁:SHARE ROW EXCLUSIVElocking将阻止任何可能插入,删除或更新目标表中的行的操作。 但是,不更新唯一密钥的更新是安全的,所以如果没有操作会执行此操作,请改用build议锁。
另外,COPY命令不使用RULES,所以如果你使用COPY插入,你需要使用触发器。
我上面定制了“upsert”函数,如果你想插入和replace:
`
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;`
执行之后,执行如下操作:
SELECT upsert($$INSERT INTO ...$$,$$UPDATE... $$)
重要的是把双美元逗号,以避免编译错误
- 检查速度…
我有同样的问题pipe理帐户设置名称值对。 devise标准是不同的客户可以有不同的设置集。
我的解决scheme,类似于JWP,是批量擦除和replace,在您的应用程序中生成合并logging。
这是非常防弹的,独立于平台,因为每个客户端的设置从不超过20个,这只是3个相当低的负载数据库调用 – 可能是最快的方法。
更新单个行的方法 – 检查exception,然后插入 – 或者某些组合是可怕的代码,因为(如上所述)非标准SQLexception处理从数据库更改为数据库 – 甚至发布到发布,速度较慢并经常中断。
#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
类似于最喜欢的答案,但稍微快一点:
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/ )
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进行比较,如果没有行受到影响,请激活INSERT。 如果您使用其他编程语言,也许仍然可以获得修改的行数,请检查文档。
这可能不是那么优雅,但你有更简单的SQL,从调用代码中使用更简单。 不同的是,如果你在PL / PSQL中编写十行脚本,那么你可能应该单独testing一种或另一种types的脚本。
我使用这个函数合并
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扩展。
编辑:这不符合预期。 与接受的答案不同,当两个进程同时重复调用upsert_foo
时,会产生唯一的键违例。
find了! 我想出了一个方法来做一个查询:使用UPDATE ... RETURNING
来testing是否有任何行受到影响:
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
必须在单独的过程中完成,因为不幸的是,这是一个语法错误:
... WHERE NOT EXISTS (UPDATE ...)
现在它按需要工作:
SELECT upsert_foo(1, 'hi'); SELECT upsert_foo(1, 'bye'); SELECT upsert_foo(3, 'hi'); SELECT upsert_foo(3, 'bye');
合并小集合,使用上述function是好的。 但是,如果你正在合并大量的数据,我会build议看看http://mbk.projects.postgresql.org
目前我知道的最佳做法是:
- 复制新的/更新的数据到临时表(当然,如果成本可以,你也可以做INSERT)
- 获取locking[可选](build议优于表锁,国际海事组织)
- 合并。 (有趣的部分)