テック技報最新号へ →
コード

Postgres: 「なければ作る」は INSERT ... ON CONFLICT ... RETURNING の 1 文で

SELECT で存在を確かめてから INSERT する書き方は、並行実行で重複エラーや競合を起こす。INSERT ... ON CONFLICT (キー) DO UPDATE ... RETURNING * なら 1 往復で、競合なく「作るか更新して、その行を返す」ができる。

@amitsinha__07 · 投稿 2026-11-07
#postgres#sql#concurrency#infra

使う場面

  • メールアドレスでユーザーを 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 を使っている。

出典

X の元の投稿を開く