NULL 값으로 PostgreSQL UPSERT 문제


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;

이것은 내 문제입니다. 단순히 카운트 값을 늘리고 null 값으로 여러 개의 동일한 행을 만들지 않아야합니다.

부분 고유 인덱스를 추가하려고합니다.

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

그러나 이렇게하면 여러 개의 null 행이 삽입되거나 삽입하려고 할 때이 오류 메시지와 동일한 결과가 나타납니다.

오류 : ON CONFLICT 사양과 일치하는 고유 또는 제외 제약 조건이 없습니다.

이미와 같은 부분 인덱스에 추가 세부 정보를 추가하려고했습니다 WHERE test_field is not null OR identifier is not null. 그러나 삽입하면 제약 조건 오류 메시지가 나타납니다.

답변:


15

ON CONFLICT DO UPDATE행동을 명확하게

여기서 매뉴얼을 고려하십시오 .

삽입을 위해 제안 된 각 개별 행에 대해 삽입이 진행되거나,에 의해 지정된 중재자 구속 조건 또는 색인 conflict_target이 위반되는 경우 대안 conflict_action이 사용됩니다.

대담한 강조 광산. 당신은에서 고유 인덱스에 포함 된 컬럼에 대한 조건을 반복 할 필요가 없습니다 WHERE받는 절 UPDATE합니다 ( conflict_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 데이터베이스가이 규칙을 따르지 않을 수 있다고 들었습니다. 따라서 이식성이 뛰어난 응용 프로그램을 개발할 때는주의하십시오.

관련 :

NULL10 개의 nullable 열의 값을 모두 동일한 것으로 간주 한다고 가정 합니다. 여기에 설명 된 것처럼 추가 부분 인덱스로 단일 nullable 열을 덮는 것이 우아하고 실용적입니다.

그러나 더 많은 널 입력 가능 열의 경우 신속하게 처리되지 않습니다. 널 입력 가능 컬럼의 모든 고유 조합에 대해 부분 인덱스가 필요합니다. 만 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하면 단순화 할 수 있습니다.


또 다른 아이디어는 모든 고유 한 위반 을 다루기 위해 statement 에서 conflict_target 을 삭제하는 것 입니다. 그런 다음 "고유 한"것으로보다 정교한 정의를 위해 다양한 고유 색인을 정의 할 수 있습니다. 그러나 그것은와 함께 날지 않을 것입니다 . 매뉴얼을 한 번 더 :ON CONFLICT DO UPDATE

의 경우 ON CONFLICT DO NOTHINGconflict_target을 지정하는 것은 선택 사항입니다. 생략하면 사용 가능한 모든 제약 조건 (및 고유 인덱스)과의 충돌이 처리됩니다. 의 경우 ON CONFLICT DO UPDATEconflict_target 제공 해야 합니다.


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

@ 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

업데이트가 발생하지 않으므로 'maria'의 수가 1로 유지되어서는 안됩니까?
mpprdev

@mpprdev 네, 맞습니다.
ypercubeᵀᴹ
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.