基于具有 NULL 值的 UNIQUE 约束的 UPSERT

use*_*146 1 sql postgresql null unique upsert

我有一个 Postgres 表,对多个列有唯一约束,其中一列可以为 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)
);
Run Code Online (Sandbox Code Playgroud)

我有一个 upsert 查询,当它遇到 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;
Run Code Online (Sandbox Code Playgroud)

但当 col3 为 NULL 时,冲突不会触发。我读过有关使用触发器的内容。请问,引发冲突的最佳解决方案是什么?

Lau*_*lbe 5

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

CREATE UNIQUE INDEX ON my_table (
   col2,
   coalesce(col3, -1.0)
);
Run Code Online (Sandbox Code Playgroud)

并将其用于您的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;
Run Code Online (Sandbox Code Playgroud)


Erw*_*ter 5

Postgres 15

\n

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

\n
ALTER TABLE my_table\n  DROP CONSTRAINT IF EXISTS ux_my_table_unique\n, ADD CONSTRAINT ux_my_table_unique UNIQUE NULLS NOT DISTINCT (col2, col3);\n\nINSERT INTO my_table (col2, col3, col4)\nVALUES (p_col2, p_col3, p_col4)\nON     CONFLICT (col2, col3) DO UPDATE\nSET    col4 = EXCLUDED.col4;\n
Run Code Online (Sandbox Code Playgroud)\n

看:

\n\n

Postgres 14 或以上

\n

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

\n

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

\n\n

适用于您的案例:

\n
CREATE UNIQUE INDEX my_table_col2_uni_idx ON my_table (col2)\nWHERE col3 IS NULL;\n\nCREATE UNIQUE INDEX my_table_col2_col3_uni_idx ON my_table (col2, col3)\nWHERE col3 IS NOT NULL;\n
Run Code Online (Sandbox Code Playgroud)\n

ON CONFLICT ... DO UPDATE只能基于单个 UNIQUE索引或约束。只有ON CONFLICT DO NOTHING变体才能起到“包罗万象”的作用。看:

\n\n

看起来你想要的目前是不可能,但是有一个......

\n

完美解决方案

\n

两个部分UNIQUE索引就位后,您可以根据 的输入值使用正确的语句col3

\n
WITH input(col2, col3, col4) AS (\n   VALUES\n     (3, NULL::real, 5)  -- \xe2\x91\xa0\n   , (3, 4, 5)\n   )\n, upsert1 AS (\n   INSERT INTO my_table AS t(col2, col3, col4)\n   SELECT * FROM input          WHERE col3 IS NOT NULL\n   ON     CONFLICT (col2, col3) WHERE col3 IS NOT NULL  -- matching index_predicate!\n   DO     UPDATE\n   SET    col4 = EXCLUDED.col4\n   WHERE  t.col4 IS DISTINCT FROM EXCLUDED.col4  -- \xe2\x91\xa1\n   )\nINSERT INTO my_table AS t(col2, col3, col4)\nSELECT * FROM input    WHERE col3 IS NULL\nON     CONFLICT (col2) WHERE col3 IS NULL  -- matching index_predicate!\nDO     UPDATE SET col4 = EXCLUDED.col4\nWHERE  t.col4 IS DISTINCT FROM EXCLUDED.col4;  -- \xe2\x91\xa1\n
Run Code Online (Sandbox Code Playgroud)\n

db<>在这里摆弄

\n

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

\n

这是其中之一“尤里卡!” 尽管困难重重,但一切都会点击的查询。:)

\n

::real\xe2\x91\xa0 请注意CTE 中的显式转换input。这个相关答案解释了原因:

\n\n

\xe2\x91\xa1 最后一个WHERE子句是可选的,但强烈推荐。UPDATE如果它实际上没有改变任何东西,那将是一种浪费。看:

\n\n