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

How to include excluded rows in RETURNING from INSERT ... ON CONFLICT

我有这个 table(由 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)
);

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

问题是我需要为 所有 行取回 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 会导致语句 return 该行的 id。除了,它不适用于此 table。我认为它不起作用,因为我有多个唯一的字段在一起,而在其他情况下我使用这种方法我只有一个唯一的字段。

我在尝试通过 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.

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

你得到的错误:

ON CONFLICT DO UPDATE command cannot affect row a second time

... 表示您正尝试在单个命令中多次插入同一行。换句话说:您的 VALUES 列表中的 (name, url, email) 上有骗子。折叠重复项(如果这是一个选项),错误就会消失。这从每组 dupes 中选择任意行:

INSERT INTO feeds_person (created, modified, name, url, email)
SELECT DISTINCT ON (name, url, email) *
FROM  (
   VALUES
   ('blah', 'blah', 'blah', 'blah', 'blah')
   -- ... more rows
   ) AS 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)。)

如果您想在从每组骗子中选择哪行有发言权,有一些方法可以做到这一点:

  • Select first row in each GROUP BY group?

你是对的,(目前)无法使用 [=20= 中的 excluded 列] 条款。我引用 Postgres Wiki:

Note that RETURNING does not make visible the "EXCLUDED.*" alias from the UPDATE (just the generic "TARGET.*" alias is visible there). Doing so is thought to create annoying ambiguity for the simple, common cases [30] for little to no benefit. At some point in the future, we may pursue a way of exposing if RETURNING-projected tuples were inserted and updated, but this probably doesn't need to make it into the first committed iteration of the feature [31].

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

  • Is SELECT or INSERT in a function prone to race conditions?

一个 插入一组行的更简洁的方法是使用数据修改 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);

增加的复杂性应该为大 table 付出代价,其中 INSERT 是规则,SELECT 是例外。

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

不幸的是,这也会为竞争条件打开一个微小的window。如果...

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

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

您可能只是 INSERT .. ON CONFLICT DO NOTHING,然后对所有行进行单独的 SELECT 查询 - 在同一事务中解决这个问题。如果并发事务可以在 table 之间提交写入 竞争条件,这又会打开另一个 tiny window =26=] 和 SELECT(默认 READ COMMITTED isolation level). Can be avoided with REPEATABLE READ transaction isolation(或更严格)。或者在整个 table 上使用(可能昂贵甚至 unacceptable)写锁。你可以获得您需要的任何行为,但可能需要付出一定的代价。

相关: