UPSERT 基于具有 NULL 值的 UNIQUE 约束

UPSERT based on UNIQUE constraint with NULL values

我有一个 Postgres table 对多列有唯一约束,其中一列可以为 NULL。对于每个组合,我只想在该列中允许一条记录为 NULL。

create table my_table (
   col1 int generated by default as identity primary key,
   col2 int not null,
   col3 real,
   col4 int,
   constraint ux_my_table_unique unique (col2, col3)
);

我有一个更新插入查询,当它遇到在 col2、col3 中具有相同值的记录时,我想更新 col4:

insert into my_table (col2, col3, col4) values (p_col2, p_col3, p_col4)
on conflict (col2, col3) do update set col4=excluded.col4;

但当 col3 为 NULL 时,冲突并未触发。我已经阅读了有关使用触发器的信息。请问解决冲突的最佳解决方案是什么?

如果你能找到一个永远不能合法存在于 col3 中的值(确保有检查约束),你可以使用唯一索引:

CREATE UNIQUE INDEX ON my_table (
   col2,
   coalesce(col3, -1.0)
);

并在您的 INSERT:

中使用它
INSERT INTO my_table (col2, col3, col4)
VALUES (p_col2, p_col3, p_col4)
ON CONFLICT (col2, coalesce(col3, -1.0))
DO UPDATE SET col4 = excluded.col4;

Postgres 15

... 添加子句 NULLS NOT DISTINCT。您的案例现在开箱即用:

ALTER TABLE my_table
  DROP CONSTRAINT IF EXISTS ux_my_table_unique
, ADD CONSTRAINT ux_my_table_unique UNIQUE NULLS NOT DISTINCT (col2, col3);

INSERT INTO my_table (col2, col3, col4)
VALUES (p_col2, p_col3, p_col4)
ON     CONFLICT (col2, col3) DO UPDATE
SET    col4 = EXCLUDED.col4;

参见:

  • Create unique constraint with null columns

Postgres 14 或更早版本

NULL 值被认为彼此不相等,因此永远不会触发 UNIQUE 违规。这意味着,您当前的 table 定义没有按照您所说的去做。 (col2, col3) = (1, NULL) 可以已经有多个行。 ON CONFLICT 在您当前的设置中 col3 IS NULL 永远不会触发。

UNIQUE 约束的 UNIQUE NULLS [NOT] DISTINCT 选项是 in development。但这充其量是在未来的 Postgres 15 中。

您可以使用两个部分 UNIQUE 索引强制执行 UNIQUE 约束,如下所述:

  • Create unique constraint with null columns

适用于您的情况:

CREATE UNIQUE INDEX my_table_col2_uni_idx ON my_table (col2)
WHERE col3 IS NULL;

CREATE UNIQUE INDEX my_table_col2_col3_uni_idx ON my_table (col2, col3)
WHERE col3 IS NOT NULL;

但是ON CONFLICT ... DO UPDATE只能基于单个UNIQUE索引或约束。只有 ON CONFLICT DO NOTHING 变体可以作为“包罗万象”。参见:

看起来你想要的目前是不可能的...

完美解决

虽然有一个完美的解决方案。有了两个部分 UNIQUE 索引,您可以根据 col3:

的输入值使用正确的语句
WITH input(col2, col3, col4) AS (
   VALUES
     (3, NULL::real, 5)  -- ①
   , (3, 4, 5)
   )
, upsert1 AS (
   INSERT INTO my_table AS t(col2, col3, col4)
   SELECT * FROM input          WHERE col3 IS NOT NULL
   ON     CONFLICT (col2, col3) WHERE col3 IS NOT NULL  -- matching index_predicate!
   DO     UPDATE
   SET    col4 = EXCLUDED.col4
   WHERE  t.col4 IS DISTINCT FROM EXCLUDED.col4  -- ②
   )
INSERT INTO my_table AS t(col2, col3, col4)
SELECT * FROM input    WHERE col3 IS NULL
ON     CONFLICT (col2) WHERE col3 IS NULL  -- matching index_predicate!
DO     UPDATE SET col4 = EXCLUDED.col4
WHERE  t.col4 IS DISTINCT FROM EXCLUDED.col4;  -- ②

db<>fiddle here

适用于所有情况。
甚至适用于 col3.
NULLNOT NULL 值的任意组合的多个输入行 而且甚至不会比普通语句花费更多,因为每一行只进入两个 UPSERT 之一。

这是其中一个“Eurika!”查询一切都只是点击,排除万难。 :)

① 注意在 CTE input 中显式转换为 ::real。这个相关的答案解释了原因:

  • Casting NULL type when updating multiple rows

② 最后的 WHERE 子句是可选的,但强烈推荐。如果 UPDATE 实际上没有改变任何东西,那将是一种浪费。参见:

  • How do I (or can I) SELECT DISTINCT on multiple columns?