INSERT ... ON CONFLICT
INSERT ... ON CONFLICT (Upsert)
INSERT에 충돌 처리 기능을 얹어 주는 upsert 절이에요. UNIQUE나 PRIMARY KEY 제약 위반이 예상될 때 오류로 실패하는 대신, 충돌 행을 건너뛰거나 갱신하도록 지정할 수 있어요.
출처: 문서
본문
upsert 절은 삽입이 UNIQUE나 PRIMARY KEY 제약 조건을 위반하려 할 때 어떻게 할지 지정하는 INSERT의 확장이에요. 오류로 실패하는 대신 문장이 충돌 행을 건너뛰거나 갱신하도록 만들 수 있어요.
Syntax
INSERT INTO table-name [(column-name [, column-name ...])]
VALUES (expression [, expression ...]) [, ...]
ON CONFLICT [(conflict-target)] DO NOTHING
[ON CONFLICT [(conflict-target)] DO NOTHING | DO UPDATE ...]
INSERT INTO table-name [(column-name [, column-name ...])]
VALUES (expression [, expression ...]) [, ...]
ON CONFLICT [(conflict-target)] DO UPDATE
SET column-name = expression [, column-name = expression ...]
[WHERE expression]
[ON CONFLICT [(conflict-target)] DO NOTHING | DO UPDATE ...]
conflict-target은 다음과 같아요.
(column-name [, column-name ...]) [WHERE expression]
Description
upsert 절은 제약 조건 위반을 우아하게 처리하기 위해 INSERT 문 끝에 붙여요. 동작은 두 가지예요. DO NOTHING은 충돌 행을 조용히 건너뛰고, DO UPDATE는 기존 행을 그 자리에서 수정해요.
DO NOTHING
DO NOTHING은 지정한 제약 조건을 위반할 행을 INSERT가 조용히 건너뛰게 해요. 오류가 발생하지 않고 기존 행은 그대로 유지돼요.
충돌 대상(conflict target)을 지정하면 그 특정 제약 조건에서의 위반만 건너뛰기를 유발해요. 충돌 대상을 지정하지 않으면 어떤 제약 위반이든 건너뛰기를 유발해요.
INSERT INTO users (id, name) VALUES (1, 'Alice')
ON CONFLICT DO NOTHING;
DO UPDATE
DO UPDATE는 제약 조건 위반이 발생하면 INSERT가 기존 행을 갱신하게 해요. SET 절이 어떤 컬럼을 어떤 값으로 갱신할지 지정해요. DO UPDATE에는 충돌 대상이 반드시 필요해요.
INSERT INTO counters (name, value) VALUES ('hits', 1)
ON CONFLICT (name) DO UPDATE
SET value = counters.value + 1;
Conflict Target
충돌 대상은 이 절이 어느 UNIQUE 또는 PRIMARY KEY 제약 조건에 적용되는지 식별해요. 테이블에 존재하는 유니크 제약 조건의 컬럼과 정확히 일치해야 하는 컬럼 이름 목록을 괄호로 감싼 형태예요.
부분 유니크 인덱스(WHERE 절이 붙은 인덱스)의 경우, 충돌 대상에 인덱스 조건과 일치하는 WHERE 절도 함께 포함해야 해요.
-- Given: CREATE UNIQUE INDEX idx ON t(a, b) WHERE c > 0
INSERT INTO t VALUES (1, 2, 3)
ON CONFLICT (a, b) WHERE c > 0 DO NOTHING;
The excluded Table
DO UPDATE의 SET과 WHERE 절 안에서는 특별한 excluded 테이블에 삽입하려고 했던 값들이 담겨 있어요. 덕분에 갱신 표현식이 기존 행(테이블 이름 사용)과 삽입하려던 새 행(excluded 사용)을 모두 참조할 수 있어요.
| Reference | Meaning |
|---|---|
table-name.column |
기존 행에서 그 컬럼의 현재 값 |
excluded.column |
삽입하려다 충돌을 일으킨 값 |
INSERT INTO products (sku, name, price) VALUES ('A001', 'Widget', 12.99)
ON CONFLICT (sku) DO UPDATE
SET name = excluded.name,
price = excluded.price;
WHERE Clause on DO UPDATE
DO UPDATE 절에는 갱신이 실제로 일어나는 조건을 더 좁히는 WHERE 표현식을 넣을 수 있어요. 충돌 행에 대해 WHERE 조건이 거짓으로 평가되면 그 행은 그대로 유지돼요(DO NOTHING과 마찬가지로 삽입이 조용히 건너뛰어져요).
INSERT INTO products (sku, name, price) VALUES ('A001', 'Widget', 8.99)
ON CONFLICT (sku) DO UPDATE
SET price = excluded.price
WHERE excluded.price < products.price;
이 예제에서는 새 가격이 기존 가격보다 낮을 때만 가격이 갱신돼요.
Multiple ON CONFLICT Clauses
INSERT 문에 ON CONFLICT 절을 여러 개 둘 수 있고, 각 절은 서로 다른 제약 조건을 겨냥해요. 절은 순서대로 평가되고, 제약 위반이 발생하면 처음으로 일치하는 절이 사용돼요.
INSERT INTO users (id, email, name) VALUES (1, '[email protected]', 'Alice')
ON CONFLICT (id) DO UPDATE SET name = excluded.name, email = excluded.email
ON CONFLICT (email) DO NOTHING;
충돌 대상을 생략할 수 있는 `ON CONFLICT` 절은 최대 하나예요. 충돌 대상이 없는 절은 catch-all처럼 동작하며 반드시 마지막에 나와야 해요. 충돌 대상 없이는 `DO NOTHING`만 허용돼요.
Examples
Upsert a counter
CREATE TABLE counters (name TEXT PRIMARY KEY, value INTEGER NOT NULL DEFAULT 0);
-- 첫 삽입은 행을 만들어요
INSERT INTO counters (name, value) VALUES ('logins', 1)
ON CONFLICT (name) DO UPDATE SET value = counters.value + excluded.value;
SELECT * FROM counters;
-- logins|1
-- 이후의 삽입은 카운터를 증가시켜요
INSERT INTO counters (name, value) VALUES ('logins', 1)
ON CONFLICT (name) DO UPDATE SET value = counters.value + excluded.value;
SELECT * FROM counters;
-- logins|2
Bulk insert, skipping duplicates
CREATE TABLE tags (id INTEGER PRIMARY KEY, label TEXT UNIQUE);
INSERT INTO tags VALUES (1, 'rust');
INSERT INTO tags VALUES (2, 'sql');
-- 여러 태그를 삽입하되 이미 있는 것은 건너뛰어요
INSERT INTO tags VALUES (2, 'sql'), (3, 'wasm'), (4, 'database')
ON CONFLICT DO NOTHING;
SELECT * FROM tags;
-- 1|rust
-- 2|sql
-- 3|wasm
-- 4|database
Conditional update with excluded
CREATE TABLE prices (
product_id INTEGER PRIMARY KEY,
price REAL NOT NULL,
updated_at TEXT NOT NULL
);
INSERT INTO prices VALUES (1, 29.99, '2025-01-01');
-- 새 타임스탬프가 더 최신일 때만 가격을 갱신해요
INSERT INTO prices (product_id, price, updated_at) VALUES (1, 24.99, '2025-06-15')
ON CONFLICT (product_id) DO UPDATE
SET price = excluded.price,
updated_at = excluded.updated_at
WHERE excluded.updated_at > prices.updated_at;
SELECT * FROM prices;
-- 1|24.99|2025-06-15
See Also
- INSERT for standard insert operations
- REPLACE for delete-and-reinsert conflict resolution
- UPDATE for modifying existing rows directly
- CREATE TABLE for defining
UNIQUEandPRIMARY KEYconstraints
더 알아보기 (Learn more)
- INSERT - 표준 INSERT 연산
- REPLACE - 삭제 후 재삽입 방식의 충돌 처리
- UPDATE - 기존 행 직접 수정
- CREATE TABLE -
UNIQUE,PRIMARY KEY제약 정의