🆙

【PostgreSQL】MERGE文を使ってUpsertする方法

に公開

はじめに

前回の記事で、PostgreSQLでUpsertする方法のうち、ON CONFLICT句を使った方法を見ていきました。

今回はそこでは触れられなかったMERGE文によるUpsertを見ていきます!

この記事でわかること

  • MERGE文を使ってUpsertする方法
  • RETURNING句を使ったDMLの確認方法

MERGE文を使ってUpsertする方法

PostgreSQLにおける MERGE文 は、2022年10月13日にリリースされたバージョン15.0で追加されました。ON CONFLICT句 同様、Upsertをするためのものとなります。

MERGE文の構文は以下の通りです。

構文
MERGE INTO <対象テーブル> AS <エイリアス>
USING <データソース> AS エイリアス
ON <結合条件>
WHEN MATCHED THEN
    <更新または削除処理>
WHEN NOT MATCHED THEN
    <登録処理>;

各要素には、以下のような意味や役割があります。

要素 意味
MERGE INTO 対象となるテーブルを指定
USING 比較対象となるデータソース(テーブル・副問い合わせ・VALUES句)を指定
ON 対象テーブルとデータソースを照合するための結合条件を指定
WHEN MATCHED THEN 既存データがある場合の処理を指定(通常はUPDATEまたはDELETE)
WHEN NOT MATCHED THEN 新規データの場合の処理を指定(通常はINSERT)

それでは、実際にMERGE文を使ってUpsertをしてみましょう!

前提

ここでは、ユーザーを管理するusersテーブルと、一時的にユーザー情報を保持するためのusers_sourceテーブルが存在するものとします。
これらのテーブルの関係性イメージは、以下のようなものとします。

  • 外部システムや別のDBから、夜間バッチでユーザーデータをusers_sourceテーブルに取り込む
  • ユーザー情報の更新があったらusersテーブルに反映
  • 新規ユーザーの場合はusersテーブルにもデータを反映

それぞれ、既に以下のようなデータが存在するものとします。

データ
-- SELECT * FROM users;
 id |  name  |  birthday  
----+--------+------------
  1 | tomoya | 2025-10-15
(1 row)

-- SELECT * FROM users_source;
 id |  name  |  birthday  
----+--------+------------
  1 | tomoya | 2025-10-15
(1 row)

既にデータが存在する場合

users_sourceテーブルに存在するデータのうち、nameカラムに変更があった場合を想定します。

データ
-- SELECT * FROM users_source;
id |      name      |  birthday  
----+----------------+------------
  1 | tomoya updated | 2025-10-15

このデータを元に、MERGE文を用いてUpsertを行います。

MERGE
MERGE INTO users AS targe
USING users_source AS source
ON targe.id = source.id
WHEN MATCHED THEN
    UPDATE 
    SET name = source.name
WHEN NOT MATCHED THEN
   INSERT (id, name, birthday)
   VALUES (source.id, source.name, source.birthday);

実行すると、以下のような結果となります。

実行結果
-- MERGE 1

-- SELECT * FROM users;
-- nameが更新されていることがわかる
 id |      name      |  birthday  
----+----------------+------------
  1 | tomoya updated | 2025-10-15
(1 row)

新規データの場合

users_sourceテーブルに、新たなデータが登録されたものとします。

データ
-- SELECT * FROM users_source;
 id |      name      |  birthday  
----+----------------+------------
  1 | tomoya updated | 2025-10-15
  2 | Alice          | 2025-10-18
(2 rows)

このデータを元に、MERGE文を用いてUpsertを行います。

MERGE
MERGE INTO users AS targe
USING users_source AS source
ON targe.id = source.id
WHEN MATCHED THEN
    UPDATE 
    SET name = source.name
WHEN NOT MATCHED THEN
   INSERT (id, name, birthday)
   VALUES (source.id, source.name, source.birthday);

実行すると、以下のような結果となります。

実行結果
-- MERGE 2

-- SELECT * FROM users;
 id |      name      |  birthday  
----+----------------+------------
  1 | tomoya updated | 2025-10-15
  2 | Alice          | 2025-10-18
(2 rows)

RETURNING句を使ってみる

2024年9月26日にリリースされたPostgreSQL 17.0では、MERGE文に RETURNING句 を使うことができるようになりました。merge_action() という関数を使うことで、実行されたDML(INSERT/UPDATE/DELETE)を確認することができます。

今回は、users_sourceテーブルに、さらに新たなデータが登録されたものとします。

データ
 id |      name      |  birthday  
----+----------------+------------
  1 | tomoya updated | 2025-10-15
  2 | Alice          | 2025-10-18
  3 | Bob            | 2025-10-19
(3 rows)

このデータを元に、MERGE文を用いてUpsertを行います。

MERGE文
MERGE INTO users AS target
USING users_source AS source
ON target.id = source.id
WHEN MATCHED THEN
    UPDATE 
    SET name = source.name
WHEN NOT MATCHED THEN
   INSERT (id, name, birthday)
   VALUES (source.id, source.name, source.birthday)
RETURNING 
    merge_action() AS action,
    target.*;

実行すると、以下のような結果となります。

実行結果
 action | id |      name      |  birthday  
--------+----+----------------+------------
 UPDATE |  1 | tomoya updated | 2025-10-15  -- 既存データの更新
 UPDATE |  2 | Alice          | 2025-10-18  -- 既存データの更新
 INSERT |  3 | Bob            | 2025-10-19  -- 新規データの登録
(3 rows)

MERGE 3

まとめ

今回は、PostgreSQL 15.0で追加されたMERGE文を用いたUpsertの方法を見ていきました。

登録・更新するテーブルとソースとなるテーブルを結合し、データが存在する場合はUpdate、データが存在しない場合はInsertをするような仕組みとなっております。
また、PostgreSQL 17.0で追加されたRETURNING句を用いることで、どのようなDMLを実行したのかを明示的に確認することができます。

参考

Discussion