はじめに

データベース設計において、適切なデータ型の選択はパフォーマンス、ストレージ効率、データ整合性に直結する重要な決定です。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)

NUMERICDECIMALと同義)は、正確な精度が必要な数値に使用します。金額計算など、丸め誤差が許容されない場面で必須の型です。

 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では、VARCHARTEXTCHARの間にパフォーマンスの差はほとんどありません。

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のデータ型選択は、以下の観点から決定します。

  1. 正確性: 金額計算にはNUMERIC、近似値で良ければDOUBLE PRECISION
  2. 範囲: 値の最大値を考慮してINTEGERBIGINTを選択
  3. 柔軟性: 構造が不定なデータにはJSONB、固定構造には正規化テーブル
  4. パフォーマンス: インデックスの効きやすさを考慮
  5. セキュリティ: 公開IDにはUUID、内部IDにはSERIAL

適切なデータ型を選択することで、データの整合性を保ちながら、効率的なストレージとクエリパフォーマンスを実現できます。

参考リンク