PostgreSQL UPSERT问题与NULL值


13

我在使用Postgres 9.5中的新UPSERT功能时遇到问题

我有一个表,用于汇总来自另一个表的数据。复合键由20列组成,其中10列可以为空。下面,我为我遇到的问题创建了一个较小的版本,尤其是NULL值。

CREATE TABLE public.test_upsert (
upsert_id serial,
name character varying(32) NOT NULL,
status integer NOT NULL,
test_field text,
identifier character varying(255),
count integer,
CONSTRAINT upsert_id_pkey PRIMARY KEY (upsert_id),
CONSTRAINT test_upsert_name_status_test_field_key UNIQUE (name, status, test_field)
);

根据需要运行此查询(首先插入,然后随后的插入仅增加计数):

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun',1,'test value','ident', 1)
ON CONFLICT (name,status,test_field) DO UPDATE set count = tu.count + 1 
where tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value';

但是,如果我运行此查询,则每次插入1行,而不是增加初始行的计数:

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun',1,null,'ident', 1)
ON CONFLICT (name,status,test_field) DO UPDATE set count = tu.count + 1  
where tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = null;

这是我的问题。我只需要简单地增加计数值,而不用空值创建多个相同的行。

尝试添加部分唯一索引:

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status, test_field, identifier);

但是,这将产生相同的结果,或者插入多个空行,或者在尝试插入时显示此错误消息:

错误:没有符合ON CONFLICT规范的唯一或排除约束

我已经尝试在部分索引上添加额外的详细信息,例如WHERE test_field is not null OR identifier is not null。但是,在插入时我收到约束错误消息。

Answers:


15

澄清 ON CONFLICT DO UPDATE行为

在这里考虑手册

对于建议插入的每个单独行,要么继续插入,要么如果conflict_target违反仲裁者约束或由指定的索引 ,则采用替代conflict_action方法。

大胆强调我的。因此,您不必为()WHERE子句的唯一索引中包含的列重复谓词:UPDATEconflict_action

INSERT INTO test_upsert AS tu
       (name   , status, test_field  , identifier, count) 
VALUES ('shaun', 1     , 'test value', 'ident'   , 1)
ON CONFLICT (name, status, test_field) DO UPDATE
SET count = tu.count + 1;
WHERE tu.name = 'shaun' AND tu.status = 1 AND tu.test_field = 'test value'

唯一违规已经确定了您添加的内容 WHERE子句将强制执行。

澄清部分指标

添加一个WHERE子句以使其像您自己提到的那样成为实际的部分索引(但逻辑相反):

CREATE UNIQUE INDEX test_upsert_partial_idx
ON public.test_upsert (name, status)
WHERE test_field IS NULL;  -- not: "is not null"

要在您的UPSERT中使用此部分索引,您需要一个类似@ypercube演示的匹配conflict_target

ON CONFLICT (name, status) WHERE test_field IS NULL

现在,推断出上面的部分索引。但是,正如该手册还指出的那样

ON CONFLICT如果有满足其他所有条件的索引,则将推断非局部唯一索引(不带谓词的唯一索引)。

如果仅具有附加(或唯一)索引,(name, status)则将使用(也)索引。上的索引(name, status, test_field)将明确推断。这并不能解释您的问题,但是可能会在测试时增加混乱。

AIUI,以上都不是解决您的问题的方法。使用部分索引,只会捕获具有匹配NULL值的特殊情况。如果没有其他匹配的唯一索引/约束,则将插入其他重复的行;如果没有,则将引发异常。我想那不是你想要的。你写:

复合键由20列组成,其中10列可以为空。

您究竟认为什么重复?Postgres(根据SQL标准)不认为两个NULL值相等。手册:

通常,如果表中有多于一行的行,其中约束中包括的所有列的值均相等,则违反唯一约束。但是,在此比较中,永远不会将两个空值视为相等。这意味着即使在存在唯一约束的情况下,也可以在至少一个约束列中存储包含空值的重复行。此行为符合SQL标准,但是我们听说其他SQL数据库可能不遵循此规则。因此,在开发可移植的应用程序时要小心。

有关:

我假设您希望将NULL所有10个可为空的列中的值都视为相等。用一个额外的部分索引覆盖单个可为空的列是优雅而实用的,如下所示:

但这对于更多可为空的列很快就失去了控制。对于可空列的每种不同组合,您都需要部分索引。对于刚刚那些是3个指数为2 (a)(b)(a,b)。这个数字随着呈指数增长2^n - 1。对于10个可空列,要覆盖NULL值的所有可能组合,您已经需要1023个局部索引。不行

一个简单的解决方案:替换NULL值并定义所涉及的列NOT NULL,使用一个简单的方法,一切都将正常工作UNIQUE约束。

如果那不是一个选择,我建议使用表达式索引COALESCE替换索引中的NULL:

CREATE UNIQUE INDEX test_upsert_solution_idx
    ON test_upsert (name, status, COALESCE(test_field, ''));

空字符串('')很明显是字符类型的候选者,但是您可以根据对“唯一”的定义,使用从不出现或可以用NULL折叠的任何合法值。

然后使用以下语句:

INSERT INTO test_upsert as tu(name,status,test_field,identifier, count) 
VALUES ('shaun', 1, null        , 'ident', 11)  -- works with
     , ('bob'  , 2, 'test value', 'ident', 22)  -- and without NULL
ON     CONFLICT (name, status, COALESCE(test_field, '')) DO UPDATE  -- match expr. index
SET    count = COALESCE(tu.count + EXCLUDED.count, EXCLUDED.count, tu.count);

像@ypercube一样,我假设您实际上要添加count到现有计数中。由于列可以为NULL,因此添加NULL会将列设置为NULL。如果定义count NOT NULL,则可以简化。


另一个想法是从语句中删除冲突目标,以涵盖所有唯一的违规。然后,您可以定义各种唯一索引,以对所谓的“唯一”进行更复杂的定义。但这不会成功ON CONFLICT DO UPDATE。手册再次:

对于ON CONFLICT DO NOTHING,可以指定冲突的目标。如果省略,则处理与所有可用约束(和唯一索引)的冲突。对于ON CONFLICT DO UPDATE必须提供一个冲突目标。


1
真好 第一次阅读该问题时,我跳过了20-10列的内容,后来没有时间完成。的count = CASE WHEN EXCLUDED.count IS NULL THEN tu.count ELSE COALESCE(tu.count, 0) + COALESCE(EXCLUDED.count, 0) END可被简化为count = COALESCE(tu.count+EXCLUDED.count, EXCLUDED.count, tu.count)
ypercubeᵀᴹ

再看一遍,我的“简体”版本不是那么自我记录。
ypercubeᵀᴹ

@ypercubeᵀᴹ:我应用了您建议的更新。更简单,谢谢。
Erwin Brandstetter,2016年

@ErwinBrandstetter,你是最好的
Seamus Abshere

7

我认为问题在于您没有部分索引,ON CONFLICT语法与test_upsert_upsert_id_idx索引不匹配,但与另一个唯一约束不匹配。

如果将索引定义为局部索引(带有WHERE test_field IS NULL):

CREATE UNIQUE INDEX test_upsert_upsert_id_idx
ON public.test_upsert
USING btree
(name COLLATE pg_catalog."default", status)
WHERE test_field IS NULL ;

并且这些行已在表中:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('shaun', 1, null, 'ident', 1),
    ('maria', 1, null, 'ident', 1) ;

那么查询将成功:

INSERT INTO test_upsert as tu
    (name, status, test_field, identifier, count) 
VALUES 
    ('peter', 1,   17, 'ident', 1),
    ('shaun', 1, null, 'ident', 3),
    ('maria', 1, null, 'ident', 7)
ON CONFLICT 
    (name, status) WHERE test_field IS NULL   -- the conflicting condition
DO UPDATE SET
    count = tu.count + EXCLUDED.count 
WHERE                                         -- when to update
    tu.name = 'shaun' AND tu.status = 1 ;     -- if you don't want all of the
                                              -- updates to happen

结果如下:

('peter', 1,   17, 'ident', 1)  -- no conflict: row inserted

('shaun', 1, null, 'ident', 3)  -- conflict: no insert
                           -- matches where: row updated with count = 1+3 = 4

('maria', 1, null, 'ident', 1)  -- conflict: no insert
                     -- doesn't match where: no update

这阐明了如何使用部分索引。但是(我认为)它还不能解决问题。
Erwin Brandstetter,2016年

既然没有更新,“玛丽亚”的计数应该不保持在1吗?
mpprdev

@mpprdev是的,您是对的。
ypercubeᵀᴹ
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.