Postgres: 「なければ作る」は INSERT ... ON CONFLICT ... RETURNING の 1 文で
SELECT で存在を確かめてから INSERT する書き方は、並行実行で重複エラーや競合を起こす。INSERT ... ON CONFLICT (キー) DO UPDATE ... RETURNING * なら 1 往復で、競合なく「作るか更新して、その行を返す」ができる。
使う場面
- メールアドレスでユーザーを upsert して、その ID をすぐ使いたいとき
- 外部 ID をキーに同期処理でレコードを作成・更新するとき
- 「チェックしてから挿入」の間に別リクエストが割り込む競合(check-then-insert race)をなくしたいとき
やり方
sql
-- email に UNIQUE 制約(または一意インデックス)が必要
INSERT INTO users (email, name)
VALUES ($1, $2)
ON CONFLICT (email) DO UPDATE
SET name = EXCLUDED.name,
updated_at = now()
RETURNING id, email, name, (xmax = 0) AS inserted;EXCLUDEDは「挿入しようとした行」を指す。(xmax = 0) AS insertedは新規挿入なら true、更新なら false になる小技(内部列に依存するので、厳密さが必要なら別の方法で判定する)。- 変えたくない列は SET に書かない。条件付きで更新するなら
DO UPDATE SET ... WHERE users.updated_at < EXCLUDED.updated_at。
注意点
ON CONFLICT DO NOTHINGにRETURNINGを付けても、衝突した行は返らない(0 行になる)。既存行も必要なら、DO UPDATE SET email = EXCLUDED.emailのような何も変えない更新にするか、続けて SELECT する。- 衝突対象
(email)には UNIQUE 制約か一意インデックスが要る。無いとエラーになる。 DO UPDATEは衝突のたびに行を書き換えるので、更新が多いとテーブルが膨らみやすい(VACUUM 対象が増える)。- 1 文の中で同じキーを 2 回 upsert すると「ON CONFLICT DO UPDATE command cannot affect row a second time」になる。まとめて入れる前に重複を除く。
- 関連: 冪等キーは「処理結果」と一緒に、同じトランザクションで保存する でも
ON CONFLICTを使っている。
出典
- @amitsinha__07 · 2026-11-08
- @amitsinha__07 2026-11-07(UNIQUE と衝突時の扱い): @amitsinha__07 · 2026-11-07