PostgreSQL에서 ON CONFLICT와 함께 RETURNING을 사용하는 방법은 무엇입니까?


149

PostgreSQL 9.5에는 다음과 같은 UPSERT가 있습니다.

INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO NOTHING
RETURNING id;

충돌이 없으면 다음과 같이 반환됩니다.

----------
    | id |
----------
  1 | 50 |
----------
  2 | 51 |
----------

그러나 충돌이 있으면 행을 반환하지 않습니다.

----------
    | id |
----------

id충돌이 없으면 새 열 을 반환 하거나 충돌하는 id열의 기존 열 을 반환하고 싶습니다 .
이것을 할 수 있습니까? 그렇다면 어떻게?


1
ON CONFLICT UPDATE행이 변경되도록 사용하십시오 . 그런 다음 RETURNING캡처합니다.
Gordon

1
@GordonLinoff 업데이트 할 것이 없다면?
Okku

1
업데이트 할 것이 없다면 충돌이 없었 음을 의미하므로 새로운 값을 삽입하고 ID를 반환합니다.
zola

1
다른 방법은 여기에서 찾을 수 있습니다 . 그래도 성능 측면에서 두 가지의 차이점을 알고 싶습니다.
Stanislasdrg 복원 모니카

답변:


88

나는 정확히 같은 문제가 있었고, 업데이트 할 것이 없어도 '아무것도하지 않음'대신 '업데이트 수행'을 사용하여 해결했습니다. 귀하의 경우 다음과 같습니다.

INSERT INTO chats ("user", "contact", "name") 
       VALUES ($1, $2, $3), 
              ($2, $1, NULL) 
ON CONFLICT("user", "contact") DO UPDATE SET name=EXCLUDED.name RETURNING id;

이 쿼리는 방금 삽입했거나 이전에 존재했던 모든 행을 반환합니다.


11
이 접근 방식의 한 가지 문제점은 기본 키의 시퀀스 번호가 모든 충돌 (보그 업데이트)마다 증가한다는 것입니다. 이는 기본적으로 시퀀스에 큰 간격이 생길 수 있음을 의미합니다. 어떻게 피할 수 있습니까?
Mischa

9
@Mischa : 그래서 무엇? 시퀀스는 처음부터 틈새를 보장하지 않으며 틈새는 중요하지 않습니다 (그렇다면 시퀀스가 ​​잘못된 것입니다)
a_horse_with_no_name

24
나는 대부분의 경우 이것을 사용 하지 않는 것이 좋습니다. 나는 왜 대답을 추가했습니다.
Erwin Brandstetter 2012

4
이 답변은 DO NOTHING원래 질문 의 측면 을 달성하는 것으로 보이지 않습니다. 나에게 모든 행에 대해 충돌하지 않는 필드 (여기서는 "이름")를 업데이트하는 것으로 보입니다.
PeterJCLaw

아래의 매우 긴 답변에서 설명한 것처럼 변경되지 않은 필드에 "업데이트 수행"을 사용하는 것은 "깨끗한"솔루션이 아니며 다른 문제를 일으킬 수 있습니다.
Bill Worthington

202

현재 허용 대답은 하나의 충돌 대상, 몇 충돌, 작은 튜플없이 트리거에 대한 확인을 보인다. 무차별 강제로 동시성 문제 1 (아래 참조)을 피 합니다 . 간단한 해결책은 매력이 있으며 부작용은 덜 중요 할 수 있습니다.

그러나 다른 모든 경우에는 필요없이 동일한 행을 업데이트 하지 마십시오 . 표면에 차이가 보이지 않더라도 다양한 부작용이 있습니다 .

  • 발사해서는 안되는 방아쇠를 발사 할 수 있습니다.

  • "무고한"행을 쓰기 잠금하여 동시 트랜잭션에 비용이 발생할 수 있습니다.

  • 오래되었지만 행이 새로운 것처럼 보일 수 있습니다 (트랜잭션 타임 스탬프).

  • 가장 중요한 것은 , 함께 PostgreSQL을의 MVCC 모델 에 새로운 행 버전은 모든 위해 작성 UPDATE, 행 데이터가 변경되었는지 여부에 상관없이. 이로 인해 UPSERT 자체에 대한 성능 저하, 테이블 팽창, 인덱스 팽창, 테이블에서의 후속 작업에 대한 성능 저하, VACUUM비용이 발생합니다. 몇 번의 복제에는 미미한 효과가 있지만 대부분의 듀피 에는 거대 합니다.

또한 때로는 실용적이지 않거나 사용하기가 불가능합니다 ON CONFLICT DO UPDATE. 매뉴얼 :

를 들어 ON CONFLICT DO UPDATE, A를 conflict_target제공해야합니다.

여러 인덱스 / 제약 참여하는 경우 "충돌 대상"수 없습니다.

빈 업데이트 나 부작용없이 거의 동일한 결과를 얻을 수 있습니다. 다음 해결책 중 일부는 ON CONFLICT DO NOTHING(충돌 대상이 아님) 함께 작동하여 발생할 수있는 모든 가능한 충돌 을 잡을 수 있습니다.

동시 쓰기로드없이

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, ins AS (
   INSERT INTO chats (usr, contact, name) 
   SELECT * FROM input_rows
   ON CONFLICT (usr, contact) DO NOTHING
   RETURNING id  --, usr, contact              -- return more columns?
   )
SELECT 'i' AS source                           -- 'i' for 'inserted'
     , id  --, usr, contact                    -- return more columns?
FROM   ins
UNION  ALL
SELECT 's' AS source                           -- 's' for 'selected'
     , c.id  --, usr, contact                  -- return more columns?
FROM   input_rows
JOIN   chats c USING (usr, contact);           -- columns of unique index

source열은 작동 방식을 보여주기 위해 선택 사항으로 추가되었습니다. 실제로 두 경우의 차이를 알려주기 위해 필요할 수 있습니다 (빈 쓰기보다 다른 이점).

JOIN chats연결된 데이터 수정 CTE 에서 새로 삽입 된 행 이 아직 기본 테이블에 표시되지 않기 때문에 최종 작업 이 이루어집니다. 동일한 SQL 문의 모든 부분은 동일한 기본 테이블의 스냅 샷을 참조합니다.

VALUES표현식은 독립형 이므로 (직접 첨부되지 않음 INSERT) Postgres는 대상 열에서 데이터 유형을 파생 할 수 없으므로 명시 적 유형 캐스트를 추가해야 할 수도 있습니다. 매뉴얼 :

경우 VALUES에 사용되어 INSERT, 값이 모두 자동으로 해당 대상 칼럼의 데이터 형식으로 강제된다. 다른 컨텍스트에서 사용될 경우 올바른 데이터 유형을 지정해야 할 수도 있습니다. 항목이 모두 인용 된 리터럴 상수 인 경우 첫 번째 항목을 강제 변환하면 모든 가정 유형을 판별하기에 충분합니다.

쿼리 자체가 (부작용을 계산하지 않음)에 대한 조금 더 비싼있을 수 있습니다 인해 CTE의 오버 헤드와 추가로, 속는 SELECT고유 제한 조건이 구현된다 - 정의가 완벽한 인덱스 이후 저렴해야하는 ( 색인).

많은 중복에 대해 훨씬 빠를 수 있습니다 . 추가 쓰기의 유효 비용은 여러 요인에 따라 다릅니다.

그러나 어떤 경우에도 부작용과 숨겨진 비용적습니다 . 전체적으로 가장 저렴합니다.

충돌을 테스트 하기 전에 기본값이 채워 지므로 첨부 된 시퀀스는 계속 진행 됩니다.

CTE 정보 :

동시 쓰기로드

기본 READ COMMITTED트랜잭션 격리를 가정 합니다 . 관련 :

경쟁 조건을 방어하는 가장 좋은 전략은 정확한 요구 사항, 테이블 및 UPSERT의 행 수 및 크기, 동시 트랜잭션 수, 충돌 가능성, 사용 가능한 리소스 및 기타 요인에 따라 다릅니다.

동시성 문제 1

트랜잭션이 UPSERT하려고 시도하는 행에 동시 트랜잭션이 작성된 경우 트랜잭션은 다른 트랜잭션이 완료 될 때까지 기다려야합니다.

다른 거래가 ROLLBACK(또는 오류, 즉 오류)로 끝나면 ROLLBACK거래가 정상적으로 진행될 수 있습니다. 사소한 부작용 : 일련 번호의 간격. 그러나 행이 누락되지 않았습니다.

다른 트랜잭션이 정상적으로 종료되면 (암시 적 또는 명시 적 COMMIT) INSERT충돌을 감지하고 ( UNIQUE인덱스 / 제약은 절대) DO NOTHING행을 반환하지 않습니다. ( 아래의 동시성 문제 2 에서 보여 지듯이 행을 잠글 수는 없습니다 . 표시되지 않기 때문 입니다.) SELECT쿼리 시작에서 동일한 스냅 샷을보고 아직 보이지 않는 행을 반환 할 수도 없습니다.

기본 테이블에 존재하더라도 이러한 행은 결과 집합에서 누락됩니다!

이것은 괜찮을 수도 있습니다 . 특히 예제와 같이 행을 반환하지 않고 행이 있다는 것을 알고 만족하는 경우. 충분하지 않으면 여러 가지 방법이 있습니다.

출력의 행 수를 확인하고 입력의 행 수와 일치하지 않으면 명령문을 반복 할 수 있습니다. 드문 경우에 충분할 수 있습니다. 요점은 새로운 쿼리를 시작하는 것입니다 (같은 트랜잭션에있을 수 있음). 그러면 새로 커밋 된 행이 표시됩니다.

또는 동일한 쿼리 에서 누락 된 결과 행을 확인 하고 Alextoni의 답변 에서 입증 된 무차별 대입 트릭으로 덮어 씁니다 .

WITH input_rows(usr, contact, name) AS ( ... )  -- see above
, ins AS (
   INSERT INTO chats AS c (usr, contact, name) 
   SELECT * FROM input_rows
   ON     CONFLICT (usr, contact) DO NOTHING
   RETURNING id, usr, contact                   -- we need unique columns for later join
   )
, sel AS (
   SELECT 'i'::"char" AS source                 -- 'i' for 'inserted'
        , id, usr, contact
   FROM   ins
   UNION  ALL
   SELECT 's'::"char" AS source                 -- 's' for 'selected'
        , c.id, usr, contact
   FROM   input_rows
   JOIN   chats c USING (usr, contact)
   )
, ups AS (                                      -- RARE corner case
   INSERT INTO chats AS c (usr, contact, name)  -- another UPSERT, not just UPDATE
   SELECT i.*
   FROM   input_rows i
   LEFT   JOIN sel   s USING (usr, contact)     -- columns of unique index
   WHERE  s.usr IS NULL                         -- missing!
   ON     CONFLICT (usr, contact) DO UPDATE     -- we've asked nicely the 1st time ...
   SET    name = c.name                         -- ... this time we overwrite with old value
   -- SET name = EXCLUDED.name                  -- alternatively overwrite with *new* value
   RETURNING 'u'::"char" AS source              -- 'u' for updated
           , id  --, usr, contact               -- return more columns?
   )
SELECT source, id FROM sel
UNION  ALL
TABLE  ups;

위의 쿼리와 같지만 완전한 결과 집합을 ups반환하기 전에 CTE로 한 단계 더 추가 합니다. 마지막 CTE는 대부분의 시간 동안 아무것도하지 않습니다. 반환 된 결과에서 행이 누락 된 경우에만 무차별 강제를 사용합니다.

더 많은 오버 헤드. 기존 행과의 충돌이 많을수록 간단한 접근 방식보다 성능이 뛰어납니다.

한 가지 부작용 : 두 번째 UPSERT는 행을 순서대로 쓰지 않으므로 동일한 행에 쓰거나 세 개 이상의 트랜잭션이 겹치는 경우 교착 상태 (아래 참조)를 다시 도입합니다 . 이것이 문제라면 위에서 언급 한 것처럼 전체 문장을 반복하는 것과 같은 다른 솔루션이 필요합니다.

동시성 문제 2

동시 트랜잭션이 영향을받는 행의 관련 열에 쓸 수 있고 찾은 행이 동일한 트랜잭션의 이후 단계에 여전히 있는지 확인해야하는 경우 CTE에서 기존 행을 저렴하게 잠글 수 있습니다 ins(그렇지 않으면 잠금 해제 됨) 와:

...
ON CONFLICT (usr, contact) DO UPDATE
SET name = name WHERE FALSE  -- never executed, but still locks the row
...

그리고 추가 받는 잠금 절을 SELECT같이뿐만 아니라FOR UPDATE .

이렇게하면 모든 잠금이 해제 될 때 경쟁 쓰기 작업이 트랜잭션이 끝날 때까지 대기합니다. 간단하게

자세한 내용과 설명 :

교착 상태?

일관된 순서로 행을 삽입 하여 교착 상태 에 대비하십시오 . 보다:

데이터 유형 및 캐스트

데이터 형식의 템플릿으로 존재하는 기존 테이블 ...

독립형 VALUES표현식 에서 첫 번째 데이터 행에 대한 명시 적 유형 캐스트는 불편할 수 있습니다. 그 주위에 방법이 있습니다. 기존 관계 (테이블, 뷰, ...)를 행 템플릿으로 사용할 수 있습니다. 목표 테이블은 유스 케이스에 대한 확실한 선택입니다. 입력 데이터는 다음의 VALUES절 에서와 같이 자동으로 적절한 유형으로 강제됩니다 INSERT.

WITH input_rows AS (
  (SELECT usr, contact, name FROM chats LIMIT 0)  -- only copies column names and types
   UNION ALL
   VALUES
      ('foo1', 'bar1', 'bob1')  -- no type casts here
    , ('foo2', 'bar2', 'bob2')
   )
   ...

일부 데이터 유형에서는 작동하지 않습니다. 보다:

...과 이름

이것은 모든 데이터 유형 에도 적용 됩니다.

테이블의 모든 선행 열에 삽입하는 동안 열 이름을 생략 할 수 있습니다. chats이 예에서 표 는 UPSERT에 사용 된 3 개의 열로만 구성되어 있다고 가정합니다 .

WITH input_rows AS (
   SELECT * FROM (
      VALUES
      ((NULL::chats).*)         -- copies whole row definition
      ('foo1', 'bar1', 'bob1')  -- no type casts needed
    , ('foo2', 'bar2', 'bob2')
      ) sub
   OFFSET 1
   )
   ...

따로 : 식별자 와 같은 예약어를 사용하지 마십시오 "user". 로드 된 풋건입니다. 소문자로 인용되지 않은 유효한 식별자를 사용하십시오. 로 교체했습니다 usr.


2
이 방법은 일련 번호에 간격을 만들지 않습니다 의미하지만은 다음과 같습니다 INSERT ... ON CONFLICT DO NOTHING 직렬에게 내가 볼 수있는 것과 때마다 증가하지 않습니다
harmic

1
그다지 중요하지 않지만 왜 일련 번호가 증가합니까? 이것을 피할 방법이 없습니까?
두드러진

1
@ salient : 위에서 추가 한 것처럼 : 동시 쓰기와의 충돌을 피하기 위해 충돌 기본값 테스트 하기 전에 열 기본값이 채워 지고 시퀀스가 ​​롤백되지 않습니다.
Erwin Brandstetter

7
놀랄 만한. 주의 깊게 보면 매력처럼 작동하고 이해하기 쉽습니다. 나는 여전히 ON CONFLICT SELECT...어떤 것이 있지만 :)
Roshambo

3
놀랄 만한. Postgres의 제작자는 사용자를 고문하는 것 같습니다. 단순히 삽입 반환 여부에 관계없이 returning 절이 항상 값 을 반환 하도록 만드는 것은 어떻습니까?
Anatoly Alekseev

16

Upsert는의 확장 된 INSERT쿼리는 제약 조건 충돌의 경우에 두 개의 서로 다른 행동을 정의 할 수 있습니다 : DO NOTHINGDO UPDATE.

INSERT INTO upsert_table VALUES (2, 6, 'upserted')
   ON CONFLICT DO NOTHING RETURNING *;

 id | sub_id | status
----+--------+--------
 (0 rows)

주뿐만 아니라 그 RETURNING아무것도 반환, 튜플가 삽입되지 되었기 때문에 . 이제와 DO UPDATE충돌하는 튜플에서 작업을 수행 할 수 있습니다. 먼저 충돌이 있음을 정의하는 데 사용될 제약 조건을 정의하는 것이 중요합니다.

INSERT INTO upsert_table VALUES (2, 2, 'inserted')
   ON CONFLICT ON CONSTRAINT upsert_table_sub_id_key
   DO UPDATE SET status = 'upserted' RETURNING *;

 id | sub_id |  status
----+--------+----------
  2 |      2 | upserted
(1 row)

2
영향을받는 행 ID를 항상 얻고 삽입인지 또는 upsert인지 알 수있는 좋은 방법입니다. 내가 필요한 것만
Moby Duck

이것은 여전히 ​​"업데이트 수행"을 사용하고 있으며, 단점은 이미 논의되었습니다.
Bill Worthington

4

단일 항목을 삽입하려면 id를 반환 할 때 병합을 사용합니다.

WITH new_chats AS (
    INSERT INTO chats ("user", "contact", "name")
    VALUES ($1, $2, $3)
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
) SELECT COALESCE(
    (SELECT id FROM new_chats),
    (SELECT id FROM chats WHERE user = $1 AND contact = $2)
);

2
WITH e AS(
    INSERT INTO chats ("user", "contact", "name") 
           VALUES ($1, $2, $3), 
                  ($2, $1, NULL) 
    ON CONFLICT("user", "contact") DO NOTHING
    RETURNING id
)
SELECT * FROM e
UNION
    SELECT id FROM chats WHERE user=$1, contact=$2;

사용의 주요 목적은 ON CONFLICT DO NOTHING오류 발생을 피하는 것이지만 행 리턴이 발생하지 않습니다. 따라서 SELECT기존 ID를 얻으려면 다른 것이 필요합니다 .

이 SQL에서 충돌로 실패하면 아무것도 반환하지 않고 두 번째 SELECT는 기존 행을 가져옵니다. 성공적으로 삽입되면 동일한 레코드가 두 개가 UNION되고 결과를 병합 해야 합니다.


이 솔루션은 잘 작동하며 DB에 대한 불필요한 쓰기 (업데이트)를 피합니다 !! 좋은!
Simon C

0

Erwin Brandstetter의 놀라운 답변을 수정하여 시퀀스를 늘리지 않으며 행을 쓰지 않습니다. 나는 비교적 PostgreSQL을 처음 사용하기 때문에이 방법에 대한 단점이 있으면 언제든지 알려주십시오.

WITH input_rows(usr, contact, name) AS (
   VALUES
      (text 'foo1', text 'bar1', text 'bob1')  -- type casts in first row
    , ('foo2', 'bar2', 'bob2')
    -- more?
   )
, new_rows AS (
   SELECT 
     c.usr
     , c.contact
     , c.name
     , r.id IS NOT NULL as row_exists
   FROM input_rows AS r
   LEFT JOIN chats AS c ON r.usr=c.usr AND r.contact=c.contact
   )
INSERT INTO chats (usr, contact, name)
SELECT usr, contact, name
FROM new_rows
WHERE NOT row_exists
RETURNING id, usr, contact, name

이것은 테이블 chats에 열에 대한 고유 제한 조건 이 있다고 가정합니다 (usr, contact).

업데이트 : 스파 타 에서 제안 된 수정 사항을 추가했습니다 (아래). 감사!


1
CASE WHEN r.id IS NULL THEN FALSE ELSE TRUE END AS row_exists그냥 쓰지 말고 r.id IS NOT NULL as row_exists. WHERE row_exists=FALSE그냥 쓰지 말고 WHERE NOT row_exists.
스파 타
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.