현재 허용 대답은 하나의 충돌 대상, 몇 충돌, 작은 튜플없이 트리거에 대한 확인을 보인다. 무차별 강제로 동시성 문제 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
.
ON CONFLICT UPDATE
행이 변경되도록 사용하십시오 . 그런 다음RETURNING
캡처합니다.