🐘

[PostgreSQL] EXCLUDED を使って UPSERT 文をちょっとだけ楽に書く

2022/09/11に公開

はじめに

PostgreSQL で UPSERT 文 が必要になり書き方を調べていたのですが、下記のような書き方が多くヒットしました。

-- ユニーク制約に違反したら何もしない
INSERT INTO テーブル名 (カラム1, カラム2,...)
  VALUES (1,2,...)
  ON CONFLICT (ユニーク制約名)
  DO NOTHING;

-- ユニーク制約に違反したら指定されたカラムを更新する
INSERT INTO テーブル名 (カラム1, カラム2,...)
  VALUES (1,2,...)
  ON CONFLICT (ユニーク制約名)
  DO UPDATE SET カラム1 =1, カラム2 =2, ...;

-- 例
INSERT INTO users (id, family_name, first_name, email, is_activated, is_admin)
  VALUES ('user-id1', '山田', '太郎', 'example@example.com', TRUE, FALSE)
  ON CONFLICT(id) DO UPDATE SET
    family_name = '山田',
    first_name = '太郎',
    email = 'example@example.com',
    is_activated = TRUE,
    is_admin = FALSE;

DO UPDATE SET 以降で VALUES に指定した値と同じ値をわざわざ書き連ねて書くのが面倒くさく DRY でないのでもう少し良い書き方がないか調べました。

EXCLUDED を使う

DO UPDATE SET 以降で EXCLUDED という特別なテーブルが参照できるらしいです。このテーブルにはもともと INSERT されようとしていた値が入っているので、hoge='value' みたいにいちいち値を指定し直さなくて良くなります。

確認したドキュメントはこちら。

https://www.postgresql.jp/docs/9.5/sql-insert.html

すべての行レベルの BEFORE INSERT トリガーの結果が excluded の値に反映されることに注意して下さい。 これらの結果として、行が挿入から除外されることになったかもしれないからです。

BEFORE INSERT トリガーが設定されている場合は、その処理を行った結果が EXCLUDED に入るようです。
先程例に出した INSERT 文を EXCLUDED を使うと以下のように書き換えられます。

INSERT INTO users (id, family_name, first_name, email, is_activated, is_admin)
  VALUES ('user-id1', '山田', '太郎', 'example@example.com', TRUE, FALSE)
  ON CONFLICT(id) DO UPDATE SET
    family_name = EXCLUDED.family_name,
    first_name = EXCLUDED.first_name,
    email = EXCLUDED.email,
    is_activated = EXCLUDED.is_activated,
    is_admin = EXCLUDED.is_admin;

よく考えたら複数件 Upsert したいときは EXCLUDED を使わないと INSERT しようとした値で UPDATE が行えませんね。

INSERT INTO users (id, family_name, first_name, email, is_activated, is_admin)
  VALUES
    ('user-id1', '山田', '太郎', 'example@example.com', TRUE, FALSE)
    ('user-id2', '田中', '太郎', 'example+tanaka@example.com', TRUE, FALSE),
    ('user-id3', '鈴木', '太郎', 'example+suzuki@example.com', TRUE, FALSE),
  ON CONFLICT(id) DO UPDATE SET
    family_name = EXCLUDED.family_name,
    first_name = EXCLUDED.first_name,
    email = EXCLUDED.email,
    is_activated = EXCLUDED.is_activated,
    is_admin = EXCLUDED.is_admin;

UPSERT (ON CONFLICT 句)、EXCLUDED はともに PostgreSQL 9.5 以降で利用可能なようです。

参考

https://thinkit.co.jp/article/9586

https://www.postgresql.jp/docs/9.5/sql-insert.html

GitHubで編集を提案

Discussion