PostgreSQL: PostgreSQLデータ型:完全ガイド
最終更新:2026-08-26
適切なデータ型の選択は、データベース設計の基盤です。ストレージ効率、クエリパフォーマンス、データ精度を決定します。
1. 学習内容
- 整数型(SMALLINT / INTEGER / BIGINT)
- 自動採番シーケンス(SERIAL / BIGSERIAL / IDENTITY)
- 浮動小数点と正確な数値(REAL / DOUBLE / DECIMAL / NUMERIC)
- 文字列型(CHAR / VARCHAR / TEXT)
- 日付/時刻型(DATE / TIME / TIMESTAMP / TIMESTAMPTZ / INTERVAL)
- 特殊型(UUID / INET / BOOLEAN / ENUM / JSONB)
2. 開発者の実体験
(1) 悩み:データ型について混乱
BobはEコマースデータベースのテーブル設計時に、一連の難しい選択に直面しました:
- ユーザーIDはINTEGERかBIGINTか?ユーザー数が21億を超えたらどうする?
- 商品価格はREALかDECIMALか?浮動小数点には精度の問題があるらしい。
- 商品説明はVARCHAR(5000)かTEXTか?
- タイムスタンプはTIMESTAMPかTIMESTAMPTZか?グローバルユーザーにはどう対応?
- IPアドレスはVARCHARで保存すべきか、専用の型があるのか?
(2) 解決策:正確な型の選択
PostgreSQLは豊富な専用データ型を提供しており、正確に選択することでストレージを節約しつつ精度を保証します:
-- Eコマースデータベースの正しい型選択
CREATE TABLE smart_products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, -- 将来を見据えたID
name VARCHAR(200) NOT NULL, -- 制限付き文字列
description TEXT, -- 無制限テキスト
price DECIMAL(10, 2) NOT NULL, -- 正確な金額
weight_kg REAL, -- 近似値で十分
is_available BOOLEAN DEFAULT true, -- はい/いいえフラグ
created_at TIMESTAMPTZ DEFAULT NOW(), -- タイムゾーン対応
source_ip INET, -- IPアドレス型
attributes JSONB DEFAULT '{}'::jsonb -- 柔軟なスキーマ
);
(3) 得られた成果
- DECIMALは金額の精度損失ゼロを保証(89.99は常に89.99、89.9899999にはならない)
- TIMESTAMPTZはタイムゾーンを自動処理し、グローバルユーザーが各自のローカル時間を確認可能
- INETはIP範囲クエリをサポートし、VARCHARでのIP保存より10倍効率的
- JSONBは動的属性を柔軟に保存し、頻繁なALTER TABLE変更を回避
3. データ型決定フロー
graph TB
START[どのようなデータ?] --> NUM{数値?}
NUM -->|はい| INT{正確な<br/>精度が必要?}
INT -->|はい、金額/レート| DEC[DECIMAL / NUMERIC]
INT -->|いいえ、測定値| FLOAT[REAL / DOUBLE PRECISION]
NUM -->|整数| RANGE{値の範囲?}
RANGE -->|-32768〜32767| SMALL[SMALLINT]
RANGE -->|-21億〜21億| INT2[INTEGER]
RANGE -->|より大きい| BIG[BIGINT]
NUM -->|自動採番ID| AUTO[SERIAL / IDENTITY]
START --> STR{テキスト?}
STR -->|固定長| CHAR[CHAR]
STR -->|可変長(制限付き)| VAR[VARCHAR n]
STR -->|無制限| TEXT2[TEXT]
START --> TIME{日付/時刻?}
TIME -->|日付のみ| DATE2[DATE]
TIME -->|時刻のみ| TIME2[TIME]
TIME -->|タイムスタンプ| TZ{タイムゾーン?}
TZ -->|はい、グローバルアプリ| TSTZ[TIMESTAMPTZ]
TZ -->|いいえ、ローカルのみ| TS[TIMESTAMP]
TIME -->|期間| IV[INTERVAL]
START --> SPEC{特殊?}
SPEC -->|はい/いいえ| BOOL[BOOLEAN]
SPEC -->|UUID| UUID2[UUID]
SPEC -->|IPアドレス| IP[INET / CIDR]
SPEC -->|列挙リスト| ENUM2[ENUM]
SPEC -->|JSONドキュメント| JSON[JSONB]
4. 整数型
| 型 | ストレージ | 範囲 | 典型的な用途 |
|---|---|---|---|
SMALLINT |
2バイト | -32,768 〜 32,767 | 年齢、評価、ステータスコード |
INTEGER(INT) |
4バイト | -2,147,483,648 〜 2,147,483,647 | 一般的な整数、主キーID |
BIGINT |
8バイト | ±9,223,372,036,854,775,807 | 大規模テーブルのID、金額(セント単位) |
▶ サンプル:整数型の選択
-- SMALLINT:小範囲の値
CREATE TABLE ratings (
user_id INTEGER REFERENCES users(id),
product_id INTEGER REFERENCES products(id),
score SMALLINT CHECK (score BETWEEN 1 AND 5), -- 1-5はSMALLINTの範囲内で十分
PRIMARY KEY (user_id, product_id)
);
-- INTEGER:ほとんどのID
CREATE TABLE categories (
id SERIAL PRIMARY KEY, -- SERIAL = INTEGER + 自動採番シーケンス
name VARCHAR(100) NOT NULL
);
-- BIGINT:高成長テーブル
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY, -- BIGSERIAL = BIGINT + 自動採番
action VARCHAR(50),
details JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Output:
CREATE TABLE
5. 自動採番シーケンス
(1) SERIAL vs IDENTITY
| 方法 | 構文 | 標準 | 手動挿入? | 推奨 |
|---|---|---|---|---|
SERIAL |
id SERIAL PRIMARY KEY |
PG固有 | 可 | レガシー互換性 |
BIGSERIAL |
id BIGSERIAL PRIMARY KEY |
PG固有 | 可 | レガシー互換性 |
IDENTITY |
id INT GENERATED ALWAYS AS IDENTITY |
SQL標準 | OVERRIDINGが必要 | ✅ 新規プロジェクト |
▶ サンプル:IDENTITY列(推奨)
-- SQL標準のIDENTITY列(PostgreSQL 10+)
CREATE TABLE orders_v2 (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
total DECIMAL(12, 2) DEFAULT 0
);
-- idを指定せずに挿入(自動生成)
INSERT INTO orders_v2 (user_id, total) VALUES (1, 99.99);
-- idを手動で挿入しようとすると失敗(GENERATED ALWAYSの場合)
-- INSERT INTO orders_v2 (id, user_id, total) VALUES (100, 1, 50.00);
-- 時々手動上書きが必要な場合はGENERATED BY DEFAULTを使用
CREATE TABLE logs (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
message TEXT
);
Output:
INSERT 0 1
6. 浮動小数点と正確な数値
(1) REAL / DOUBLE vs DECIMAL
| 型 | ストレージ | 精度 | 最適な用途 |
|---|---|---|---|
REAL |
4バイト | 有効数字6桁 | 科学計算、センサー |
DOUBLE PRECISION |
8バイト | 有効数字15桁 | 科学計算、統計 |
DECIMAL(p,s) |
可変 | 正確 | 金額、レート(推奨) |
NUMERIC(p,s) |
可変 | 正確 | DECIMALと同じ |
▶ サンプル:浮動小数点の精度の落とし穴
-- 浮動小数点の精度トラップ:0.1 + 0.2 != 0.3
SELECT 0.1::REAL + 0.2::REAL = 0.3::REAL AS float_equal;
-- 出力:f(偽!)
-- DECIMALは精度損失なし
SELECT 0.1::DECIMAL + 0.2::DECIMAL = 0.3::DECIMAL AS decimal_equal;
-- 出力:t(真!)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
▶ サンプル:金額にDECIMALを使用
-- DECIMAL(精度, スケール)
-- 精度 = 総桁数、スケール = 小数点以下の桁数
-- DECIMAL(10,2) = 最大99,999,999.99
CREATE TABLE products_precise (
id SERIAL PRIMARY KEY,
name VARCHAR(200),
price DECIMAL(10, 2) NOT NULL, -- 最大99,999,999.99
tax_rate DECIMAL(5, 4) DEFAULT 0.0875, -- 0.0875 = 8.75%
discount NUMERIC(5, 2) DEFAULT 0.00 -- 最大999.99%
);
-- 金額計算は正確
SELECT name,
price,
price * tax_rate AS tax_amount,
price + (price * tax_rate) AS price_with_tax
FROM products_precise;
Output:
count
-------
5
(1 row)
| 精度の選択 | DECIMALパラメータ | 最大値 | 最適な用途 |
|---|---|---|---|
| 小規模Eコマース | DECIMAL(8,2) |
999,999.99 | 日次取引量100万未満 |
| 大規模Eコマース | DECIMAL(12,2) |
99,999,999,999.99 | グローバルEコマース |
| 暗号資産 | DECIMAL(20,8) |
非常に大きい | BTCの8桁精度 |
7. 文字列型
| 型 | ストレージ | 最大長 | 最適な用途 |
|---|---|---|---|
CHAR(n) |
固定長、スペースパディング | n | ハッシュ、ISOコード |
VARCHAR(n) |
可変長 | n | 長さ制限のあるテキスト |
TEXT |
可変長 | 無制限 | 長さ制限のないテキスト |
▶ サンプル:文字列型の比較
-- CHAR:固定長(スペースでパディング)
SELECT LENGTH('abc'::CHAR(5));
-- 出力:5(5文字にパディング)
-- VARCHAR:制限付き可変長
SELECT LENGTH('abc'::VARCHAR(5));
-- 出力:3(そのまま保存、最大5)
-- TEXT:可変長、制限なし
SELECT LENGTH('abc'::TEXT);
-- 出力:3(制限なし)
-- パフォーマンステスト:PGではVARCHARとTEXTは同一
-- (MySQLのようにVARCHARがTEXTより速いということはない)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
8. 日付と時刻型
| 型 | ストレージ | 範囲 | 最適な用途 |
|---|---|---|---|
DATE |
4バイト | 紀元前4713年〜西暦5874897年 | 日付のみ(誕生日、祝日) |
TIME |
8バイト | 00:00:00〜24:00:00 | 時刻のみ(営業時間) |
TIMESTAMP |
8バイト | 紀元前4713年〜西暦294276年 | 日付+時刻(タイムゾーンなし) |
TIMESTAMPTZ |
8バイト | 同上 | 日付+時刻+タイムゾーン(✅推奨) |
INTERVAL |
16バイト | ±1億7800万年 | 時間の長さ |
▶ サンプル:TIMESTAMP vs TIMESTAMPTZ
-- タイムゾーンをUTCに設定
SET timezone = 'UTC';
-- 異なる型で同じ時刻を挿入
INSERT INTO test_times (ts_no_tz, ts_with_tz) VALUES
('2026-07-13 10:00:00', '2026-07-13 10:00:00+00');
-- 東京のタイムゾーンに変更
SET timezone = 'Asia/Tokyo';
-- TIMESTAMP(タイムゾーンなし)は同じリテラルを表示
SELECT ts_no_tz FROM test_times;
-- 出力:2026-07-13 10:00:00(変更なし、どのタイムゾーンか不明!)
-- TIMESTAMPTZはローカルタイムゾーンに変換
SELECT ts_with_tz FROM test_times;
-- 出力:2026-07-13 19:00:00+09(10:00 UTC = 19:00 東京)
Output:
INSERT 0 1
▶ サンプル:日付/時刻関数
-- 現在の日付と時刻
SELECT NOW(); -- 2026-07-13 10:30:00.123456+00
SELECT CURRENT_DATE; -- 2026-07-13
SELECT CURRENT_TIMESTAMP; -- NOW()と同じ
-- INTERVALを使った日付演算
SELECT NOW() + INTERVAL '7 days'; -- 1週間後
SELECT NOW() - INTERVAL '3 months'; -- 3ヶ月前
-- 年齢計算
SELECT AGE(TIMESTAMP '1990-05-15'); -- 36年1ヶ月28日
-- 部分抽出
SELECT EXTRACT(YEAR FROM NOW()); -- 2026
SELECT EXTRACT(MONTH FROM NOW()); -- 7
SELECT EXTRACT(DOW FROM NOW()); -- 1(月曜日、0=日曜日)
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
9. 特殊型
(1) UUID
-- uuid-ossp拡張を有効化
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- 異なるUUIDバージョンを生成
SELECT uuid_generate_v4(); -- ランダムUUID(最も一般的)
SELECT uuid_generate_v1(); -- 時刻ベースUUID
| UUIDバージョン | 生成方法 | 最適な用途 |
|---|---|---|
| v1 | タイムスタンプ+MACアドレス | 時刻順序付け |
| v4 | ランダム | ほとんどのシナリオ(推奨) |
| v7 | タイムスタンプ+ランダム(新標準) | 時刻順序付け+ランダム |
(2) INET / CIDR(ネットワークアドレス)
▶ サンプル:IPアドレスの保存とクエリ
-- INET:単一IPまたはIP範囲
CREATE TABLE access_log (
id BIGSERIAL PRIMARY KEY,
source_ip INET NOT NULL,
access_time TIMESTAMPTZ DEFAULT NOW()
);
INSERT INTO access_log (source_ip) VALUES
('192.168.1.100'),
('10.0.0.5'),
('2001:db8::1');
-- クエリ:サブネット内の全IPを検索(VARCHARでは不可能!)
SELECT source_ip FROM access_log
WHERE source_ip << '192.168.1.0/24'::INET;
-- << は「含まれる」を意味する
-- CIDR:ネットワーク範囲
SELECT '192.168.1.0/24'::CIDR;
Output:
INSERT 0 1
| 演算子 | 意味 | 例 |
|---|---|---|
<< |
含まれる | 192.168.1.5' << '192.168.1.0/24' = true |
>> |
含む | '192.168.1.0/24' >> '192.168.1.5' = true |
= |
等しい | '192.168.1.5'::INET = '192.168.1.5'::INET |
(3) BOOLEAN
▶ サンプル:ブーリアンの3つの表現
-- ブーリアンは複数の表現を受け付ける
SELECT true, 't', 'true', 'yes', 'on', '1'; -- すべて = TRUE
SELECT false, 'f', 'false', 'no', 'off', '0'; -- すべて = FALSE
-- WHERE句でのブーリアン
SELECT name FROM products WHERE is_available IS TRUE;
SELECT name FROM products WHERE is_available IS NOT FALSE;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(4) ENUM
▶ サンプル:カスタム列挙型
-- 列挙型を作成(1回作成すれば複数テーブルで再利用可能)
CREATE TYPE order_status AS ENUM (
'pending', 'paid', 'shipped', 'delivered', 'cancelled'
);
-- テーブル定義で使用
CREATE TABLE orders_enum (
id SERIAL PRIMARY KEY,
status order_status DEFAULT 'pending'
);
-- 列挙値は自動的に検証される
INSERT INTO orders_enum (status) VALUES ('pending'); -- OK
-- INSERT INTO orders_enum (status) VALUES ('unknown'); -- エラー!
Output:
INSERT 0 1
ALTER TYPE ... ADD VALUE が必要です(PG 9.1以降はトランザクション内で許可)。値が頻繁に変更される場合は、VARCHAR + CHECK制約の方が柔軟です。
10. データ型比較表
| ニーズ | ❌ 非推奨 | ✅ 推奨 | 理由 |
|---|---|---|---|
| 金額 | REAL / FLOAT | DECIMAL(p,s) | 浮動小数点は精度を失う |
| タイムスタンプ | TIMESTAMP | TIMESTAMPTZ | グローバルアプリにはタイムゾーンが必要 |
| IPアドレス | VARCHAR | INET | INETは範囲クエリをサポート |
| はい/いいえ | INTEGER(0/1) | BOOLEAN | より明確な意味 |
| 長文テキスト | VARCHAR(9999) | TEXT | TEXTは制限がなく同じパフォーマンス |
| ステータス列挙 | VARCHAR + CHECK | ENUMまたはVARCHAR+CHECK | 安定しているならENUM、頻繁に変更されるならCHECK |
| 柔軟な属性 | 多数のNULL列 | JSONB | 動的フィールドを1列で |
| 自動採番ID | SERIAL | IDENTITY | SQL標準構文 |
11. 完全な例:多様な型を使用したテーブル
-- ============================================
-- 完全な例:主要な型をすべて使用したテーブル
-- センサーデータ収集システム
-- ============================================
-- 列挙型を作成
CREATE TYPE sensor_status AS ENUM ('active', 'inactive', 'maintenance');
-- 多様なデータ型でテーブルを作成
CREATE TABLE sensors (
-- 識別
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
uuid UUID DEFAULT uuid_generate_v4() UNIQUE,
-- テキスト
name VARCHAR(100) NOT NULL,
description TEXT,
location_code CHAR(10),
-- 数値
latitude DECIMAL(9, 6) CHECK (latitude BETWEEN -90 AND 90),
longitude DECIMAL(9, 6) CHECK (longitude BETWEEN -180 AND 180),
altitude REAL,
-- ネットワーク
ip_address INET,
subnet CIDR,
-- ステータス
status sensor_status DEFAULT 'active',
is_online BOOLEAN DEFAULT false,
-- 時間
installed_at DATE,
last_reading_at TIMESTAMPTZ,
reading_interval INTERVAL DEFAULT INTERVAL '5 minutes',
-- 柔軟なデータ
metadata JSONB DEFAULT '{}'::jsonb,
-- 監査
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- サンプルデータを挿入
INSERT INTO sensors (name, latitude, longitude, ip_address, status, installed_at, metadata)
VALUES (
'温度センサー A1',
35.6762, 139.6503,
'192.168.1.50',
'active',
'2025-01-15',
'{"model": "TX-200", "unit": "celsius", "range_min": -40, "range_max": 85}'::jsonb
);
-- 型固有の演算子を使ったクエリ
SELECT name, ip_address, metadata->>'model' AS model
FROM sensors
WHERE ip_address << '192.168.1.0/24'::INET
AND status = 'active';
❓ よくある質問
📖 まとめ
- 整数の選択:SMALLINT(小範囲) / INTEGER(デフォルト) / BIGINT(大規模テーブルID)、自動採番にはIDENTITYを優先
- 金額は必ずDECIMALを使用、浮動小数点型は精度を失う
- 文字列:VARCHAR(長さ制限あり) / TEXT(無制限)、パフォーマンスは同一
- 時間:TIMESTAMPTZ(タイムゾーン対応)を推奨、INTERVALは時間演算用
- 特殊型:UUID(分散ID) / INET(IPアドレス) / BOOLEAN(はい/いいえ) / ENUM(固定列挙) / JSONB(柔軟な属性)
- 核心原則:最も正確な型を選ぶこと。迷ったら大きい方を選ぶ(BIGINTは1行あたり4バイト増えるだけだが、ALTER TABLEで大規模テーブルを書き換えるコストははるかに高い)
📝 練習問題
-
基礎(★): id(SERIAL主キー)、name(VARCHAR(100))、iso_code(CHAR(2))、population(BIGINT)、gdp DECIMAL(15,2)、is_developed(BOOLEAN)を持つ
countriesテーブルを作成し、テストデータを3行挿入してください。 -
中級(★★): id(BIGINT IDENTITY主キー)、source_ip(INET)、request_time(TIMESTAMPTZ)、response_time_ms(INTEGER)、is_error(BOOLEAN)、メタデータ(JSONB)を持つ
server_logsテーブルを作成し、ログレコードを2件挿入した後、10.0.0.0/8ネットワークからの全ログをクエリしてください。 -
発展(★★★): 異なる小数点精度(USD/EUR:小数点2桁、JPY:小数点0桁、CNY:小数点2桁)を持つ複数通貨(USD/EUR/JPY/CNY)を扱う
financial_transactionsテーブルを設計してください。DECIMAL + CHECK制約を使用して、異なる通貨で異なる小数点スケールを要求するよう実装してください。CREATE TABLEのSQLと、異なる通貨のINSERT例を3つ記述してください。