在 ON CONFLICT 子句中使用多个冲突目标

Posted

技术标签:

【中文标题】在 ON CONFLICT 子句中使用多个冲突目标【英文标题】:Use multiple conflict_target in ON CONFLICT clause 【发布时间】:2016-06-23 14:47:49 【问题描述】:

我在表col1col2 中有两列,它们都是唯一索引(col1 是唯一的,col2 也是唯一的)。

我需要在插入此表时使用ON CONFLICT 语法并更新其他列,但我不能同时使用conflict_targetclause 中的两个列。

有效:

INSERT INTO table
...
ON CONFLICT ( col1 ) 
DO UPDATE 
SET 
-- update needed columns here

但是如何对多个列执行此操作,如下所示:

...
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 
....

【问题讨论】:

"col1, col2,它们都是唯一索引。"这是否意味着 col1 是唯一的,而 col2 是唯一的,还是 col1,col2 的组合是唯一的? 这是否意味着 col1 是唯一的,而 col2 是唯一的,单独的 【参考方案1】:

ON CONFLICT 需要唯一索引* 来进行冲突检测。所以你只需要在两列上创建一个唯一索引:

t=# create table t (id integer, a text, b text);
CREATE TABLE
t=# create unique index idx_t_id_a on t (id, a);
CREATE INDEX
t=# insert into t values (1, 'a', 'foo');
INSERT 0 1
t=# insert into t values (1, 'a', 'bar') on conflict (id, a) do update set b = 'bar';
INSERT 0 1
t=# select * from t;
 id | a |  b  
----+---+-----
  1 | a | bar

* 除了唯一索引,您还可以使用exclusion constraints。这些比唯一约束更通用。假设您的表有idvalid_time 的列(并且valid_timetsrange),并且您希望允许重复的ids,但不允许重叠的时间段。唯一约束对您没有帮助,但是通过排除约束,您可以说“如果新记录的 id 等于旧的 id 并且它们的 valid_time 与其 valid_time 重叠,则排除新记录。”

【讨论】:

这会创建一个共同的唯一索引 create unique index idx_t_id_a on t (id, a);当然,OP 并没有明确说明这两列是单独还是一起是唯一的。 为什么postgres有时会说没有以索引命名的列,无法使用ON CONFLICT @Pak 听起来您应该使用您正在使用的特定命令和收到的错误消息编写自己的问题。 @PaulAJungwirth 我不知道,你的答案是正确的 - 唯一索引作为 on conflict 命令的约束。错误只是“列 my_index_name 不存在”。 我确实尝试过这个,正如 OP 要求的那样,在每一列上都有一个单独的唯一约束,但它没有用。不是我期望的,而是我希望的。【参考方案2】:

示例表和数据

CREATE TABLE dupes(col1 int primary key, col2 int, col3 text,
   CONSTRAINT col2_unique UNIQUE (col2)
);

INSERT INTO dupes values(1,1,'a'),(2,2,'b');

重现问题

INSERT INTO dupes values(3,2,'c')
ON CONFLICT (col1) DO UPDATE SET col3 = 'c', col2 = 2

我们称之为 Q1。结果是

ERROR:  duplicate key value violates unique constraint "col2_unique"
DETAIL:  Key (col2)=(2) already exists.

documentation 说什么

conflict_target 可以执行唯一索引推断。表演时 推断,它由一个或多个 index_column_name 列和/或 index_expression 表达式和可选的 index_predicate。全部 table_name 唯一索引,不考虑顺序,包含 准确推断冲突目标指定的列/表达式 (选择)作为仲裁索引。如果指定了 index_predicate,则 作为推理的进一步要求,必须满足仲裁索引。

这给人的印象是下面的查询应该可以工作,但事实并非如此,因为它实际上需要 col1 和 col2 上的唯一索引。然而,这样的索引并不能保证 col1 和 col2 单独是唯一的,这是 OP 的要求之一。

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT (col1,col2) DO UPDATE SET col3 = 'c', col2 = 2

让我们将此查询称为 Q2(此查询因语法错误而失败)

为什么?

Postgresql 这样做是因为没有很好地定义第二列发生冲突时应该发生的情况。有多种可能性。比如上面的Q1查询,当col2有冲突时,postgresql是否应该更新col1?但是,如果这导致col1 上的另一个冲突怎么办? postgresql 应该如何处理呢?

解决方案

解决方案是将 ON CONFLICT 与 old fashioned UPSERT 结合使用。

CREATE OR REPLACE FUNCTION merge_db(key1 INT, key2 INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE dupes SET col3 = data WHERE col1 = key1 and col2 = key2;
        IF found THEN
            RETURN;
        END IF;

        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently, or key2
        -- already exists in col2,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col1) DO UPDATE SET col3 = data;
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            BEGIN
                INSERT INTO dupes VALUES (key1, key2, data) ON CONFLICT (col2) DO UPDATE SET col3 = data;
                RETURN;
            EXCEPTION WHEN unique_violation THEN
                -- Do nothing, and loop to try the UPDATE again.
            END;
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

您需要修改此存储函数的逻辑,以便它完全按照您希望的方式更新列。像这样调用它

SELECT merge_db(3,2,'c');
SELECT merge_db(1,2,'d');

【讨论】:

这是可行的方法,但比必要的工作/逻辑多一点,您真正要做的就是在两列上创建一个唯一的约束。请参阅下面的答案。 如果我同时插入多组 VALUES,是否也可以使用 merge_db 解决方案? @daniyel 你将不得不重写存储的函数 我不清楚建议使用老式 upsert 有什么用 - 这个问题在“postgres upsert 9.5”中有很好的参考,通过解释如何将它与所有约束名称一起使用可能会更好选项。 @Pak 您不清楚,因为您没有清楚地阅读问题。该操作不是在这些字段上寻找复合键。另一个答案适用于复合键【参考方案3】:

在当今(似乎)是不可能的。 ON CONFLICT syntax 的最后一个版本不允许重复该子句,CTE 也不可能:不可能从 ON CONFLICT 中断 INSERT 以添加更多冲突目标。

【讨论】:

【参考方案4】:

如果您使用的是 postgres 9.5,则可以使用 EXCLUDED 空间。

示例取自What's new in PostgreSQL 9.5:

INSERT INTO user_logins (username, logins)
VALUES ('Naomi',1),('James',1)
ON CONFLICT (username)
DO UPDATE SET logins = user_logins.logins + EXCLUDED.logins;

【讨论】:

【参考方案5】:

弗拉德的想法是对的。

首先,您必须在列col1, col2 上创建一个表唯一约束,然后您可以执行以下操作:

INSERT INTO dupes values(3,2,'c') 
ON CONFLICT ON CONSTRAINT dupes_pkey 
DO UPDATE SET col3 = 'c', col2 = 2

【讨论】:

抱歉,您误解了这个问题。 OP 不想要一个共同的唯一约束。【参考方案6】:
    创建约束(例如,外部索引)。

或/与

    查看现有约束(psq 中的 \d)。 在 INSERT 子句中使用 ON CONSTRAINT(constraint_name)。

【讨论】:

【参考方案7】:

有点老套,但我通过将 col1 和 col2 中的两个值连接到一个新列 col3(有点像两者的索引)并与之进行比较来解决这个问题。这仅在您需要它同时匹配 col1 和 col2 时才有效。

INSERT INTO table
...
ON CONFLICT ( col3 ) 
DO UPDATE 
SET 
-- update needed columns here

其中 col3 = col1 和 col2 的值的串联。

【讨论】:

您可以为这两列创建一个唯一索引,并在on conflict 中给出该约束。 @KishoreRelangi 如果他们没有唯一索引而不是普通索引怎么办?【参考方案8】:

您通常(我认为)可以生成一个仅包含一个 on conflict 的语句,该语句为您插入的内容指定唯一且唯一的相关约束。

因为通常情况下,一次只有一个约束是“相关”的。 (如果很多,那么我想知道是不是有些奇怪/奇怪的设计,嗯。)

示例:(许可证: CC0,仅 CC-By)

// there're these unique constraints:
//   unique (site_id, people_id, page_id)
//   unique (site_id, people_id, pages_in_whole_site)
//   unique (site_id, people_id, pages_in_category_id)
// and only *one* of page-id, category-id, whole-site-true/false
// can be specified. So only one constraint is "active", at a time.

val thingColumnName = thingColumnName(notfificationPreference)

val insertStatement = s"""
  insert into page_notf_prefs (
    site_id,
    people_id,
    notf_level,
    page_id,
    pages_in_whole_site,
    pages_in_category_id)
  values (?, ?, ?, ?, ?, ?)
  -- There can be only one on-conflict clause.
  on conflict (site_id, people_id, $thingColumnName)   <—— look
  do update set
    notf_level = excluded.notf_level
  """

val values = List(
  siteId.asAnyRef,
  notfPref.peopleId.asAnyRef,
  notfPref.notfLevel.toInt.asAnyRef,
  // Only one of these is non-null:
  notfPref.pageId.orNullVarchar,
  if (notfPref.wholeSite) true.asAnyRef else NullBoolean,
  notfPref.pagesInCategoryId.orNullInt)

runUpdateSingleRow(insertStatement, values)

还有:

private def thingColumnName(notfPref: PageNotfPref): String =
  if (notfPref.pageId.isDefined)
    "page_id"
  else if (notfPref.pagesInCategoryId.isDefined)
    "pages_in_category_id"
  else if (notfPref.wholeSite)
    "pages_in_whole_site"
  else
    die("TyE2ABK057")

on conflict 子句是动态生成的,具体取决于我要执行的操作。如果我要为页面插入通知首选项,那么在site_id, people_id, page_id 约束上可能存在唯一冲突。如果我正在为一个类别配置通知首选项,那么我知道可能违反的约束是site_id, people_id, category_id

所以我可以,而且很可能你也可以,在你的情况下?生成正确的on conflict (... columns ),因为我知道我想要做什么,然后我知道哪一个许多独特的约束,是可以被违反的。

【讨论】:

【参考方案9】:

我知道我迟到了,但对于寻找答案的人,我发现了这个: here

INSERT INTO tbl_Employee 
VALUES (6,'Noor')
ON CONFLICT (EmpID,EmpName)
DO NOTHING;

【讨论】:

Ben,很遗憾,这是错误的,因为您忽略了用户需要在 EmpId 和 EmpNAme 列上添加约束。【参考方案10】:
ON CONFLICT ( col1, col2 )
DO UPDATE 
SET 

工作正常。但您不应更新 col1col2SET 部分。

【讨论】:

【参考方案11】:

ON CONFLICT 是非常笨拙的解决方案,运行

UPDATE dupes SET key1=$1, key2=$2 where key3=$3    
if rowcount > 0    
  INSERT dupes (key1, key2, key3) values ($1,$2,$3);

适用于 Oracle、Postgres 和所有其他数据库

【讨论】:

它不是原子的,因此如果同时有多个连接,它可能会失败并产生错误的结果。

以上是关于在 ON CONFLICT 子句中使用多个冲突目标的主要内容,如果未能解决你的问题,请参考以下文章

Postgres ON CONFLICT 缺少我声明支持唯一索引的主键冲突

Postgresql insert on conflict笔记

如何在 flask_sqlalchemy 中使用 PostgreSQL 的“INSERT...ON CONFLICT”(UPSERT)功能?

Postgresql on conflict do update 设置当前值,原始值,当前值与原始值相加值

Gitgit使用 - 冲突conflict的解决演示

LINQ Join 与 On 子句中的多个条件