PostgreSQL: PostgreSQLデータ型:完全ガイド

最終更新:2026-08-26

適切なデータ型の選択は、データベース設計の基盤です。ストレージ効率、クエリパフォーマンス、データ精度を決定します。

1. 学習内容


2. 開発者の実体験

(1) 悩み:データ型について混乱

BobはEコマースデータベースのテーブル設計時に、一連の難しい選択に直面しました:

(2) 解決策:正確な型の選択

PostgreSQLは豊富な専用データ型を提供しており、正確に選択することでストレージを節約しつつ精度を保証します:

SQL
-- 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) 得られた成果


3. データ型決定フロー

100%
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、金額(セント単位)

▶ サンプル:整数型の選択

SQL
-- 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:

TEXT 📖 参照専用
CREATE TABLE
💡 ヒント: INTEGERかBIGINTか迷ったら?主キーIDで10億行を超える見込みがある場合は、最初からBIGINTにしましょう。1行あたり4バイトの追加(100万行あたりたった4MB)で済みますが、後で大規模テーブルのALTER 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
-- 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:

TEXT 📖 参照専用
INSERT 0 1

6. 浮動小数点と正確な数値

(1) REAL / DOUBLE vs DECIMAL

ストレージ 精度 最適な用途
REAL 4バイト 有効数字6桁 科学計算、センサー
DOUBLE PRECISION 8バイト 有効数字15桁 科学計算、統計
DECIMAL(p,s) 可変 正確 金額、レート(推奨)
NUMERIC(p,s) 可変 正確 DECIMALと同じ

▶ サンプル:浮動小数点の精度の落とし穴

SQL
-- 浮動小数点の精度トラップ: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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

▶ サンプル:金額にDECIMALを使用

SQL
-- 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:

TEXT 📖 参照専用
 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 可変長 無制限 長さ制限のないテキスト

▶ サンプル:文字列型の比較

SQL
-- 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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)
💡 ヒント: PostgreSQLでは、VARCHARとTEXTのクエリおよびストレージパフォーマンスは同一です。TEXTに長さ制限がないからといってPGが遅くなることはありません。どちらを選ぶかは、データベースに長さ制限を強制させる必要があるかどうかだけです。


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

SQL
-- タイムゾーンを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:

TEXT 📖 参照専用
INSERT 0 1

▶ サンプル:日付/時刻関数

SQL
-- 現在の日付と時刻
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

9. 特殊型

(1) UUID

SQL
-- 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アドレスの保存とクエリ

SQL
-- 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:

TEXT 📖 参照専用
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つの表現

SQL
-- ブーリアンは複数の表現を受け付ける
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:

TEXT 📖 参照専用
 id | name     | value 
----+----------+-------
  1 | example  | 42
(1 row)

(4) ENUM

▶ サンプル:カスタム列挙型

SQL
-- 列挙型を作成(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:

TEXT 📖 参照専用
INSERT 0 1
⚠️ 注意: PGのENUM型は一度作成すると、新しい値の追加には 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. 完全な例:多様な型を使用したテーブル

SQL
-- ============================================
-- 完全な例:主要な型をすべて使用したテーブル
-- センサーデータ収集システム
-- ============================================

-- 列挙型を作成
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';

❓ よくある質問

Q DECIMALとNUMERICの違いは?
A PostgreSQLでは、DECIMALとNUMERICは完全に同等で互換性があります。SQL標準では微妙な区別がありますが、PGの実装は同一です。DECIMALが推奨されます(より直感的です)。
Q SERIAL列のシーケンスが番号をスキップするのはなぜ?
A SERIALはシーケンスオブジェクトを使用しており、失敗またはロールバックされたINSERTの後もシーケンス値はロールバックされません(同時実行の安全性を保証するための設計です)。欠番は正常であり機能に影響しません。連続した値が必須の場合は、シーケンスに頼るのではなくアプリケーション層で処理してください。
Q VARCHAR(50)をVARCHAR(100)に変更するとテーブルがロックされますか?
A PostgreSQLでは、VARCHARの長さを増やす場合はテーブルをロックしません(データの書き換えなし)ので即座に完了します。長さを縮める場合や型を変更する場合はテーブルがロックされます。
Q JSONとJSONBのどちらを選ぶべき?
A ほぼ常にJSONBです。JSONBはバイナリで保存されインデックス検索をサポートするため高速です。JSONはテキストで保存され、毎回のクエリで再解析が必要です。JSONを選ぶ唯一のケース:入力の空白/キー順序を保持する必要がある場合(例:監査ログ)。
Q TIMESTAMPTZがTIMESTAMPより推奨される理由は?
A TIMESTAMPTZは保存時にUTCに変換し、読み取り時にクライアントのタイムゾーンに戻します。つまりグローバルユーザーが各自のローカル時間を混乱なく確認できます。TIMESTAMPは与えられた値をそのまま保存・返却するため、タイムゾーンを跨ぐアプリケーションでは曖昧です。
Q 1つのテーブルで複数のデータ型を混在させるとパフォーマンスが低下しますか?
A いいえ。PGは型情報を列ごとに保存し、必要な列のみを読み取ります。専用の型を適切に使用する(例:IPにINET)ことで、型の演算子がインデックスを使用できるため、クエリパフォーマンスが実際に向上します。

📖 まとめ


📝 練習問題

  1. 基礎(★): id(SERIAL主キー)、name(VARCHAR(100))、iso_code(CHAR(2))、population(BIGINT)、gdp DECIMAL(15,2)、is_developed(BOOLEAN)を持つ countries テーブルを作成し、テストデータを3行挿入してください。

  2. 中級(★★): 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 ネットワークからの全ログをクエリしてください。

  3. 発展(★★★): 異なる小数点精度(USD/EUR:小数点2桁、JPY:小数点0桁、CNY:小数点2桁)を持つ複数通貨(USD/EUR/JPY/CNY)を扱う financial_transactions テーブルを設計してください。DECIMAL + CHECK制約を使用して、異なる通貨で異なる小数点スケールを要求するよう実装してください。CREATE TABLEのSQLと、異なる通貨のINSERT例を3つ記述してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%