PostgreSQL カラム一覧取得の完全ガイド:テーブル構造を瞬時に把握する全テクニック

データベース・SQL

「このテーブルにどんなカラムがあるか確認したい」 「カラムのデータ型や制約を一覧で見たい」 「複数テーブルのカラム情報をまとめて取得したい」

PostgreSQLでデータベース開発をしていると、こんなニーズが頻繁に発生しませんか?

カラム一覧の効率的な確認方法を知ることは、開発速度を大幅に向上させます。適切な方法を使えば、テーブル構造の理解、ドキュメント作成、データ移行の計画などが格段に楽になります。

この記事では、PostgreSQLでカラム一覧を取得するすべての方法から、実践的な活用例、自動化スクリプトまで、実例を交えながら徹底的に解説していきます。


スポンサーリンク

🔍 基本的なカラム一覧の確認方法

psqlメタコマンドでの確認

\d コマンド(最も簡単な方法)

基本的な使い方:

-- テーブルの詳細情報(カラム一覧含む)
\d users

-- 実行結果
                         Table "public.users"
    Column    |            Type             | Collation | Nullable |              Default
--------------+-----------------------------+-----------+----------+-----------------------------------
 user_id      | integer                     |           | not null | nextval('users_user_id_seq')
 username     | character varying(50)       |           | not null |
 email        | character varying(100)      |           | not null |
 created_at   | timestamp without time zone |           |          | CURRENT_TIMESTAMP
 updated_at   | timestamp without time zone |           |          |
 is_active    | boolean                     |           |          | true
 age          | integer                     |           |          |
 bio          | text                        |           |          |
Indexes:
    "users_pkey" PRIMARY KEY, btree (user_id)
    "users_email_key" UNIQUE, btree (email)

\d+ コマンド(詳細情報付き)

-- より詳細な情報を表示
\d+ orders

-- 実行結果(追加情報含む)
                                    Table "public.orders"
    Column    |            Type             | ... | Storage  | Stats target | Description
--------------+-----------------------------+-----+----------+--------------+-------------
 order_id     | integer                     | ... | plain    |              | 注文ID
 customer_id  | integer                     | ... | plain    |              | 顧客ID
 order_date   | date                        | ... | plain    |              | 注文日
 total_amount | numeric(10,2)               | ... | main     |              | 合計金額
 status       | character varying(20)       | ... | extended |              | 注文ステータス

information_schema を使用した確認

標準SQLでのカラム情報取得

基本的なクエリ:

-- 特定テーブルのカラム一覧
SELECT 
    column_name,
    data_type,
    character_maximum_length,
    is_nullable,
    column_default
FROM information_schema.columns
WHERE table_schema = 'public'
  AND table_name = 'users'
ORDER BY ordinal_position;

-- 実行結果
column_name | data_type         | character_maximum_length | is_nullable | column_default
------------+-------------------+--------------------------+-------------+-----------------------------
user_id     | integer           |                          | NO          | nextval('users_user_id_seq')
username    | character varying | 50                       | NO          |
email       | character varying | 100                      | NO          |
created_at  | timestamp         |                          | YES         | CURRENT_TIMESTAMP
updated_at  | timestamp         |                          | YES         |
is_active   | boolean           |                          | YES         | true

より詳細な情報を含むクエリ

-- 完全な型情報と制約を含む
SELECT 
    c.column_name,
    c.data_type,
    CASE 
        WHEN c.character_maximum_length IS NOT NULL 
        THEN c.data_type || '(' || c.character_maximum_length || ')'
        WHEN c.numeric_precision IS NOT NULL 
        THEN c.data_type || '(' || c.numeric_precision || ',' || c.numeric_scale || ')'
        ELSE c.data_type
    END as full_data_type,
    c.is_nullable,
    c.column_default,
    c.is_identity,
    c.is_generated,
    c.generation_expression
FROM information_schema.columns c
WHERE c.table_schema = 'public'
  AND c.table_name = 'users'
ORDER BY c.ordinal_position;

システムカタログでの確認

pg_catalog の活用

-- システムカタログを直接参照
SELECT 
    a.attname as column_name,
    pg_catalog.format_type(a.atttypid, a.atttypmod) as data_type,
    a.attnotnull as not_null,
    a.atthasdef as has_default,
    pg_get_expr(d.adbin, d.adrelid) as default_value,
    col_description(a.attrelid, a.attnum) as comment
FROM pg_catalog.pg_attribute a
LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum) = (d.adrelid, d.adnum)
WHERE a.attrelid = 'public.users'::regclass
  AND a.attnum > 0 
  AND NOT a.attisdropped
ORDER BY a.attnum;

-- 実行結果
column_name | data_type                   | not_null | has_default | default_value                  | comment
------------+-----------------------------+----------+-------------+--------------------------------+---------
user_id     | integer                     | t        | t           | nextval('users_user_id_seq')  | ユーザーID
username    | character varying(50)       | t        | f           |                                | ユーザー名
email       | character varying(100)      | t        | f           |                                | メールアドレス
created_at  | timestamp without time zone | f        | t           | CURRENT_TIMESTAMP              | 作成日時

📊 複数テーブルのカラム一覧取得

データベース全体のカラム情報

すべてのテーブルとカラムを一覧化

-- データベース内の全テーブルのカラム一覧
SELECT 
    t.table_schema,
    t.table_name,
    c.column_name,
    c.data_type,
    c.is_nullable,
    c.column_default
FROM information_schema.tables t
JOIN information_schema.columns c 
    ON t.table_schema = c.table_schema 
    AND t.table_name = c.table_name
WHERE t.table_schema NOT IN ('pg_catalog', 'information_schema')
  AND t.table_type = 'BASE TABLE'
ORDER BY t.table_schema, t.table_name, c.ordinal_position;

特定パターンのテーブル

LIKE条件での絞り込み

-- 特定のプレフィックスを持つテーブルのカラム一覧
WITH target_tables AS (
    SELECT table_schema, table_name
    FROM information_schema.tables
    WHERE table_schema = 'public'
      AND table_name LIKE 'user_%'
      AND table_type = 'BASE TABLE'
)
SELECT 
    t.table_name,
    c.column_name,
    c.data_type,
    CASE 
        WHEN c.is_nullable = 'NO' THEN 'NOT NULL'
        ELSE ''
    END as nullable,
    COALESCE(c.column_default, '') as default_value
FROM target_tables t
JOIN information_schema.columns c 
    ON t.table_schema = c.table_schema 
    AND t.table_name = c.table_name
ORDER BY t.table_name, c.ordinal_position;

🔧 カラムの詳細情報取得

制約情報の確認

PRIMARY KEY, FOREIGN KEY, UNIQUE制約

-- カラムに設定された制約を確認
WITH column_constraints AS (
    SELECT 
        kcu.table_schema,
        kcu.table_name,
        kcu.column_name,
        tc.constraint_type,
        tc.constraint_name
    FROM information_schema.key_column_usage kcu
    JOIN information_schema.table_constraints tc
        ON kcu.constraint_name = tc.constraint_name
        AND kcu.table_schema = tc.table_schema
    WHERE kcu.table_schema = 'public'
)
SELECT 
    c.table_name,
    c.column_name,
    c.data_type,
    STRING_AGG(
        cc.constraint_type, 
        ', ' 
        ORDER BY 
            CASE cc.constraint_type 
                WHEN 'PRIMARY KEY' THEN 1
                WHEN 'FOREIGN KEY' THEN 2
                WHEN 'UNIQUE' THEN 3
                ELSE 4
            END
    ) as constraints
FROM information_schema.columns c
LEFT JOIN column_constraints cc 
    ON c.table_schema = cc.table_schema 
    AND c.table_name = cc.table_name 
    AND c.column_name = cc.column_name
WHERE c.table_schema = 'public'
  AND c.table_name IN ('users', 'orders', 'order_items')
GROUP BY c.table_name, c.column_name, c.data_type, c.ordinal_position
ORDER BY c.table_name, c.ordinal_position;

CHECK制約の確認

-- CHECK制約を含むカラム情報
SELECT 
    c.table_name,
    c.column_name,
    c.data_type,
    cc.check_clause
FROM information_schema.columns c
LEFT JOIN information_schema.check_constraints cc
    ON c.table_schema = cc.constraint_schema
    AND EXISTS (
        SELECT 1 
        FROM information_schema.constraint_column_usage ccu
        WHERE ccu.constraint_name = cc.constraint_name
          AND ccu.table_name = c.table_name
          AND ccu.column_name = c.column_name
    )
WHERE c.table_schema = 'public'
  AND cc.check_clause IS NOT NULL
ORDER BY c.table_name, c.ordinal_position;

インデックス情報の確認

-- カラムに設定されたインデックス
SELECT 
    t.tablename,
    i.indexname,
    i.indexdef,
    ARRAY_AGG(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns
FROM pg_indexes i
JOIN pg_class c ON c.relname = i.indexname
JOIN pg_index ix ON ix.indexrelid = c.oid
JOIN pg_class t2 ON t2.oid = ix.indrelid
JOIN pg_attribute a ON a.attrelid = t2.oid AND a.attnum = ANY(ix.indkey)
JOIN pg_tables t ON t.tablename = t2.relname
WHERE t.schemaname = 'public'
GROUP BY t.tablename, i.indexname, i.indexdef
ORDER BY t.tablename, i.indexname;

💡 実践的な活用例

テーブル定義書の自動生成

Markdown形式での出力

-- Markdownテーブルとして出力
CREATE OR REPLACE FUNCTION generate_table_documentation(p_table_name TEXT)
RETURNS TABLE(documentation TEXT) AS $$
BEGIN
    -- ヘッダー
    RETURN QUERY SELECT '## Table: ' || p_table_name;
    RETURN QUERY SELECT '';
    RETURN QUERY SELECT '| Column | Type | Nullable | Default | Description |';
    RETURN QUERY SELECT '|--------|------|----------|---------|-------------|';
    
    -- カラム情報
    RETURN QUERY
    SELECT 
        '| ' || 
        a.attname || ' | ' ||
        pg_catalog.format_type(a.atttypid, a.atttypmod) || ' | ' ||
        CASE WHEN a.attnotnull THEN 'NOT NULL' ELSE 'YES' END || ' | ' ||
        COALESCE(pg_get_expr(d.adbin, d.adrelid), '-') || ' | ' ||
        COALESCE(col_description(a.attrelid, a.attnum), '-') || ' |'
    FROM pg_catalog.pg_attribute a
    LEFT JOIN pg_catalog.pg_attrdef d ON (a.attrelid, a.attnum) = (d.adrelid, d.adnum)
    WHERE a.attrelid = p_table_name::regclass
      AND a.attnum > 0 
      AND NOT a.attisdropped
    ORDER BY a.attnum;
    
    -- インデックス情報
    RETURN QUERY SELECT '';
    RETURN QUERY SELECT '### Indexes';
    RETURN QUERY
    SELECT '- ' || indexname || ': ' || indexdef
    FROM pg_indexes
    WHERE tablename = p_table_name
      AND schemaname = 'public';
    
END;
$$ LANGUAGE plpgsql;

-- 使用例
SELECT * FROM generate_table_documentation('users');

データ型の統計

プロジェクト全体のデータ型使用状況

-- データ型の使用頻度を分析
WITH data_type_stats AS (
    SELECT 
        data_type,
        COUNT(*) as usage_count,
        COUNT(DISTINCT table_name) as table_count,
        STRING_AGG(DISTINCT table_name, ', ' ORDER BY table_name) as used_in_tables
    FROM information_schema.columns
    WHERE table_schema = 'public'
    GROUP BY data_type
)
SELECT 
    data_type,
    usage_count,
    table_count,
    ROUND(usage_count::numeric / SUM(usage_count) OVER() * 100, 2) as percentage,
    CASE 
        WHEN LENGTH(used_in_tables) > 50 
        THEN SUBSTRING(used_in_tables, 1, 47) || '...'
        ELSE used_in_tables
    END as sample_tables
FROM data_type_stats
ORDER BY usage_count DESC;

カラム名の命名規則チェック

-- 命名規則に違反するカラムを検出
WITH naming_check AS (
    SELECT 
        table_name,
        column_name,
        CASE
            WHEN column_name !~ '^[a-z][a-z0-9_]*$' THEN 'Invalid format'
            WHEN column_name ~ '^[0-9]' THEN 'Starts with number'
            WHEN column_name ~ '[A-Z]' THEN 'Contains uppercase'
            WHEN column_name ~ '__' THEN 'Double underscore'
            WHEN LENGTH(column_name) > 30 THEN 'Too long (>30 chars)'
            ELSE 'OK'
        END as issue
    FROM information_schema.columns
    WHERE table_schema = 'public'
)
SELECT 
    table_name,
    column_name,
    issue,
    'ALTER TABLE ' || table_name || ' RENAME COLUMN ' || 
    column_name || ' TO ' || LOWER(REPLACE(column_name, '__', '_')) || ';' as fix_sql
FROM naming_check
WHERE issue != 'OK'
ORDER BY table_name, column_name;

🚀 高度な分析クエリ

カラムの重複検出

同じ名前・型のカラムを持つテーブル

-- 同じカラム名と型を持つテーブルを検出(正規化の候補)
WITH column_patterns AS (
    SELECT 
        column_name,
        data_type,
        COUNT(DISTINCT table_name) as table_count,
        STRING_AGG(table_name, ', ' ORDER BY table_name) as tables
    FROM information_schema.columns
    WHERE table_schema = 'public'
      AND column_name NOT IN ('id', 'created_at', 'updated_at')  -- 共通カラムを除外
    GROUP BY column_name, data_type
    HAVING COUNT(DISTINCT table_name) > 1
)
SELECT 
    column_name,
    data_type,
    table_count,
    tables,
    'Candidate for normalization' as recommendation
FROM column_patterns
ORDER BY table_count DESC, column_name;

NULL値の多いカラム検出

-- NULL値が多いカラムを検出(設計見直しの候補)
CREATE OR REPLACE FUNCTION analyze_null_columns()
RETURNS TABLE(
    table_name TEXT,
    column_name TEXT,
    total_rows BIGINT,
    null_count BIGINT,
    null_percentage NUMERIC
) AS $$
DECLARE
    v_record RECORD;
    v_query TEXT;
    v_total BIGINT;
    v_nulls BIGINT;
BEGIN
    FOR v_record IN 
        SELECT 
            c.table_name AS tbl,
            c.column_name AS col
        FROM information_schema.columns c
        WHERE c.table_schema = 'public'
          AND c.is_nullable = 'YES'
    LOOP
        v_query := format(
            'SELECT COUNT(*), COUNT(*) - COUNT(%I) FROM %I',
            v_record.col, v_record.tbl
        );
        
        EXECUTE v_query INTO v_total, v_nulls;
        
        IF v_total > 0 THEN
            RETURN QUERY SELECT 
                v_record.tbl::TEXT,
                v_record.col::TEXT,
                v_total,
                v_nulls,
                ROUND((v_nulls::NUMERIC / v_total) * 100, 2);
        END IF;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 使用例(NULL率が50%以上のカラムを表示)
SELECT * FROM analyze_null_columns()
WHERE null_percentage >= 50
ORDER BY null_percentage DESC;

カラムサイズの分析

-- 各カラムの実際のデータサイズを分析
CREATE OR REPLACE FUNCTION analyze_column_sizes(p_table_name TEXT)
RETURNS TABLE(
    column_name TEXT,
    data_type TEXT,
    avg_size NUMERIC,
    max_size NUMERIC,
    total_size TEXT
) AS $$
DECLARE
    v_column RECORD;
    v_query TEXT;
BEGIN
    FOR v_column IN 
        SELECT 
            a.attname,
            pg_catalog.format_type(a.atttypid, a.atttypmod) as dtype
        FROM pg_catalog.pg_attribute a
        WHERE a.attrelid = p_table_name::regclass
          AND a.attnum > 0 
          AND NOT a.attisdropped
    LOOP
        v_query := format(
            'SELECT 
                %L::TEXT,
                %L::TEXT,
                ROUND(AVG(pg_column_size(%I))::NUMERIC, 2),
                MAX(pg_column_size(%I))::NUMERIC,
                pg_size_pretty(SUM(pg_column_size(%I))::BIGINT)
            FROM %I',
            v_column.attname,
            v_column.dtype,
            v_column.attname,
            v_column.attname,
            v_column.attname,
            p_table_name
        );
        
        RETURN QUERY EXECUTE v_query;
    END LOOP;
END;
$$ LANGUAGE plpgsql;

-- 使用例
SELECT * FROM analyze_column_sizes('users');

📝 カラム変更履歴の追跡

DDLログの活用

-- カラム変更を追跡するトリガー
CREATE TABLE column_change_log (
    id SERIAL PRIMARY KEY,
    table_name TEXT,
    column_name TEXT,
    change_type TEXT,
    old_definition TEXT,
    new_definition TEXT,
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    changed_by TEXT DEFAULT CURRENT_USER
);

-- イベントトリガーで変更を記録
CREATE OR REPLACE FUNCTION log_column_changes()
RETURNS event_trigger AS $$
DECLARE
    v_obj RECORD;
BEGIN
    FOR v_obj IN SELECT * FROM pg_event_trigger_ddl_commands()
    LOOP
        INSERT INTO column_change_log (table_name, change_type, new_definition)
        VALUES (
            v_obj.object_identity,
            v_obj.command_tag,
            v_obj.object_identity
        );
    END LOOP;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER track_column_changes
ON ddl_command_end
WHEN TAG IN ('ALTER TABLE', 'CREATE TABLE', 'DROP TABLE')
EXECUTE FUNCTION log_column_changes();

🔄 他システムとの連携

JSON形式でのエクスポート

-- テーブル構造をJSON形式で出力
SELECT jsonb_pretty(
    jsonb_build_object(
        'table_name', table_name,
        'columns', jsonb_agg(
            jsonb_build_object(
                'name', column_name,
                'type', data_type,
                'nullable', is_nullable,
                'default', column_default,
                'position', ordinal_position
            ) ORDER BY ordinal_position
        )
    )
) as table_structure
FROM information_schema.columns
WHERE table_schema = 'public'
  AND table_name = 'users'
GROUP BY table_name;

CSV形式でのエクスポート

-- カラム情報をCSVファイルに出力
COPY (
    SELECT 
        table_name,
        column_name,
        data_type,
        is_nullable,
        column_default
    FROM information_schema.columns
    WHERE table_schema = 'public'
    ORDER BY table_name, ordinal_position
) TO '/tmp/column_list.csv' WITH CSV HEADER;

🛠️ 便利な関数とビュー

カスタムビューの作成

-- よく使うカラム情報をビューとして定義
CREATE OR REPLACE VIEW v_column_details AS
SELECT 
    c.table_name,
    c.column_name,
    c.data_type,
    CASE 
        WHEN c.character_maximum_length IS NOT NULL 
        THEN c.data_type || '(' || c.character_maximum_length || ')'
        WHEN c.numeric_precision IS NOT NULL 
        THEN c.data_type || '(' || c.numeric_precision || ',' || c.numeric_scale || ')'
        ELSE c.data_type
    END as full_type,
    c.is_nullable,
    c.column_default,
    pgd.description as comment,
    COALESCE(
        (SELECT STRING_AGG(tc.constraint_type, ', ')
         FROM information_schema.key_column_usage kcu
         JOIN information_schema.table_constraints tc
         ON kcu.constraint_name = tc.constraint_name
         WHERE kcu.table_name = c.table_name 
           AND kcu.column_name = c.column_name),
        ''
    ) as constraints
FROM information_schema.columns c
LEFT JOIN pg_catalog.pg_description pgd 
    ON pgd.objoid = (c.table_schema || '.' || c.table_name)::regclass
    AND pgd.objsubid = c.ordinal_position
WHERE c.table_schema = 'public'
ORDER BY c.table_name, c.ordinal_position;

-- 使用例
SELECT * FROM v_column_details WHERE table_name = 'users';

検索機能付きカラム検索

-- カラム名で検索する関数
CREATE OR REPLACE FUNCTION search_columns(p_search_term TEXT)
RETURNS TABLE(
    table_name TEXT,
    column_name TEXT,
    data_type TEXT,
    column_comment TEXT
) AS $$
BEGIN
    RETURN QUERY
    SELECT 
        c.table_name::TEXT,
        c.column_name::TEXT,
        c.data_type::TEXT,
        COALESCE(pgd.description, '')::TEXT
    FROM information_schema.columns c
    LEFT JOIN pg_catalog.pg_description pgd 
        ON pgd.objoid = (c.table_schema || '.' || c.table_name)::regclass
        AND pgd.objsubid = c.ordinal_position
    WHERE c.table_schema = 'public'
      AND (
          c.column_name ILIKE '%' || p_search_term || '%'
          OR COALESCE(pgd.description, '') ILIKE '%' || p_search_term || '%'
      )
    ORDER BY 
        CASE WHEN c.column_name = p_search_term THEN 1
             WHEN c.column_name ILIKE p_search_term || '%' THEN 2
             ELSE 3
        END,
        c.table_name, c.column_name;
END;
$$ LANGUAGE plpgsql;

-- 使用例
SELECT * FROM search_columns('email');

💡 ベストプラクティス

カラム情報管理のコツ

-- 1. コメントを必ず付ける
COMMENT ON COLUMN users.email IS 'ユーザーのメールアドレス(ユニーク)';

-- 2. 定期的な構造チェック
CREATE OR REPLACE FUNCTION check_table_health()
RETURNS TABLE(
    issue_type TEXT,
    table_name TEXT,
    details TEXT
) AS $$
BEGIN
    -- コメントなしカラム
    RETURN QUERY
    SELECT 
        'Missing comment'::TEXT,
        c.table_name::TEXT,
        'Column: ' || c.column_name
    FROM information_schema.columns c
    LEFT JOIN pg_catalog.pg_description pgd 
        ON pgd.objoid = (c.table_schema || '.' || c.table_name)::regclass
        AND pgd.objsubid = c.ordinal_position
    WHERE c.table_schema = 'public'
      AND pgd.description IS NULL;
    
    -- インデックスなし外部キー
    RETURN QUERY
    SELECT 
        'Missing index on FK'::TEXT,
        tc.table_name::TEXT,
        'Column: ' || kcu.column_name
    FROM information_schema.table_constraints tc
    JOIN information_schema.key_column_usage kcu
        ON tc.constraint_name = kcu.constraint_name
    WHERE tc.constraint_type = 'FOREIGN KEY'
      AND NOT EXISTS (
          SELECT 1 FROM pg_indexes i
          WHERE i.tablename = tc.table_name
            AND i.indexdef LIKE '%' || kcu.column_name || '%'
      );
END;
$$ LANGUAGE plpgsql;

📚 まとめ:カラム一覧取得を極める

PostgreSQLでカラム一覧を効率的に取得する方法:

psqlの\dコマンドで素早く確認
information_schemaで標準SQL互換の取得
pg_catalogで詳細な情報を取得
カスタム関数で自動ドキュメント生成
定期的な構造チェックで品質維持

これらのテクニックを活用することで、データベース構造の把握と管理が格段に効率化されます。

今すぐ実践すべき3つのアクション:

  1. よく使うカラム確認クエリをビューとして保存
  2. テーブルとカラムにコメントを追加
  3. 自動ドキュメント生成スクリプトを作成

データベース構造を完全に把握して、効率的な開発を実現しましょう!


コメント

タイトルとURLをコピーしました