|
| 1 | +export const metadata = { |
| 2 | + title: 'PostgreSQL で UPSERT を実現する方法', |
| 3 | + openGraph: { |
| 4 | + title: 'PostgreSQL で UPSERT を実現する方法', |
| 5 | + }, |
| 6 | +} |
| 7 | + |
| 8 | +PostgreSQL では、他のデータベースシステムで一般的に使用される UPSERT ステートメントは存在しません。しかし、PostgreSQL には UPSERT と同等の機能を実現するための方法が用意されています。本記事では、PostgreSQL で UPSERT を実現する主な方法について解説します。 |
| 9 | + |
| 10 | +## 1. `ON CONFLICT` 句を使用した `INSERT` 文 |
| 11 | + |
| 12 | +PostgreSQL では、`INSERT` 文に `ON CONFLICT` 句を追加することで、UPSERT 操作を実現できます。この方法は、特定の一意制約や排他制約に基づいて挿入操作を制御する際に有効です。 |
| 13 | + |
| 14 | +### 使用例 |
| 15 | + |
| 16 | +以下に、`ON CONFLICT` 句を使用した基本的な `INSERT` 文の例を示します。 |
| 17 | + |
| 18 | +```sql |
| 19 | +INSERT INTO users (id, name, email) |
| 20 | +VALUES ( 1, 'Alice', '[email protected]') |
| 21 | +ON CONFLICT (id) |
| 22 | +DO UPDATE SET |
| 23 | + name = EXCLUDED.name, |
| 24 | + email = EXCLUDED.email; |
| 25 | +``` |
| 26 | + |
| 27 | +この例では、`users` テーブルに新しいレコードを挿入しようとしていますが、`id` 列に一意制約が設定されている場合、既存の `id` が存在する場合には `DO UPDATE` によって既存のレコードが更新されます。 |
| 28 | + |
| 29 | +#### 競合時の処理 |
| 30 | + |
| 31 | +`ON CONFLICT` 句では、競合が発生した際の動作を指定できます。指定できる動作は以下 2 つです。 |
| 32 | + |
| 33 | +##### DO UPDATE |
| 34 | + |
| 35 | +競合が発生した場合に既存のレコードを更新します。更新するカラムや値は柔軟に指定できます。 |
| 36 | + |
| 37 | +**例:** |
| 38 | + |
| 39 | +```sql |
| 40 | +INSERT INTO products (id, name, quantity) |
| 41 | +VALUES (1001, 'Widget', 10) |
| 42 | +ON CONFLICT (id) |
| 43 | +DO UPDATE SET |
| 44 | + quantity = products.quantity + EXCLUDED.quantity; |
| 45 | +``` |
| 46 | + |
| 47 | +この例では、既存の `id` が存在する場合、`quantity` を既存の値に新たに挿入しようとしている値を加算します。 |
| 48 | + |
| 49 | +##### DO NOTHING |
| 50 | + |
| 51 | +競合が発生した場合に何もせず、エラーを発生させません。挿入をスキップします。 |
| 52 | + |
| 53 | +**例:** |
| 54 | + |
| 55 | +```sql |
| 56 | +INSERT INTO orders (order_id, customer_id, amount) |
| 57 | +VALUES (5001, 300, 250.00) |
| 58 | +ON CONFLICT (corder_id) DO NOTHING; |
| 59 | +``` |
| 60 | + |
| 61 | +この場合、既存の `order_id` が存在する場合、新しいレコードは挿入されず、エラーも発生しません。 |
| 62 | + |
| 63 | +<Callout emoji="💡"> |
| 64 | +**競合条件を省略できる** |
| 65 | + |
| 66 | +`ON CONFLICT` 句では、`DO UPDATE` を使用する場合には競合条件(特定の制約やインデックス)を明示的に指定する必要があります。しかし、`DO NOTHING` を使用する場合には競合条件を省略することができます。この場合、テーブルに設定されているすべての一意制約や排他制約が競合条件として適用されます。 |
| 67 | + |
| 68 | +**例:** |
| 69 | + |
| 70 | +```sql |
| 71 | +-- DO UPDATE を使用する場合は競合条件を指定する必要がある |
| 72 | +INSERT INTO employees (employee_id, name, department) |
| 73 | +VALUES (101, 'Bob', 'Sales') |
| 74 | +ON CONFLICT (employee_id) |
| 75 | +DO UPDATE SET department = EXCLUDED.department; |
| 76 | + |
| 77 | +-- DO NOTHING を使用する場合は競合条件を省略できる |
| 78 | +INSERT INTO employees (employee_id, name, department) |
| 79 | +VALUES (102, 'Charlie', 'Marketing') |
| 80 | +ON CONFLICT DO NOTHING; |
| 81 | +``` |
| 82 | +</Callout> |
| 83 | + |
| 84 | +#### 更新は行わない |
| 85 | + |
| 86 | +`DO NOTHING` オプションを使用すると、競合が発生した場合に更新を行わず、単に挿入をスキップします。これは、重複を許容しないが更新を必要としない場合に有用です。 |
| 87 | + |
| 88 | +### 複数の競合条件を指定できない |
| 89 | + |
| 90 | +`ON CONFLICT` 句では、一度に指定できる競合条件は一つだけです。複数の一意制約やインデックスに基づく競合を同時に処理することはできません。 |
| 91 | + |
| 92 | +**例:** |
| 93 | + |
| 94 | +```sql |
| 95 | +INSERT INTO table_name (col1, col2, col3) |
| 96 | +VALUES (val1, val2, val3) |
| 97 | +ON CONFLICT (col1) DO UPDATE SET ... |
| 98 | +ON CONFLICT (col2) DO UPDATE SET ...; -- これはエラーとなる |
| 99 | +``` |
| 100 | + |
| 101 | +上記のように、複数の `ON CONFLICT` 句を同時に使用しようとすると、構文エラーとなります。 |
| 102 | + |
| 103 | +<Callout emoji="💡"> |
| 104 | + **トランザクションを使ってもユニーク制約を回避できない** |
| 105 | + |
| 106 | + 複数の競合条件を処理するためにトランザクションを使用しようとしても、最初の `INSERT` 操作で一意制約に基づく競合が発生すると、トランザクション全体がエラーとなります。 |
| 107 | + このため、複数の競合条件を同時に処理する場合には、他の方法を検討する必要があります。 |
| 108 | + |
| 109 | + ```sql |
| 110 | + -- テーブル |
| 111 | + CREATE TABLE users ( |
| 112 | + id SERIAL PRIMARY KEY, |
| 113 | + username TEXT UNIQUE, |
| 114 | + email TEXT UNIQUE |
| 115 | + phone_number TEXT UNIQUE |
| 116 | + ); |
| 117 | + |
| 118 | + -- トランザクション |
| 119 | + BEGIN; |
| 120 | + |
| 121 | + INSERT INTO users (username, email, phone_number) |
| 122 | + VALUES ( 'foo', '[email protected]', '012345678'); |
| 123 | + |
| 124 | + INSERT INTO users (username, email) |
| 125 | + VALUES ( 'hogehoge', '[email protected]', '012345678') |
| 126 | + ON CONFLICT (email) |
| 127 | + DO UPDATE; -- phone_number のユニーク制約により、エラーが起きる |
| 128 | + |
| 129 | + INSERT INTO users (username, email) |
| 130 | + VALUES ( 'hogehoge', '[email protected]', '012345678') |
| 131 | + ON CONFLICT (phone_number) |
| 132 | + DO UPDATE; |
| 133 | + |
| 134 | + COMMIT; |
| 135 | + ``` |
| 136 | +</Callout> |
| 137 | + |
| 138 | +## 2. `MERGE` ステートメント |
| 139 | + |
| 140 | +PostgreSQL では、SQL 標準の `MERGE` ステートメントがバージョン 15 以降でサポートされています。`MERGE` を使用すると、条件に基づいて挿入、更新、削除を一括して行うことができます。これにより、複数の競合条件を柔軟に処理することが可能です。 |
| 141 | + |
| 142 | +### 使用例 |
| 143 | + |
| 144 | +以下に、`MERGE` ステートメントを使用した UPSERT 操作の例を示します。 |
| 145 | + |
| 146 | +```sql |
| 147 | +MERGE INTO target_table AS t |
| 148 | +USING source_table AS s |
| 149 | +ON t.id = s.id |
| 150 | +WHEN MATCHED THEN |
| 151 | + UPDATE SET |
| 152 | + t.name = s.name, |
| 153 | + t.email = s.email |
| 154 | +WHEN NOT MATCHED THEN |
| 155 | + INSERT (id, name, email) |
| 156 | + VALUES (s.id, s.name, s.email); |
| 157 | +``` |
| 158 | + |
| 159 | +この例では、`source_table` から `target_table` へのデータをマージしています。`id` が一致する場合には既存のレコードを更新し、一致しない場合には新しいレコードを挿入します。`ON` 句では、複数の条件を指定可能です。 |
| 160 | + |
| 161 | +### `MERGE` と `ON CONFLICT` の違い |
| 162 | + |
| 163 | + |
| 164 | +<Table> |
| 165 | + <Thead> |
| 166 | + <Th>特徴</Th><Th><code>ON CONFLICT</code></Th><Th><code>MERGE</code></Th> |
| 167 | + </Thead> |
| 168 | + <Tbody> |
| 169 | + <tr> |
| 170 | + <Td>対応する PostgreSQL バージョン</Td><Td>9.5 以降</Td><Td>15 以降</Td> |
| 171 | + </tr> |
| 172 | + <tr> |
| 173 | + <Td>処理可能な条件数</Td><Td>単一の競合条件のみ</Td><Td>複数の条件や複雑な条件を指定可能</Td> |
| 174 | + </tr> |
| 175 | + <tr> |
| 176 | + <Td>操作の種類</Td><Td>主に挿入と更新(<code>DO UPDATE</code>、<code>DO NOTHING</code>)</Td><Td>挿入、更新、削除など多様な操作が可能</Td> |
| 177 | + </tr> |
| 178 | + </Tbody> |
| 179 | +</Table> |
| 180 | + |
| 181 | +`ON CONFLICT` はシンプルな UPSERT 操作に最適ですが、より複雑なシナリオでは `MERGE` ステートメントが適しています。 |
| 182 | + |
| 183 | +## 3. まとめ |
| 184 | + |
| 185 | +PostgreSQL では、UPSERT 操作を実現するために `ON CONFLICT` 句を使用した `INSERT` 文と、SQL 標準の `MERGE` ステートメントの2つの主要な方法が提供されています。`ON CONFLICT` はシンプルな競合処理に適しており、`MERGE` は複数の競合条件や複雑な処理を必要とする場合に有用です。 |
| 186 | + |
| 187 | +**結論として、複数の競合条件を指定したい場合は、`MERGE` ステートメントを使用することを推奨します。** これにより、柔軟かつ効率的にデータの挿入や更新を管理することが可能となります。 |
| 188 | + |
| 189 | +# 参考資料 |
| 190 | + |
| 191 | +- [PostgreSQL Documentation - INSERT](https://www.postgresql.jp/docs/9.6/sql-insert.html) |
| 192 | +- [PostgreSQL Documentation - MERGE](https://www.postgresql.jp/docs/15/sql-merge.html) |
0 commit comments