如何在INSERT ... ON CONFLICT中包含RETURNING中的排除行

时间:2016-03-11 21:04:29

标签: sql django postgresql upsert sql-returning

我有这张桌子(由Django生成):

CREATE TABLE feeds_person (
  id serial PRIMARY KEY,
  created timestamp with time zone NOT NULL,
  modified timestamp with time zone NOT NULL,
  name character varying(4000) NOT NULL,
  url character varying(1000) NOT NULL,
  email character varying(254) NOT NULL,
  CONSTRAINT feeds_person_name_ad8c7469_uniq UNIQUE (name, url, email)
);

我尝试使用带有INSERT子句的ON CONFLICT批量插入大量数据。

皱纹是我需要为所有行获取id,无论它们是否已经存在。

在其他情况下,我会做类似的事情:

INSERT INTO feeds_person (created, modified, name, url, email)
VALUES blah blah blah
ON CONFLICT (name, url, email) DO UPDATE SET url = feeds_person.url
RETURNING id

执行UPDATE会导致语句返回该行的id。除此之外,它不适用于此表。我认为它没有用,因为我有多个字段是唯一的,而在其他情况下,我使用过这种方法,我只有一个独特的字段。

尝试通过Django的游标运行SQL时出现此错误:

django.db.utils.ProgrammingError: ON CONFLICT DO UPDATE command cannot affect row a second time
HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

如何使用此表进行批量插入并返回插入的和现有的ID?

1 个答案:

答案 0 :(得分:22)

你得到的错误:

  

ON CONFLICT DO UPDATE命令不能再次影响行

表示您尝试在单个命令中多次突发同一行。换句话说:您在(name, url, email)列表中的VALUES上有欺骗行为。折叠重复(如果这是一个选项),它应该工作。但是你必须决定从每组欺骗中挑选哪一行。

INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more
   ) v(created, modified, name, url, email)  -- match column list
ON     CONFLICT (name, url, email) DO UPDATE
SET    url = feeds_person.url
RETURNING id;

由于我们现在使用独立的VALUES表达式,您必须为非默认类型添加显式类型转换。像:

VALUES
    (timestamptz '2016-03-12 02:47:56+01'
   , timestamptz '2016-03-12 02:47:56+01'
   , 'n3', 'u3', 'e3')
   ...

您的timestamptz列需要显式类型转换,而字符串类型可以使用默认text。 (你仍然可以立即施放到varchar(n)。)

有多种方法可以确定从每组欺骗中选择哪一行:

您是对的,(目前)无法在RETURNING子句中获取 排除 行。我引用Postgres Wiki

  

请注意,RETURNING无法显示“EXCLUDED.*”别名   来自UPDATE(只是通用的“TARGET.*”别名是可见的   那里)。这样做被认为会造成烦人的歧义   简单,常见的案例[30]几乎没有任何好处。在某些   在未来,我们可能会采取一种暴露方式   RETURNING - 插入并更新了预计的元组,但是这个   可能不需要将它变成第一个提交的迭代   功能[31]

但是,您不应该更新不应更新的行。空更新几乎与常规更新一样昂贵 - 并且可能会产生意想不到的副作用。你不一定要开始使用UPSERT,你的情况看起来更像是“SELECT或INSERT”。相关:

插入一组行的一种清洁方法是使用数据修改CTE:

WITH val AS (
   SELECT DISTINCT ON (name, url, email) *
   FROM  (
      VALUES 
      (timestamptz '2016-1-1 0:0+1', timestamptz '2016-1-1 0:0+1', 'n', 'u', 'e')
    , ('2016-03-12 02:47:56+01', '2016-03-12 02:47:56+01', 'n1', 'u3', 'e3')
      -- more (type cast only needed in 1st row)
      ) v(created, modified, name, url, email)
   )
, ins AS (
   INSERT INTO feeds_person (created, modified, name, url, email)
   SELECT created, modified, name, url, email FROM val
   ON     CONFLICT (name, url, email) DO NOTHING
   RETURNING id, name, url, email
   )
SELECT 'inserted' AS how, id FROM ins  -- inserted
UNION  ALL
SELECT 'selected' AS how, f.id         -- not inserted
FROM   val v
JOIN   feeds_person f USING (name, url, email);

增加的复杂性应该支付以INSERT为规则且SELECT为例外的大表。

最初,我在最后NOT EXISTS添加了SELECT谓词,以防止结果重复。但那是多余的。 单个查询的所有CTE都会看到相同的表快照。 ON CONFLICT (name, url, email) DO NOTHING返回的集与INNER JOIN之后返回的集互斥。在同一列上。

不幸的是,这也为比赛条件打开了 小窗口 。如果......

  • 并发事务插入冲突行
  • 尚未提交
  • 但最终提交

......某些行可能会丢失。

您可能只需INSERT .. ON CONFLICT DO NOTHING,然后在同一事务中对所有行进行单独的SELECT查询即可克服此问题。如果并发事务可以在INSERTSELECT之间提交对表的写入(默认情况下NSString,则会为竞争条件打开另一个 微小窗口 {3}})。可以使用READ COMMITTED isolation level(或更严格)来避免。或者在整个桌子上使用(可能是昂贵的甚至是不可接受的)写锁定。您可以获得所需的任何行为,但可能需要付出代价。

相关: