👨‍🔧

Spanner に UPSERT(INSERT OR UPDATE) が来ました

2024/02/09に公開

概要

RDBMS を使うアプリケーションではよく「このプライマリキーと同じレコードは存在するかもしれないけど、存在した場合は更新して、しなかった場合には追記して欲しい」という処理が発生します。
このような処理は素直に実装する場合には該当のレコードを SELECT で存在確認を行い、その後アプリケーションで SELECT を行って存在確認をしてからその結果で UPDATE か INSERT に処理を分岐させる方法があります。一方で、このような処理に対応するために SQL 文のレベルで対応している RDBMS もあります。このような処理は一般的に UPSERT という名称で呼ばれることが多いです。

具体的な実装例としては SQLite の UPSERT、PostgreSQL の INSERT の ON CONFLICT DO UPDATEや MySQL の INSERT ... ON DUPLICATE UPDATEがその例です。

Spanner は2024年1月23日の機能追加されるまでは DML での UPSERT をサポートしていませんでしたが、UPSERT に相当する INSERT OR UPDATE に対応したことで他の RDBMS からのアプリケーションの移行が容易になりました。

ミューテーションによる insertOrUpdate

Spanner は SQL の DML(INSERT/UPDATE/DELETE)による更新以外に、ミューテーション(Mutation)というより低レイヤーの API により更新などを行う方法があります。
ミューテーションによる更新では今回の変更以前から insertOrUpdate が利用可能でした。この処理は名前の通り、UPSERT 相当の機能を実現できるものです。各言語のクライアントライブラリからも実行が可能です(Goのクライアントでの例)。

このため、UPSERT が必要な場合に、ミューテーションによる更新を行う方法がありました。同一のインスタンスやデータベースに対して DML とミューテーションの併用は利用可能ですが、同一のトランザクション内では混在させることはできません。そのため、ミューテーションによる InsertOrUpdate を利用したい場合は、トランザクション全体でミューテーションを利用する必要がありました。

INSERT OR UPDATE

2024年1月23日のリリースにおいて Google 標準 SQL で INSERT OR UPDATEINSERT OR IGNORE に対応しました。PostgreSQL 互換 SQL では PostgreSQL と同様に INSERT 文に ON CONFLICT DO UPDATE SET 句をつける構文で同様の機能がサポートされます。

これにより、従来は MySQL や PostgreSQL からアプリケーションを移植する際に UPSERT 部分をアプリケーションロジックの分岐として実装するか、ミューテーションを使う必要がありました。今回のアップデートでよりストレートに実装が可能になりました。

実行例

テーブル定義と事前データは以下のような内容を前提にしています。

spanner> show create table Singers\G
*************************** 1. row ***************************
       Table: Singers
Create Table: CREATE TABLE Singers (
  SingerId INT64 NOT NULL,
  FirstName STRING(1024),
  LastName STRING(1024),
  SingerInfo BYTES(MAX),
) PRIMARY KEY(SingerId)
1 rows in set (0.49 sec)

spanner> select * from Singers;
+----------+-----------+----------+------------+
| SingerId | FirstName | LastName | SingerInfo |
+----------+-----------+----------+------------+
| 1        | Marc      | Richards | NULL       |
| 2        | Catalina  | Smith    | NULL       |
| 3        | Alice     | Trentor  | NULL       |
+----------+-----------+----------+------------+
3 rows in set (3.78 msecs)

ここに新たにプライマリキーが SingerId=4 のレコードを追記します。続けて、同じプライマリキーを指定して INSERT OR UPDATEを実行してもエラーとならず完了します。

spanner> INSERT OR UPDATE Singers (SingerId, FirstName, LastName) VALUES (4, 'Lea', 'Martin');
Query OK, 1 rows affected (0.08 sec)

spanner> select * from Singers;
+----------+-----------+----------+------------+
| SingerId | FirstName | LastName | SingerInfo |
+----------+-----------+----------+------------+
| 1        | Marc      | Richards | NULL       |
| 2        | Catalina  | Smith    | NULL       |
| 3        | Alice     | Trentor  | NULL       |
| 4        | Lea       | Martin   | NULL       |
+----------+-----------+----------+------------+
4 rows in set (8.46 msecs)

spanner> INSERT OR UPDATE Singers (SingerId, FirstName, LastName) VALUES (4, 'Lea', 'Martin');
Query OK, 1 rows affected (0.03 sec)

spanner> select * from Singers;
+----------+-----------+----------+------------+
| SingerId | FirstName | LastName | SingerInfo |
+----------+-----------+----------+------------+
| 1        | Marc      | Richards | NULL       |
| 2        | Catalina  | Smith    | NULL       |
| 3        | Alice     | Trentor  | NULL       |
| 4        | Lea       | Martin   | NULL       |
+----------+-----------+----------+------------+
4 rows in set (1.39 msecs)

INSERT OR UPDATE なので、プライマリキー以外のカラム(FirstNameとLastName)に実行前と違う内容を指定すると当然更新(UPDATE)されます。

spanner> INSERT OR UPDATE Singers (SingerId, FirstName, LastName) VALUES (4, 'Nick', 'Porter');
Query OK, 1 rows affected (0.09 sec)

spanner> select * from Singers where SingerId=4;
+----------+-----------+----------+------------+
| SingerId | FirstName | LastName | SingerInfo |
+----------+-----------+----------+------------+
| 4        | Nick      | Porter   | NULL       |
+----------+-----------+----------+------------+
1 rows in set (1.38 msecs)

INSERT OR IGNORE

INSERT OR UPDATE に比べると使い所がやや少ないですが、INSERT OR IGNORE という構文もあります。こちらはすでにレコードがある場合には追記も更新もしないという動作となります。

実行例

すでに同じプライマリキーのレコードがある状態で、INSERT OR IGNORE で異なる値を追記しようとするとエラーとはならず、更新もされないという動作となります。

spanner> select * from Singers where SingerId=4;
+----------+-----------+----------+------------+
| SingerId | FirstName | LastName | SingerInfo |
+----------+-----------+----------+------------+
| 4        | Nick      | Porter   | NULL       |
+----------+-----------+----------+------------+
1 rows in set (4.02 msecs)

spanner> INSERT OR IGNORE Singers (SingerId, FirstName, LastName) VALUES (4, 'Nick', 'Porter');
Query OK, 0 rows affected (0.05 sec)

spanner> select * from Singers where SingerId=4;
+----------+-----------+----------+------------+
| SingerId | FirstName | LastName | SingerInfo |
+----------+-----------+----------+------------+
| 4        | Nick      | Porter   | NULL       |
+----------+-----------+----------+------------+
1 rows in set (4.24 msecs)
GitHubで編集を提案
Google Cloud Japan

Discussion