はじめに#
データベース設計において、適切なデータ型の選択はパフォーマンス、ストレージ効率、データ整合性に直結する重要な決定です。PostgreSQLは豊富なデータ型を提供しており、要件に応じて最適な型を選択することで、アプリケーションの品質を大きく向上させることができます。
本記事では、PostgreSQLで頻繁に使用されるデータ型について、その特徴と適切な使い分けの基準を解説します。数値型、文字列型、日付時刻型といった基本型から、UUID、配列型、JSONB型といった高度な型まで、実践的なサンプルコードとともに学んでいきます。
この記事を読むことで、以下のことができるようになります。
- 数値型(INTEGER/BIGINT/NUMERIC/SERIAL)の特徴を理解し、用途に応じて選択できる
- 文字列型(VARCHAR/TEXT/CHAR)の違いを把握し、適切に使い分けられる
- 日付時刻型(TIMESTAMP/DATE/TIME/INTERVAL)を正しく扱える
- UUID、配列型、JSONB型を活用した柔軟なデータ設計ができる
前提条件#
- PostgreSQL 14以降がインストールされていること
- psqlまたはGUIツール(pgAdmin、DBeaver等)でデータベースに接続できること
- 基本的なSQL構文(CREATE TABLE、INSERT等)を理解していること
数値型の選択#
PostgreSQLの数値型は、整数型、任意精度型、浮動小数点型、シリアル型に分類されます。それぞれの特徴を理解し、要件に応じて適切な型を選択しましょう。
整数型(INTEGER / BIGINT / SMALLINT)#
整数型は最も基本的な数値型で、小数点を含まない値を格納します。
| 型名 |
サイズ |
範囲 |
| SMALLINT |
2バイト |
-32,768 〜 32,767 |
| INTEGER |
4バイト |
-2,147,483,648 〜 2,147,483,647 |
| BIGINT |
8バイト |
-9,223,372,036,854,775,808 〜 9,223,372,036,854,775,807 |
1
2
3
4
5
6
7
|
-- 整数型の使用例
CREATE TABLE users (
user_id INTEGER PRIMARY KEY,
age SMALLINT, -- 年齢は16ビットで十分
login_count INTEGER, -- 一般的なカウンター
total_points BIGINT -- 大きな値になる可能性がある累計
);
|
選択の基準
- SMALLINT: 年齢、評価スコア(1〜5)など、値の範囲が明確に小さい場合
- INTEGER: 最も一般的な選択。主キー、外部キー、カウンターなど
- BIGINT: 10億を超える可能性がある値、Unixタイムスタンプ(ミリ秒)、大規模システムのID
1
2
3
|
-- 型の範囲を超えるとエラーになる
INSERT INTO users (user_id, age) VALUES (1, 40000);
-- ERROR: smallint out of range
|
任意精度型(NUMERIC / DECIMAL)#
NUMERIC(DECIMALと同義)は、正確な精度が必要な数値に使用します。金額計算など、丸め誤差が許容されない場面で必須の型です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-- NUMERIC(精度, スケール) の形式で指定
-- 精度: 全体の桁数、スケール: 小数点以下の桁数
CREATE TABLE products (
product_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
price NUMERIC(10, 2) NOT NULL, -- 最大99,999,999.99
tax_rate NUMERIC(5, 4), -- 0.0000 〜 9.9999
weight_kg NUMERIC(8, 3) -- 99,999.999 まで
);
INSERT INTO products (name, price, tax_rate, weight_kg)
VALUES ('ノートPC', 129800.00, 0.1000, 1.850);
-- 金額計算は正確に行われる
SELECT name, price, price * tax_rate AS tax, price * (1 + tax_rate) AS total
FROM products;
|
NUMERICの特徴
- 計算結果が正確(丸め誤差なし)
- INTEGER/BIGINTより計算速度は遅い
- ストレージ効率は値に依存(可変長)
選択の基準
- 金額、通貨、為替レート →
NUMERIC必須
- 科学計算で正確な精度が必要 →
NUMERIC
- パフォーマンス重視で多少の誤差許容 →
REAL/DOUBLE PRECISION
浮動小数点型(REAL / DOUBLE PRECISION)#
IEEE 754標準に準拠した浮動小数点数です。高速な計算が可能ですが、近似値であることに注意が必要です。
| 型名 |
サイズ |
精度 |
| REAL |
4バイト |
約6桁 |
| DOUBLE PRECISION |
8バイト |
約15桁 |
1
2
3
4
5
6
|
-- 浮動小数点の近似誤差の例
SELECT 0.1::REAL + 0.2::REAL;
-- 結果: 0.30000001192092896(厳密な0.3ではない)
SELECT 0.1::NUMERIC + 0.2::NUMERIC;
-- 結果: 0.3(正確)
|
選択の基準
- 科学技術計算、統計処理で高速性が必要 →
DOUBLE PRECISION
- ストレージ容量を節約したい →
REAL
- 金額など正確性が必要 →
NUMERICを使用(浮動小数点は不適切)
シリアル型(SERIAL / BIGSERIAL)#
シリアル型は自動採番のための便利な型です。内部的にはシーケンスとINTEGER/BIGINTの組み合わせです。
| 型名 |
内部的な型 |
範囲 |
| SMALLSERIAL |
SMALLINT |
1 〜 32,767 |
| SERIAL |
INTEGER |
1 〜 2,147,483,647 |
| BIGSERIAL |
BIGINT |
1 〜 9,223,372,036,854,775,807 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
-- SERIALの使用例
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
order_date DATE NOT NULL
);
-- 自動的に連番が振られる
INSERT INTO orders (order_date) VALUES ('2026-01-03');
INSERT INTO orders (order_date) VALUES ('2026-01-04');
SELECT * FROM orders;
-- order_id | order_date
-- ---------+------------
-- 1 | 2026-01-03
-- 2 | 2026-01-04
-- SERIAL は以下と等価
-- CREATE SEQUENCE orders_order_id_seq;
-- CREATE TABLE orders (
-- order_id INTEGER NOT NULL DEFAULT nextval('orders_order_id_seq'),
-- ...
-- );
|
選択の基準
- 一般的なテーブルの主キー →
SERIAL
- 大規模システム(レコード数が21億を超える可能性) →
BIGSERIAL
- 分散システムでの一意性が必要 →
UUIDを検討
注意事項
シリアル型は欠番が発生する可能性があります。トランザクションのロールバックや削除によって番号が飛ぶことがあるため、「連続した番号」を期待する用途には適しません。
文字列型の選択#
PostgreSQLの文字列型は、用途に応じて3種類から選択できます。
VARCHAR / TEXT / CHAR の比較#
| 型名 |
説明 |
長さ制限 |
CHARACTER VARYING(n) / VARCHAR(n) |
可変長、最大n文字 |
指定した長さまで |
TEXT |
可変長、長さ制限なし |
約1GB |
CHARACTER(n) / CHAR(n) |
固定長、空白でパディング |
固定n文字 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
CREATE TABLE string_examples (
id SERIAL PRIMARY KEY,
code CHAR(5), -- 固定長5文字(短ければ空白でパディング)
username VARCHAR(50), -- 最大50文字の可変長
description TEXT -- 長さ制限なし
);
INSERT INTO string_examples (code, username, description)
VALUES ('ABC', 'tanaka', 'これは説明文です。長さに制限はありません。');
-- CHAR型は空白でパディングされる
SELECT code, LENGTH(code), username, LENGTH(username)
FROM string_examples;
-- code | length | username | length
-- ------+--------+----------+--------
-- ABC | 5 | tanaka | 6
|
パフォーマンスの真実#
PostgreSQLでは、VARCHAR、TEXT、CHARの間にパフォーマンスの差はほとんどありません。
1
2
3
4
|
-- 以下の3つはパフォーマンス面でほぼ同等
column_a VARCHAR(100)
column_b TEXT
column_c VARCHAR -- 長さ指定なし = TEXT と同等
|
公式ドキュメントより
「これら3つの型の間にパフォーマンスの差はありません。CHAR(n)は空白パディングによる追加のストレージ領域が必要になるため、むしろ最も遅くなる可能性があります。」
選択の基準#
flowchart TD
A[文字列型の選択] --> B{長さが固定?}
B -->|はい| C{規格で定められた固定長コード?}
C -->|はい| D["CHAR(n)<br/>例: 国コード, 通貨コード"]
C -->|いいえ| E["VARCHAR(n) を推奨"]
B -->|いいえ| F{最大長を制限したい?}
F -->|はい| G["VARCHAR(n)<br/>例: ユーザー名50文字"]
F -->|いいえ| H["TEXT<br/>例: 記事本文, コメント"]推奨される使い分け
| 用途 |
推奨型 |
理由 |
| メールアドレス |
VARCHAR(254) |
RFC 5321で最大254文字と規定 |
| ユーザー名 |
VARCHAR(50) |
UI/UXの観点から適切な上限を設定 |
| 記事本文 |
TEXT |
長さが不定、制限不要 |
| 国コード |
CHAR(2) |
ISO 3166-1で2文字固定 |
| 電話番号 |
VARCHAR(20) |
国際番号含め20文字程度 |
1
2
3
4
5
6
7
8
9
10
|
-- 実践的なテーブル設計例
CREATE TABLE members (
member_id SERIAL PRIMARY KEY,
email VARCHAR(254) NOT NULL UNIQUE,
username VARCHAR(50) NOT NULL,
display_name VARCHAR(100),
bio TEXT,
country_code CHAR(2),
phone VARCHAR(20)
);
|
日付時刻型の選択#
日付時刻の扱いはアプリケーション開発で頻出するテーマです。PostgreSQLは豊富な日付時刻型を提供しています。
日付時刻型の一覧#
| 型名 |
サイズ |
説明 |
範囲 |
DATE |
4バイト |
日付のみ |
紀元前4713年〜紀元294276年 |
TIME |
8バイト |
時刻のみ(タイムゾーンなし) |
00:00:00〜24:00:00 |
TIME WITH TIME ZONE |
12バイト |
時刻のみ(タイムゾーンあり) |
00:00:00+1559〜24:00:00-1559 |
TIMESTAMP |
8バイト |
日付と時刻(タイムゾーンなし) |
紀元前4713年〜紀元294276年 |
TIMESTAMP WITH TIME ZONE |
8バイト |
日付と時刻(タイムゾーンあり) |
紀元前4713年〜紀元294276年 |
INTERVAL |
16バイト |
時間間隔 |
-1億7800万年〜1億7800万年 |
TIMESTAMP vs TIMESTAMP WITH TIME ZONE#
最も重要な選択は、タイムゾーン情報を含めるかどうかです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
-- タイムゾーンの違いを確認
SET timezone = 'Asia/Tokyo';
CREATE TABLE timestamp_test (
id SERIAL PRIMARY KEY,
without_tz TIMESTAMP,
with_tz TIMESTAMP WITH TIME ZONE
);
INSERT INTO timestamp_test (without_tz, with_tz)
VALUES ('2026-01-03 15:00:00', '2026-01-03 15:00:00');
SELECT * FROM timestamp_test;
-- without_tz | with_tz
-- ---------------------+------------------------
-- 2026-01-03 15:00:00 | 2026-01-03 15:00:00+09
-- タイムゾーンを変更して再度確認
SET timezone = 'UTC';
SELECT * FROM timestamp_test;
-- without_tz | with_tz
-- ---------------------+------------------------
-- 2026-01-03 15:00:00 | 2026-01-03 06:00:00+00
|
動作の違い
TIMESTAMP: 入力された値をそのまま保存。タイムゾーン変換なし
TIMESTAMP WITH TIME ZONE: 内部的にUTCで保存。表示時にセッションのタイムゾーンに変換
選択の基準#
flowchart TD
A[日付時刻型の選択] --> B{時刻情報が必要?}
B -->|いいえ| C["DATE<br/>例: 生年月日, 入社日"]
B -->|はい| D{複数タイムゾーンのユーザー?}
D -->|はい| E["TIMESTAMP WITH TIME ZONE<br/>例: ログ, イベント時刻"]
D -->|いいえ| F{ローカル時刻として固定?}
F -->|はい| G["TIMESTAMP<br/>例: 店舗の営業時間"]
F -->|いいえ| E推奨される使い分け
| 用途 |
推奨型 |
理由 |
| 生年月日 |
DATE |
時刻不要 |
| 注文日時 |
TIMESTAMP WITH TIME ZONE |
正確な時点の記録が必要 |
| システムログ |
TIMESTAMP WITH TIME ZONE |
異なるタイムゾーンからのアクセス |
| 予約時間 |
TIMESTAMP |
ローカル時刻として固定したい場合 |
| 経過時間 |
INTERVAL |
期間の計算 |
DATE型の活用#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
-- DATE型の基本操作
CREATE TABLE events (
event_id SERIAL PRIMARY KEY,
event_name VARCHAR(100) NOT NULL,
event_date DATE NOT NULL
);
INSERT INTO events (event_name, event_date)
VALUES ('新年会', '2026-01-15');
-- 日付の計算
SELECT event_name,
event_date,
event_date - CURRENT_DATE AS days_until,
event_date + INTERVAL '1 week' AS next_week
FROM events;
-- 日付の比較
SELECT * FROM events
WHERE event_date BETWEEN '2026-01-01' AND '2026-01-31';
|
INTERVAL型の活用#
INTERVALは期間を表現する型で、日付時刻の計算に威力を発揮します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
-- INTERVAL型の基本操作
SELECT CURRENT_TIMESTAMP + INTERVAL '1 day' AS tomorrow;
SELECT CURRENT_TIMESTAMP + INTERVAL '2 hours 30 minutes' AS later;
SELECT CURRENT_TIMESTAMP - INTERVAL '1 month' AS last_month;
-- 年齢計算の例
CREATE TABLE employees (
employee_id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
birth_date DATE NOT NULL,
hire_date DATE NOT NULL
);
INSERT INTO employees (name, birth_date, hire_date)
VALUES ('田中太郎', '1990-05-15', '2015-04-01');
SELECT name,
birth_date,
AGE(birth_date) AS age,
AGE(hire_date) AS tenure
FROM employees;
-- name | birth_date | age | tenure
-- ---------+------------+--------------------+-----------------
-- 田中太郎 | 1990-05-15 | 35 years 7 mons... | 10 years 9 mons...
|
真偽値型(BOOLEAN)#
BOOLEAN型は、true/false/nullの3つの状態を持つ型です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
CREATE TABLE tasks (
task_id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
is_completed BOOLEAN DEFAULT FALSE,
is_urgent BOOLEAN DEFAULT FALSE
);
INSERT INTO tasks (title, is_completed, is_urgent) VALUES
('レポート作成', FALSE, TRUE),
('メール返信', TRUE, FALSE),
('会議準備', FALSE, FALSE);
-- 真偽値での検索
SELECT * FROM tasks WHERE is_completed = TRUE;
SELECT * FROM tasks WHERE is_completed; -- 上と同じ
SELECT * FROM tasks WHERE NOT is_completed;
SELECT * FROM tasks WHERE is_urgent AND NOT is_completed;
|
入力として受け付ける値
| TRUE |
FALSE |
TRUE, 't', 'true', 'y', 'yes', 'on', '1' |
FALSE, 'f', 'false', 'n', 'no', 'off', '0' |
出力形式
出力は常にtまたはfとなります。
1
2
3
4
|
SELECT TRUE, FALSE;
-- bool | bool
-- -----+------
-- t | f
|
UUID型#
UUID(Universally Unique Identifier)は、分散システムでの一意識別子として最適な型です。128ビットの値で、実質的に衝突しない一意性を持ちます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- UUIDの生成と使用(PostgreSQL 13以降はgen_random_uuid()が標準で使用可能)
CREATE TABLE api_keys (
key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INTEGER NOT NULL,
key_name VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO api_keys (user_id, key_name)
VALUES (1, 'Production API Key');
SELECT * FROM api_keys;
-- key_id | user_id | key_name
-- -------------------------------------+---------+---------------------
-- a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11 | 1 | Production API Key
|
SERIALとUUIDの比較#
| 観点 |
SERIAL |
UUID |
| サイズ |
4バイト(BIGSERIAL: 8バイト) |
16バイト |
| 生成速度 |
高速 |
やや遅い |
| 予測可能性 |
予測可能(連番) |
予測不可能 |
| 分散システム |
調整が必要 |
調整不要 |
| URL露出 |
リスクあり |
安全 |
選択の基準
- 内部的なIDで公開されない →
SERIAL / BIGSERIAL
- APIで外部に公開される →
UUID
- 分散システムで複数ノードからINSERT →
UUID
- セキュリティが重要(IDから情報を推測されたくない) →
UUID
配列型#
PostgreSQLでは、任意の型の配列をカラムとして定義できます。
配列型の宣言と操作#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
-- 配列型のテーブル作成
CREATE TABLE articles (
article_id SERIAL PRIMARY KEY,
title VARCHAR(200) NOT NULL,
tags TEXT[], -- 文字列の配列
scores INTEGER[] -- 整数の配列
);
-- 配列データの挿入
INSERT INTO articles (title, tags, scores) VALUES
('PostgreSQL入門', ARRAY['PostgreSQL', 'データベース', 'SQL'], ARRAY[85, 92, 78]),
('Python活用術', '{"Python", "プログラミング"}', '{90, 88, 95}');
-- 配列要素へのアクセス(1始まりのインデックス)
SELECT title, tags[1] AS first_tag, scores[1] AS first_score
FROM articles;
-- 配列のスライス
SELECT title, tags[1:2] AS first_two_tags
FROM articles;
|
配列の検索#
1
2
3
4
5
6
7
8
|
-- 特定の要素を含む配列を検索
SELECT * FROM articles WHERE 'PostgreSQL' = ANY(tags);
-- 配列に特定の要素がすべて含まれるか
SELECT * FROM articles WHERE tags @> ARRAY['PostgreSQL', 'SQL'];
-- 配列が重複しているか(共通要素があるか)
SELECT * FROM articles WHERE tags && ARRAY['Python', 'SQL'];
|
配列の更新と関数#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- 配列への要素追加
UPDATE articles
SET tags = array_append(tags, '入門')
WHERE article_id = 1;
-- 配列から要素削除
UPDATE articles
SET tags = array_remove(tags, '入門')
WHERE article_id = 1;
-- 配列の長さ
SELECT title, array_length(tags, 1) AS tag_count FROM articles;
-- 配列を行に展開
SELECT title, unnest(tags) AS tag FROM articles;
|
配列型の注意点#
配列型は便利ですが、以下の点に注意が必要です。
- 第一正規形に違反する可能性がある
- 要素が多い場合、検索パフォーマンスが低下する
- 配列要素を頻繁に検索する場合は、別テーブルへの正規化を検討
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
-- 配列より正規化が適切なケース
-- NG: 記事とタグの多対多関係を配列で表現
CREATE TABLE articles_bad (
article_id SERIAL PRIMARY KEY,
title VARCHAR(200),
tags TEXT[]
);
-- OK: 中間テーブルで正規化
CREATE TABLE articles_good (
article_id SERIAL PRIMARY KEY,
title VARCHAR(200)
);
CREATE TABLE tags (
tag_id SERIAL PRIMARY KEY,
tag_name VARCHAR(50) UNIQUE
);
CREATE TABLE article_tags (
article_id INTEGER REFERENCES articles_good(article_id),
tag_id INTEGER REFERENCES tags(tag_id),
PRIMARY KEY (article_id, tag_id)
);
|
JSONB型#
JSONBは、JSONデータをバイナリ形式で格納する型です。JSON型も存在しますが、ほとんどの場合JSONBを使用すべきです。
JSONとJSONBの違い#
| 観点 |
JSON |
JSONB |
| 格納形式 |
テキスト |
バイナリ |
| 入力速度 |
速い |
やや遅い(パース処理) |
| 検索速度 |
遅い(毎回パース) |
速い |
| インデックス |
不可 |
GINインデックス対応 |
| キー順序 |
保持 |
保持しない |
| 重複キー |
保持 |
最後の値のみ保持 |
結論: 特別な理由がない限りJSONBを使用しましょう。
JSONBの基本操作#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
CREATE TABLE user_profiles (
user_id SERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL,
profile JSONB
);
INSERT INTO user_profiles (username, profile) VALUES
('tanaka', '{"age": 30, "city": "Tokyo", "skills": ["Python", "SQL"], "social": {"twitter": "@tanaka", "github": "tanaka-dev"}}'),
('yamada', '{"age": 25, "city": "Osaka", "skills": ["JavaScript", "React"]}');
-- 値の取得(->はJSONB、->>はテキストを返す)
SELECT username,
profile -> 'age' AS age_json,
profile ->> 'age' AS age_text,
profile -> 'social' ->> 'twitter' AS twitter
FROM user_profiles;
-- 配列要素へのアクセス
SELECT username, profile -> 'skills' -> 0 AS first_skill
FROM user_profiles;
-- キーの存在チェック
SELECT * FROM user_profiles WHERE profile ? 'social';
-- 値での検索
SELECT * FROM user_profiles WHERE profile ->> 'city' = 'Tokyo';
SELECT * FROM user_profiles WHERE (profile ->> 'age')::INTEGER >= 28;
|
JSONBの更新#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
-- キーの追加・更新
UPDATE user_profiles
SET profile = profile || '{"verified": true}'::JSONB
WHERE username = 'tanaka';
-- ネストしたキーの更新
UPDATE user_profiles
SET profile = jsonb_set(profile, '{social, linkedin}', '"tanaka-pro"')
WHERE username = 'tanaka';
-- キーの削除
UPDATE user_profiles
SET profile = profile - 'verified'
WHERE username = 'tanaka';
-- ネストしたキーの削除
UPDATE user_profiles
SET profile = profile #- '{social, twitter}'
WHERE username = 'tanaka';
|
JSONBのインデックス#
JSONB型はGINインデックスを使用して高速な検索が可能です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
-- GINインデックスの作成
CREATE INDEX idx_profile ON user_profiles USING GIN (profile);
-- 包含演算子による検索(インデックスが効く)
SELECT * FROM user_profiles
WHERE profile @> '{"city": "Tokyo"}';
-- 配列内の検索(インデックスが効く)
SELECT * FROM user_profiles
WHERE profile -> 'skills' ? 'Python';
-- 特定のパスに対するインデックス
CREATE INDEX idx_profile_city ON user_profiles USING BTREE ((profile ->> 'city'));
SELECT * FROM user_profiles WHERE profile ->> 'city' = 'Tokyo';
|
JSONBの活用シーン#
| シーン |
適切 |
理由 |
| ユーザー設定 |
適切 |
スキーマが柔軟、検索頻度低 |
| ログデータ |
適切 |
構造が不定、分析用途 |
| APIレスポンスのキャッシュ |
適切 |
構造が外部依存 |
| 主要な業務データ |
検討が必要 |
リレーショナルモデルが望ましい場合も |
| 頻繁にJOINされるデータ |
不適切 |
正規化テーブルの方が効率的 |
データ型選択のまとめ#
よくある設計パターン#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
-- 実践的なテーブル設計例
CREATE TABLE customers (
-- 主キー: 外部公開するならUUID、内部のみならSERIAL
customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- 文字列: 適切な長さ制限
email VARCHAR(254) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
-- 電話番号: 国際番号対応
phone VARCHAR(20),
-- 住所: 長さ不定
address TEXT,
-- 金額: 正確な計算が必要
credit_limit NUMERIC(12, 2) DEFAULT 0,
-- フラグ: 真偽値
is_active BOOLEAN DEFAULT TRUE,
is_verified BOOLEAN DEFAULT FALSE,
-- 日時: タイムゾーン対応
registered_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMP WITH TIME ZONE,
-- 日付のみ
birth_date DATE,
-- 柔軟なメタデータ
preferences JSONB DEFAULT '{}'::JSONB,
-- 単純なリスト(検索頻度が低い場合)
tags TEXT[]
);
-- インデックス
CREATE INDEX idx_customers_email ON customers (email);
CREATE INDEX idx_customers_active ON customers (is_active) WHERE is_active = TRUE;
CREATE INDEX idx_customers_preferences ON customers USING GIN (preferences);
|
選択のフローチャート#
flowchart TD
A[データ型の選択] --> B{数値?}
B -->|はい| C{小数?}
C -->|いいえ| D["INTEGER / BIGINT"]
C -->|はい| E{正確性必須?}
E -->|はい| F["NUMERIC"]
E -->|いいえ| G["DOUBLE PRECISION"]
B -->|いいえ| H{文字列?}
H -->|はい| I{長さ上限?}
I -->|あり| J["VARCHAR(n)"]
I -->|なし| K["TEXT"]
H -->|いいえ| L{日時?}
L -->|はい| M{時刻必要?}
M -->|いいえ| N["DATE"]
M -->|はい| O{タイムゾーン?}
O -->|はい| P["TIMESTAMPTZ"]
O -->|いいえ| Q["TIMESTAMP"]
L -->|いいえ| R{真偽値?}
R -->|はい| S["BOOLEAN"]
R -->|いいえ| T{構造化データ?}
T -->|はい| U["JSONB"]
T -->|いいえ| V["UUID / 配列 等"]まとめ#
PostgreSQLのデータ型選択は、以下の観点から決定します。
- 正確性: 金額計算には
NUMERIC、近似値で良ければDOUBLE PRECISION
- 範囲: 値の最大値を考慮して
INTEGERとBIGINTを選択
- 柔軟性: 構造が不定なデータには
JSONB、固定構造には正規化テーブル
- パフォーマンス: インデックスの効きやすさを考慮
- セキュリティ: 公開IDには
UUID、内部IDにはSERIAL
適切なデータ型を選択することで、データの整合性を保ちながら、効率的なストレージとクエリパフォーマンスを実現できます。
参考リンク#