Skip to content

Commit df579ff

Browse files
committed
postgres upsert article
1 parent e9bbe4c commit df579ff

File tree

4 files changed

+242
-0
lines changed

4 files changed

+242
-0
lines changed
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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)

app/(post)/components/table.tsx

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { ReactNode } from "react";
2+
3+
export function Table({ children }: { children: ReactNode }) {
4+
return (
5+
<div className="my-6 overflow-x-auto">
6+
<table className="min-w-full border-collapse border border-gray-300 dark:border-gray-600">
7+
<>{children}</>
8+
</table>
9+
</div>
10+
);
11+
}
12+
13+
export function Thead({ children }: { children: ReactNode }) {
14+
return (
15+
<thead className="bg-gray-100 dark:bg-gray-800">
16+
<tr>{children}</tr>
17+
</thead>
18+
);
19+
}
20+
21+
export function Tbody({ children }: { children: ReactNode }) {
22+
return <tbody>{children}</tbody>;
23+
}
24+
25+
export function Th({ children }: { children: ReactNode }) {
26+
return (
27+
<th className="px-4 py-2 text-left text-gray-700 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
28+
<>{children}</>
29+
</th>
30+
);
31+
}
32+
33+
export function Td({ children }: { children: ReactNode }) {
34+
return (
35+
<td className="px-4 py-2 text-gray-600 dark:text-gray-400 border-b border-gray-300 dark:border-gray-600">
36+
<>{children}</>
37+
</td>
38+
);
39+
}

app/posts.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
"id": "engineering",
55
"date": "Novembar 15, 2024",
66
"title": "1 人で開発すること"
7+
},
8+
{
9+
"id": "postgres-upsert",
10+
"date": "Novembar 15, 2024",
11+
"title": "PostgresのUPSERT"
712
}
813
]
914
}

mdx-components.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { Callout } from "app/(post)/components/callout";
1717
import { YouTube } from "app/(post)/components/youtube";
1818
import { Ref, FootNotes, FootNote } from "app/(post)/components/footnotes";
1919
import { Blockquote as blockquote } from "app/(post)/components/blockquote";
20+
import { Table, Thead, Tbody, Th, Td } from "app/(post)/components/table";
2021

2122
export function useMDXComponents(components: {
2223
[component: string]: React.ComponentType;
@@ -46,5 +47,10 @@ export function useMDXComponents(components: {
4647
Ref,
4748
FootNotes,
4849
FootNote,
50+
Table,
51+
Thead,
52+
Tbody,
53+
Th,
54+
Td,
4955
};
5056
}

0 commit comments

Comments
 (0)