Skip to content

Commit 37d9ef9

Browse files
authored
Merge pull request #1418 from future-architect/feature
PostgreSQL17
2 parents 69a2db5 + e126f19 commit 37d9ef9

6 files changed

+293
-3
lines changed

source/_posts/20241023a_PostgreSQL17リリース記念連載を始めます.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ Technology Innovation Group真野です。
2828
| 日付|担当者 | タイトル |
2929
| --- | -- | --- |
3030
| 10/23| 山本竜玄 | [to_regtypemod関数と型修飾子について](/articles/20241023b/) |
31-
| 10/25 | 真野隼記 | 排他制約について |
32-
| 10/28 | 岸本卓也 | 未定 |
33-
| 10/29 | 杉江伸祐 | 未定 |
31+
| 11/06 | 真野隼記 | [排他制約について](/articles/20241106a/) |
32+
| 11/18 | 岸本卓也 | 未定 |
33+
| 11/19 | 杉江伸祐 | 未定 |
3434

3535
## PostgreSQL 17 アップデート概要
3636

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
---
2+
title: "PostgreSQL17リリース: 排他制約がパーティションの親テーブルに定義できるようになった"
3+
date: 2024/11/06 00:00:00
4+
postid: a
5+
tag:
6+
- PostgreSQL
7+
- PostgreSQL17
8+
- 排他制約
9+
category:
10+
- DB
11+
thumbnail: /images/20241106a/thumbnail.png
12+
author: 真野隼記
13+
lede: "パーティションテーブルに対して宣言的に排他制約を設定できるようになったアップデートについて取り上げます。"
14+
---
15+
16+
<img src="/images/20241106a/top.png" alt="top.png" width="761" height="366" loading="lazy">
17+
18+
[PostgreSQL 17のリリース記念連載](/articles/20241023a/)の2本目です。
19+
20+
# はじめに
21+
22+
Technology Innovation Group真野です。
23+
24+
リリースノートの「E.1.3.2. Utility Commands」に記載がある、パーティションテーブルに対して宣言的に排他制約を設定できるようになったアップデートについて取り上げます。
25+
26+
>
27+
> Allow exclusion constraints on partitioned tables (Paul A. Jungwirth) §
28+
> As long as exclusion constraints compare partition key columns for equality, other columns can use exclusion constraint-specific comparisons.
29+
>
30+
> パーティショニングされたテーブルに排他制約を許可する(Paul A. Jungwirth)
31+
> 排他制約がパーティションキー列に対して等価を比較する限り、他の列は排他制約特有の比較を使用できます。
32+
> https://www.postgresql.org/docs/17/release-17.html#RELEASE-17-UTILITY
33+
34+
## 排他制約とは何か?
35+
36+
[PostgreSQL 9.0 で追加された](https://www.postgresql.jp/docs/9.6/release-9-0.html)機能で、複雑な条件が指定できる一意制約のようなものと理解すると良いかなと思います。
37+
38+
- https://www.postgresql.jp/docs/16/ddl-constraints.html#DDL-CONSTRAINTS-EXCLUSION
39+
- https://www.postgresql.jp/docs/16/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
40+
41+
典型的なユースケースは会議室など限られたリソースの予約システムでしょう。
42+
43+
以下は会議室予約の、特定の部屋が同一時間帯に貸し出されないように、排他制約を付与した例です。 `EXCLUDE USING gist (...)` の部分が対象です。
44+
45+
```sql
46+
-- btree_gistを利用するために、拡張を有効にする
47+
-- https://www.postgresql.jp/docs/16/btree-gist.html
48+
CREATE EXTENSION IF NOT EXISTS btree_gist;
49+
50+
-- 同一時間帯に、同一部屋を貸し出されないようにする例
51+
CREATE TABLE reservations (
52+
id BIGSERIAL PRIMARY KEY,
53+
room_id INT NOT NULL,
54+
start_time TIMESTAMP NOT NULL,
55+
end_time TIMESTAMP NOT NULL,
56+
EXCLUDE USING GIST (
57+
room_id WITH =,
58+
tsrange(start_time, end_time) WITH &&
59+
)
60+
);
61+
```
62+
63+
`gist` はインデックス種別のことで、地理空間データや範囲型に対して効率が良いとされています。B-treeもEXCLUDE内で指定できるそうですが、一意制約以上に高速で動かないため意味がないとドキュメントにあります。
64+
65+
tsrangeは9.2から追加された[範囲型](https://www.postgresql.jp/document/16/html/rangetypes.html#RANGETYPES)です。重なり検出する[演算子](https://www.postgresql.jp/docs/16/functions-range.html) `&&` (=重なりがあることを示す)などと一緒に使います。
66+
67+
`room_id WITH =` で同じ部屋IDが等しいという条件と合わせて、特定の部屋が同一時間帯に存在しないことを制約として示しています。
68+
69+
他にも、PostGISを用いた地理系の処理で、ジオフェンシングのように特定の領域が重複しないような制約も排他制約で実現できます。このように時間(範囲)や空間の重複を弾くために存在するのが排他制約です。
70+
71+
## 国内でも使用実績がある?
72+
73+
わたしは排他制約自体を、リリースノートを読んでいて初めて存在を知ったのですが、2017時点でそーだいさんなど、多くの方々が便利さを伝えているので、おそらく実績も多数かなと思います。
74+
75+
- [PostgreSQLで排他制約がめっちゃ便利!!](https://soudai.hatenablog.com/entry/2017/04/16/152905)
76+
- [Re: PostgreSQLで排他制約がめっちゃ便利!!](https://shogo82148.github.io/blog/2017/04/22/postgresql-exclusion-constraint/)
77+
78+
そーだいさんの記事だと、`tsrange` で直接カラム定義しており、こちらを利用するほうが一般的には良いでしょう。
79+
80+
```sql
81+
CREATE TABLE schedule
82+
(
83+
schedule_id SERIAL PRIMARY KEY NOT NULL,
84+
room_name TEXT NOT NULL,
85+
reservation_time tsrange NOT NULL,
86+
EXCLUDE USING GIST (reservation_time WITH &&)
87+
);
88+
```
89+
90+
## 16以前のバージョンでは、パーティションテーブルの親側に定義することはできなかった
91+
92+
16より前のバージョンは、以下のように `PARTITON BY``EXCLUDE USING` を同時に宣言できませんでした。
93+
94+
```sql
95+
postgres=# select version();
96+
version
97+
---------------------------------------------------------------------------------------------------------------------
98+
PostgreSQL 16.4 (Debian 16.4-1.pgdg120+2) on x86_64-pc-linux-gnu, compiled by gcc (Debian 12.2.0-14) 12.2.0, 64-bit
99+
(1 row)
100+
101+
-- パーティションテーブルで排他制約を宣言
102+
postgres=# CREATE TABLE reservations (
103+
id BIGSERIAL NOT NULL,
104+
room_id INT NOT NULL,
105+
reservation_date date,
106+
start_time TIMESTAMP NOT NULL,
107+
end_time TIMESTAMP NOT NULL,
108+
CONSTRAINT reservations_pkey PRIMARY KEY (reservation_date, id),
109+
EXCLUDE USING GIST (
110+
reservation_date WITH =,
111+
room_id WITH =,
112+
tsrange(start_time, end_time) WITH &&
113+
)
114+
) PARTITION BY RANGE (reservation_date);
115+
ERROR: exclusion constraints are not supported on partitioned tables
116+
LINE 8: EXCLUDE USING GIST (
117+
```
118+
119+
`exclusion constraints are not supported on partitioned tables` とあるのがエラー部分です。
120+
121+
## 16以前の回避方法
122+
123+
16以前のバージョンでは、回避策として子テーブルそれぞれに排他制約を追加していく必要がありました。
124+
125+
以下が `reservations_20241101` などのパーティションを作成して、それに対して排他制約を個別に定義する例です。
126+
127+
```sql
128+
CREATE TABLE reservations (
129+
id BIGSERIAL NOT NULL,
130+
room_id INT NOT NULL,
131+
reservation_date date,
132+
start_time TIMESTAMP NOT NULL,
133+
end_time TIMESTAMP NOT NULL,
134+
CONSTRAINT reservations_pkey PRIMARY KEY (reservation_date, id)
135+
) PARTITION BY RANGE (reservation_date);
136+
137+
-- パーティションを作成
138+
CREATE TABLE reservations_20241101 PARTITION OF reservations
139+
FOR VALUES FROM ('2024-11-01') TO ('2024-11-02');
140+
141+
CREATE TABLE reservations_20241102 PARTITION OF reservations
142+
FOR VALUES FROM ('2024-11-02') TO ('2024-11-03');
143+
144+
-- 排他制約をそれぞれの子パーティションテーブルに設定
145+
ALTER TABLE reservations_20241101
146+
ADD CONSTRAINT reservations_20241101_exclude EXCLUDE USING GIST (
147+
room_id WITH =,
148+
tsrange(start_time, end_time) WITH &&
149+
);
150+
151+
ALTER TABLE reservations_20241102
152+
ADD CONSTRAINT reservations_20241102_exclude EXCLUDE USING GIST (
153+
room_id WITH =,
154+
tsrange(start_time, end_time) WITH &&
155+
);
156+
```
157+
158+
テーブルの状態は以下です
159+
160+
```sh \d+結果
161+
postgres=# \d+ reservations
162+
Partitioned table "public.reservations"
163+
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
164+
------------------+-----------------------------+-----------+----------+------------------------------------------+---------+-------------+--------------+-------------
165+
id | bigint | | not null | nextval('reservations_id_seq'::regclass) | plain | | |
166+
room_id | integer | | not null | | plain | | |
167+
reservation_date | date | | not null | | plain | | |
168+
start_time | timestamp without time zone | | not null | | plain | | |
169+
end_time | timestamp without time zone | | not null | | plain | | |
170+
Partition key: RANGE (reservation_date)
171+
Indexes:
172+
"reservations_pkey" PRIMARY KEY, btree (reservation_date, id)
173+
Partitions: reservations_20241101 FOR VALUES FROM ('2024-11-01') TO ('2024-11-02'),
174+
reservations_20241102 FOR VALUES FROM ('2024-11-02') TO ('2024-11-03')
175+
````
176+
177+
178+
実際にデータを登録してみます。
179+
180+
```sql psqlでの実行例
181+
-- 正常に挿入されるデータ
182+
postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)
183+
VALUES (1, '2024-11-01', '2024-11-01 10:00:00', '2024-11-01 11:00:00');
184+
INSERT 0 1
185+
postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)
186+
VALUES (1, '2024-11-02', '2024-11-02 11:00:00', '2024-11-02 12:00:00');
187+
INSERT 0 1
188+
189+
-- 時間帯が重複しているため、エラーが発生するデータ
190+
postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)
191+
VALUES (1, '2024-11-01', '2024-11-01 10:30:00', '2024-11-01 11:30:00');
192+
ERROR: conflicting key value violates exclusion constraint "reservations_20241101_exclude"
193+
DETAIL: Key (room_id, tsrange(start_time, end_time))=(1, ["2024-11-01 10:30:00","2024-11-01 11:30:00")) conflicts with existing key (room_id, tsrange(start_time, end_time))=(1, ["2024-11-01 10:00:00","2024-11-01 11:00:00"))
194+
195+
-- テーブル状態を確認
196+
postgres=# select * from reservations;
197+
id | room_id | reservation_date | start_time | end_time
198+
----+---------+------------------+---------------------+---------------------
199+
1 | 1 | 2024-11-01 | 2024-11-01 10:00:00 | 2024-11-01 11:00:00
200+
2 | 1 | 2024-11-02 | 2024-11-02 11:00:00 | 2024-11-02 12:00:00
201+
(2 rows)
202+
```
203+
204+
最後のINSERTだけが失敗して、整合性が保たれていることがわかります。
205+
206+
## PostgreSQL17からは、親テーブル側に宣言できるようになった
207+
208+
次のように、`CREATE TABLE``EXCLUDE USING``PARTITON BY` のどちらも指定できるようになりました。パーティションテーブル側それぞれに排他制約を指定しなくて済むので、より直感的になりました。
209+
210+
```sql
211+
CREATE TABLE reservations (
212+
id BIGSERIAL NOT NULL,
213+
room_id INT NOT NULL,
214+
reservation_date date,
215+
start_time TIMESTAMP NOT NULL,
216+
end_time TIMESTAMP NOT NULL,
217+
CONSTRAINT reservations_pkey PRIMARY KEY (reservation_date, id),
218+
EXCLUDE USING GIST (
219+
reservation_date WITH =,
220+
room_id WITH =,
221+
tsrange(start_time, end_time) WITH &&
222+
)
223+
) PARTITION BY RANGE (reservation_date);
224+
225+
-- パーティション作成
226+
CREATE TABLE reservations_20241101 PARTITION OF reservations
227+
FOR VALUES FROM ('2024-11-01') TO ('2024-11-02');
228+
229+
CREATE TABLE reservations_20241102 PARTITION OF reservations
230+
FOR VALUES FROM ('2024-11-02') TO ('2024-11-03');
231+
```
232+
233+
制約としては、必ずパーティションキー(今回だと `reservation_date`) をイコール条件で履いた制約の追加する必要があります。
234+
235+
テーブルの状態は以下です。親テーブル側にも排他制約の情報が追加されていますね。
236+
237+
```sh \d+結果
238+
postgres=# \d+ reservations
239+
Partitioned table "public.reservations"
240+
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
241+
------------------+-----------------------------+-----------+----------+------------------------------------------+---------+-------------+--------------+-------------
242+
id | bigint | | not null | nextval('reservations_id_seq'::regclass) | plain | | |
243+
room_id | integer | | not null | | plain | | |
244+
reservation_date | date | | not null | | plain | | |
245+
start_time | timestamp without time zone | | not null | | plain | | |
246+
end_time | timestamp without time zone | | not null | | plain | | |
247+
Partition key: RANGE (reservation_date)
248+
Indexes:
249+
"reservations_pkey" PRIMARY KEY, btree (reservation_date, id)
250+
"reservations_reservation_date_room_id_tsrange_excl" EXCLUDE USING gist (reservation_date WITH =, room_id WITH =, tsrange(start_time, end_time) WITH &&)
251+
Partitions: reservations_20241101 FOR VALUES FROM ('2024-11-01') TO ('2024-11-02'),
252+
reservations_20241102 FOR VALUES FROM ('2024-11-02') TO ('2024-11-03')
253+
```
254+
255+
さきほどと同様に、実際にデータを登録してみます。
256+
257+
```sql
258+
-- 正常に挿入されるデータ
259+
postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)
260+
VALUES (1, '2024-11-01', '2024-11-01 10:00:00', '2024-11-01 11:00:00');
261+
INSERT 0 1
262+
postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)
263+
VALUES (1, '2024-11-02', '2024-11-02 11:00:00', '2024-11-02 12:00:00');
264+
INSERT 0 1
265+
266+
-- 時間帯が重複しているため、エラーが発生するデータ
267+
postgres=# INSERT INTO reservations (room_id, reservation_date, start_time, end_time)
268+
VALUES (1, '2024-11-01', '2024-11-01 10:30:00', '2024-11-01 11:30:00');
269+
ERROR: conflicting key value violates exclusion constraint "reservations_20241101_reservation_date_room_id_tsrange_excl"
270+
DETAIL: Key (reservation_date, room_id, tsrange(start_time, end_time))=(2024-11-01, 1, ["2024-11-01 10:30:00","2024-11-01 11:30:00")) conflicts with existing key (reservation_date, room_id, tsrange(start_time, end_time))=(2024-11-01, 1, ["2024-11-01 10:00:00","2024-11-01 11:00:00")).
271+
272+
-- テーブル状態を確認
273+
postgres=# select * from reservations;
274+
id | room_id | reservation_date | start_time | end_time
275+
----+---------+------------------+---------------------+---------------------
276+
1 | 1 | 2024-11-01 | 2024-11-01 10:00:00 | 2024-11-01 11:00:00
277+
2 | 1 | 2024-11-02 | 2024-11-02 11:00:00 | 2024-11-02 12:00:00
278+
(2 rows)
279+
```
280+
281+
動作も16時点と同様、最後のINSERTだけが失敗して、整合性が保たれていることがわかります。
282+
283+
めちゃくちゃ便利!
284+
285+
## まとめ
286+
287+
PostgreSQLの排他制約を試しました。17のアップデートで、パーティションテーブルでより排他制約を利用しやすくなりました。
288+
289+
15.4 KB
Loading

source/images/20241106a/thumbnail.png:Zone.Identifier

Whitespace-only changes.

source/images/20241106a/top.png

31.7 KB
Loading

themes/future/layout/_widget/advent-calendar.ejs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<div class="widget">
22
<ul class="nav-flex">
3+
<li><a href="http://qiita.com/advent-calendar/2024/future" title="フューチャー Advent Calendar 2024 #Qiita" target="_blank" rel="noopener">2024年</a></li>
34
<li><a href="http://qiita.com/advent-calendar/2023/future" title="フューチャー Advent Calendar 2023 #Qiita" target="_blank" rel="noopener">2023年</a></li>
45
<li><a href="http://qiita.com/advent-calendar/2022/future" title="フューチャー Advent Calendar 2022 #Qiita" target="_blank" rel="noopener">2022年</a></li>
56
<li><a href="http://qiita.com/advent-calendar/2021/future" title="フューチャー Advent Calendar 2021 #Qiita" target="_blank" rel="noopener">2021年</a></li>

0 commit comments

Comments
 (0)