こんにちは、かつコーチです。
前回まではPostgreSQLの環境構築を扱ってきました。
ここからは「入門・環境構築」編を離れ、PostgreSQLならではのデータ型を扱う新しいカテゴリに入ります。
MySQLしか触ったことがない人にとって、PostgreSQLで一番驚くポイントは、標準SQLにはない独自のデータ型が数多く用意されていることです。
今回はその中でも代表的な配列型と範囲型を中心に解説します。
配列型:1つのカラムに複数の値を持たせる
配列型とは
配列型とは、1つのカラムに複数の値をまとめて格納できるデータ型です。
MySQLでは「タグ」のように1つの項目が複数の値を持ちうるデータは、中間テーブルを作ってJOINするのが定石でした。
PostgreSQLでは、値の性質によっては配列型を使うことで、テーブル設計をシンプルにできる場合があります。
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
tags TEXT[] -- TEXT型の配列
);
INSERT INTO articles (title, tags)
VALUES ('PostgreSQL入門', ARRAY['PostgreSQL', '初心者向け', 'DB']);
TEXT[]のように、既存の型名の後ろに[]を付けるだけで配列型のカラムを定義できます。
配列を検索する
配列型のカラムには専用の演算子が用意されています。
-- 配列の中に'PostgreSQL'というタグが含まれる記事を検索
SELECT title FROM articles WHERE tags @> ARRAY['PostgreSQL'];
-- 配列の要素数を取得
SELECT title, array_length(tags, 1) AS tag_count FROM articles;
-- 配列を展開して1行1要素にする
SELECT title, unnest(tags) AS tag FROM articles;
@>演算子は「左側の配列が右側の配列を含んでいるか」を判定します。
unnest関数は配列を展開して複数行に変換する関数で、集計時によく使います。
配列型を使うべきか、中間テーブルにすべきか:判断軸
配列型は便利ですが、何でも配列型にすればいいわけではありません。
- タグの値自体を検索・集計・他テーブルと関連付けたい → 中間テーブル(正規化)
- 値の順序に意味があり、他のテーブルと関連させる必要がない簡易的な情報 → 配列型
- タグごとにマスタ管理(色分けなど)をしたい → 中間テーブル
私が実務で配列型を使うのは、「複数の選択肢から選んだチェックボックスの値をそのまま保存したい」といった、他のテーブルと結合する必要がない軽量なデータに限定しています。
正規化とのトレードオフを理解した上で使い分けることが大切です。
範囲型:期間や数値の範囲を1つの値として扱う
範囲型とは
範囲型とは、「開始〜終了」のような範囲を1つの値として表現できるデータ型です。
代表的なものに、日付の範囲を表すdaterange、数値の範囲を表すint4range、タイムスタンプの範囲を表すtsrangeなどがあります。
CREATE TABLE reservations (
id SERIAL PRIMARY KEY,
room_name TEXT NOT NULL,
reserved_period DATERANGE NOT NULL
);
INSERT INTO reservations (room_name, reserved_period)
VALUES ('会議室A', '[2026-09-10, 2026-09-12)');
[2026-09-10, 2026-09-12)という表記は、「9/10を含み、9/12を含まない」範囲を意味します。
[は以上(閉区間)、)は未満(開区間)を表す数学的な区間表記です。
範囲の重なりを1発で判定する
範囲型の最大のメリットは、「期間が重複していないか」という判定が、SQLの条件式1つで完結する点です。
❌ Before:範囲型を使わず、開始日・終了日を別カラムで持ち、複雑なWHERE句で重複判定する
CREATE TABLE reservations_old (
id SERIAL PRIMARY KEY,
room_name TEXT NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL
);
-- 重複判定のWHERE句が複雑になりがちで、条件の組み合わせを間違えやすい
SELECT * FROM reservations_old
WHERE room_name = '会議室A'
AND start_date < '2026-09-12'
AND end_date > '2026-09-10';
✅ After:範囲型と&&演算子でシンプルに重複判定する
-- &&演算子は「範囲が重なっているか」を判定する
SELECT * FROM reservations
WHERE room_name = '会議室A'
AND reserved_period && '[2026-09-10, 2026-09-12)'::DATERANGE;
開始日・終了日を別々のカラムで持つ設計は、重複判定の条件式を毎回自分で組み立てる必要があり、境界値(等号を含むか含まないか)を間違えるバグの温床になりがちです。
範囲型を使えば、&&演算子1つで「重なっているかどうか」を正確に判定できます。
私が会議室予約システムを設計した際も、この範囲型と&&演算子の組み合わせに切り替えたことで、重複チェックのバグをほぼゼロにできた経験があります。
応用:範囲型に排他制約を付けて二重予約を防ぐ
GiSTインデックスと組み合わせた排他制約
範囲型は、EXCLUDE制約と組み合わせることで、アプリケーション側のロジックに頼らず、データベースレベルで二重予約を防ぐことができます。
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE reservations
ADD CONSTRAINT no_overlapping_reservations
EXCLUDE USING GIST (
room_name WITH =,
reserved_period WITH &&
);
この制約を設定しておくと、同じroom_nameで期間が重複するINSERTを実行した瞬間にデータベースがエラーを返してくれます。
-- 既存の予約と期間が重なる場合、このINSERTはエラーになる
INSERT INTO reservations (room_name, reserved_period)
VALUES ('会議室A', '[2026-09-11, 2026-09-13)');
-- ERROR: conflicting key value violates exclusion constraint "no_overlapping_reservations"
アプリケーション側の重複チェック漏れを、データベース側の制約でカバーできる点は、範囲型ならではの強力な使い方です。
インデックスの種類(GiSTなど)については、別記事で詳しく扱う予定です。
まとめ
この記事のポイント
- 配列型はカラムに複数の値をまとめて格納でき、
@>やunnestで検索・展開できる - 配列型と中間テーブルは、値を他テーブルと関連付ける必要があるかどうかで使い分ける
- 範囲型は「開始〜終了」を1つの値として扱え、
&&演算子で重複判定がシンプルになる EXCLUDE制約と組み合わせると、データベースレベルで二重予約を防げる
次に読むべき記事
配列型・範囲型に続いて、次はPostgreSQLの半構造化データ活用の核となるJSONB型を解説します。
次回はJSONB型とGINインデックスの使い方を扱います。
→ 次の記事:JSONB型とGINインデックスで半構造化データを扱う
タグ: PostgreSQL, 中級者向け, データ型