UPSERT 與衝突處理完全指南

INSERT 撞到唯一鍵時怎麼辦:ON CONFLICT、ON DUPLICATE KEY、MERGE 等各資料庫的 UPSERT 寫法與陷阱


目錄


什麼是 UPSERT?

UPSERTUPdate + inSERT 的合稱:嘗試插入一筆資料,若因為唯一鍵 / 主鍵衝突而無法插入,就改為「略過」或「更新既有資料」。

INSERT 一筆資料
├─ 沒衝突 → 正常插入
└─ 撞到唯一鍵 → 不報錯,改為「什麼都不做」或「更新」

ON CONFLICT DO NOTHING 就是其中一種:衝突時直接略過。

核心特點

  • 冪等性(Idempotent):重複執行同一筆 insert 不會報錯、結果一致
  • 原子性:插入或更新在單一語句內完成,避免「先查再寫」的競態
  • 語法各家不同:是 UPSERT 最麻煩的地方——沒有統一寫法

為什麼需要 UPSERT?

沒有 UPSERT 的問題

常見需求:「有就更新、沒有就新增」。傳統做法是先查再寫:

-- ❌ 有競態問題(race condition)
SELECT * FROM users WHERE email = 'a@x.com';
-- 若存在 → UPDATE,否則 → INSERT

問題:在「查」與「寫」之間,別的連線可能插入同一筆 → 造成重複或唯一鍵錯誤。

UPSERT 的解法

把判斷與寫入合併成單一原子語句,交給資料庫處理衝突:

-- ✅ PostgreSQL:一句話搞定,無競態
INSERT INTO users (email, name) VALUES ('a@x.com', 'Alice')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;

前提:必須有唯一性約束

UPSERT 是針對唯一鍵 / 主鍵衝突。要觸發衝突處理,欄位上必須先有 PRIMARY KEYUNIQUE 約束,否則 insert 永遠不會「衝突」。


PostgreSQL:ON CONFLICT

PostgreSQL 從 9.5 起支援 INSERT ... ON CONFLICT

DO NOTHING:衝突就略過

INSERT INTO users (email, name) VALUES ('a@x.com', 'Alice')
ON CONFLICT (email) DO NOTHING;
-- email 已存在 → 這筆被忽略,不報錯,也不更新

DO UPDATE:衝突就更新

INSERT INTO users (email, name, login_count)
VALUES ('a@x.com', 'Alice', 1)
ON CONFLICT (email)
DO UPDATE SET
    name        = EXCLUDED.name,
    login_count = users.login_count + 1;
  • EXCLUDED:代表「原本想插入但被擋下的那筆值」(這裡 EXCLUDED.name = 'Alice'
  • 等號左邊用既有列(users.login_count),右邊可混用 EXCLUDED 與既有值

conflict target(衝突目標)

括號裡指定「以哪個唯一約束判斷衝突」:

ON CONFLICT (email) ...                 -- 指定欄位(須有對應 unique 約束)
ON CONFLICT ON CONSTRAINT uq_email ...  -- 直接指定約束名稱
ON CONFLICT DO NOTHING                  -- 不指定,任一衝突都略過

加條件與回傳

INSERT INTO users (email, name) VALUES ('a@x.com', 'Alice')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name
WHERE users.name IS DISTINCT FROM EXCLUDED.name   -- 只在真的有變化時才更新
RETURNING id, name;                                -- 回傳結果

SQLite:ON CONFLICT 與 INSERT OR

SQLite 有兩套衝突處理語法。

1. ON CONFLICT(3.24+,與 PostgreSQL 相容)

INSERT INTO users (email, name) VALUES ('a@x.com', 'Alice')
ON CONFLICT (email) DO UPDATE SET name = excluded.name;

INSERT INTO users (email, name) VALUES ('a@x.com', 'Alice')
ON CONFLICT (email) DO NOTHING;

用法幾乎與 PostgreSQL 相同(excluded 小寫亦可)。

2. INSERT OR(SQLite 傳統語法)

INSERT OR IGNORE  INTO users (email, name) VALUES ('a@x.com', 'Alice');  -- 衝突略過
INSERT OR REPLACE INTO users (email, name) VALUES ('a@x.com', 'Alice');  -- 衝突就取代

⚠️ INSERT OR REPLACE先 DELETE 再 INSERT,不是 UPDATE。這會觸發 ON DELETE CASCADE、重設自增 ID、清掉沒指定的欄位——陷阱見後面章節。


MySQL:ON DUPLICATE KEY 與 INSERT IGNORE

MySQL 不支援 ON CONFLICT,改用自己的語法。

ON DUPLICATE KEY UPDATE:衝突就更新

INSERT INTO users (email, name, login_count)
VALUES ('a@x.com', 'Alice', 1)
ON DUPLICATE KEY UPDATE
    name        = VALUES(name),          -- 舊版:VALUES() 取插入值
    login_count = login_count + 1;
  • VALUES(欄位) 取「原本要插入的值」(相當於 PostgreSQL 的 EXCLUDED
  • MySQL 8.0.19+ 改推薦用別名語法:
INSERT INTO users (email, name) VALUES ('a@x.com', 'Alice') AS new
ON DUPLICATE KEY UPDATE name = new.name;
  • 注意:它對「任何唯一鍵衝突」都觸發,無法像 PostgreSQL 指定特定 conflict target

INSERT IGNORE:衝突就略過

INSERT IGNORE INTO users (email, name) VALUES ('a@x.com', 'Alice');

⚠️ INSERT IGNORE 會把多種錯誤都降級成警告(不只唯一鍵衝突,連型別錯誤、NOT NULL 違反等也可能被吞掉),容易掩蓋問題。用途要謹慎。

REPLACE INTO:衝突就取代

REPLACE INTO users (email, name) VALUES ('a@x.com', 'Alice');

與 SQLite 的 INSERT OR REPLACE 一樣,是 DELETE + INSERT,同樣有觸發 CASCADE、重設 ID 的陷阱。


SQL 標準:MERGE

MERGE 是 SQL 標準的 UPSERT 語法,功能最完整(可同時處理 matched / not matched)。

MERGE INTO users AS t
USING (SELECT 'a@x.com' AS email, 'Alice' AS name) AS s
ON t.email = s.email
WHEN MATCHED THEN
    UPDATE SET name = s.name
WHEN NOT MATCHED THEN
    INSERT (email, name) VALUES (s.email, s.name);

支援情況:

  • SQL Server、Oracle:長期支援
  • PostgreSQL:15 版起支援 MERGE
  • MySQL、SQLite:不支援

MERGE 語法繁瑣,且早期某些資料庫實作有並行 / 競態問題。一般 UPSERT 需求用各家原生語法(ON CONFLICT / ON DUPLICATE KEY)更簡潔。


各資料庫對照

需求 PostgreSQL MySQL SQLite SQL Server / Oracle
衝突略過 ON CONFLICT ... DO NOTHING INSERT IGNORE ON CONFLICT DO NOTHING / INSERT OR IGNORE MERGE(WHEN NOT MATCHED)
衝突更新 ON CONFLICT ... DO UPDATE ON DUPLICATE KEY UPDATE ON CONFLICT ... DO UPDATE MERGE
取代整列 (無直接語法) REPLACE INTO INSERT OR REPLACE
取插入值 EXCLUDED.col VALUES(col) / 別名 excluded.col 來源別名 s.col
指定衝突鍵 可(conflict target) 不可(任一唯一鍵) ON 條件
MERGE 15+ 不支援 不支援 支援

陷阱與注意事項

1. REPLACE / INSERT OR REPLACE 是「刪除再插入」

這是最大的陷阱。REPLACE(MySQL)與 INSERT OR REPLACE(SQLite)不是更新,而是先刪掉舊列再插入新列,後果:

  • 觸發 ON DELETE CASCADE:可能連帶刪掉子表資料!
  • 重設自增主鍵:舊列的 id 沒了,新列拿到新 id
  • 未指定欄位被清空 / 設回預設值:因為是全新一列
-- 想「更新 name」卻用 REPLACE → email 以外的欄位全被重置
REPLACE INTO users (email, name) VALUES ('a@x.com', 'Bob');
-- 原本的 phone、created_at 等全沒了

需要「更新」就用 ON DUPLICATE KEY UPDATE / ON CONFLICT DO UPDATE,別用 REPLACE。

2. 必須有唯一約束

ON CONFLICT (email) 指定的欄位若沒有 UNIQUE / PRIMARY KEY 約束,PostgreSQL 會直接報錯。UPSERT 是建立在唯一約束上的。

3. INSERT IGNORE 會吞掉非預期錯誤

MySQL 的 INSERT IGNORE 不只忽略唯一鍵衝突,連資料截斷、型別不符、NOT NULL 違反等也會被降為警告而靜默通過,可能讓壞資料溜進去。

4. DO NOTHING 不回傳被略過的列

PostgreSQL ON CONFLICT DO NOTHING 搭配 RETURNING 時,被略過的列不會出現在回傳結果(因為它沒被寫入)。需要「拿到既有列」要另外查或改用 DO UPDATE。

5. 多唯一鍵時的行為差異

一張表有多個唯一鍵時:PostgreSQL 可用 conflict target 精準指定要處理哪個;MySQL 的 ON DUPLICATE KEY 則對任一唯一鍵衝突都觸發,行為較難預測。


最佳實踐

1. 「更新」用 DO UPDATE,別用 REPLACE

需要保留其他欄位與子資料時,永遠選 ON CONFLICT DO UPDATE / ON DUPLICATE KEY UPDATE,避免 REPLACE 的刪除副作用。

2. 用 UPSERT 取代「先查再寫」

消除競態、減少往返,並讓操作冪等:

-- ✅ 計數器累加,併發安全
INSERT INTO counters (key, count) VALUES ('hits', 1)
ON CONFLICT (key) DO UPDATE SET count = counters.count + 1;

3. 明確指定 conflict target

PostgreSQL 盡量寫出 ON CONFLICT (欄位),意圖清楚、避免誤觸其他唯一鍵。

4. 慎用 INSERT IGNORE

若只想忽略「重複」,PostgreSQL/SQLite 的 DO NOTHING 比 MySQL 的 INSERT IGNORE 安全得多(後者吞太多錯)。


常見問題

問題 1:ON CONFLICT 是哪些資料庫的語法?

PostgreSQL(9.5+)SQLite(3.24+)。MySQL 不支援,要用 ON DUPLICATE KEY UPDATE / INSERT IGNORE

問題 2:DO NOTHING 和 DO UPDATE 差在哪?

  • DO NOTHING:衝突就略過,既有資料原封不動
  • DO UPDATE:衝突就更新既有列(可用 EXCLUDED 取想插入的值)

問題 3:REPLACE 和 ON DUPLICATE KEY UPDATE 一樣嗎?

不一樣。ON DUPLICATE KEY UPDATE就地更新REPLACE刪除再插入,會觸發 CASCADE、重設 ID、清空未指定欄位。要更新就別用 REPLACE。

問題 4:UPSERT 一定要有唯一鍵嗎?

是。衝突處理建立在唯一鍵 / 主鍵約束上,沒有約束就不會發生「衝突」,UPSERT 無從觸發(PostgreSQL 指定不存在的 conflict target 會直接報錯)。

問題 5:EXCLUDED 是什麼?

PostgreSQL / SQLite 在 DO UPDATE 中用 EXCLUDED.欄位 代表「原本要插入、但因衝突被擋下的那筆值」。MySQL 對應的是 VALUES(欄位) 或 8.0.19+ 的別名語法。


總結

核心要點

  • UPSERT = insert-or-update:插入撞到唯一鍵時,改為略過或更新,避免「先查再寫」的競態
  • ON CONFLICT DO NOTHING / DO UPDATEPostgreSQL 與 SQLite 的語法;MySQLON DUPLICATE KEY UPDATE / INSERT IGNORE
  • 取插入值:PostgreSQL/SQLite 用 EXCLUDED,MySQL 用 VALUES() 或別名
  • REPLACE / INSERT OR REPLACE 是刪除再插入,會觸發 CASCADE、重設 ID、清空欄位——要更新請用 DO UPDATE
  • 前提是欄位上有 UNIQUE / PRIMARY KEY 約束

快速參考

需求 PostgreSQL / SQLite MySQL
衝突略過 ON CONFLICT ... DO NOTHING INSERT IGNORE
衝突更新 ON CONFLICT ... DO UPDATE SET ... ON DUPLICATE KEY UPDATE ...
取插入值 EXCLUDED.col VALUES(col) / 別名
取代整列(危險) INSERT OR REPLACE(僅 SQLite) REPLACE INTO

建立日期:2026-06-17

🔗相關文章