この記事の要点
• psqlのメタコマンド(\dt, \d)とSQL(CRUD, JOIN, CTE)を網羅
• EXPLAIN ANALYZEで実行計画を確認し、インデックスを最適化
• JSONB・ウィンドウ関数・パーティショニングなどPostgreSQL固有の強力な機能
概要
PostgreSQL は ACID 準拠のオープンソースリレーショナルデータベースです。標準 SQL への高い準拠度、豊富なデータ型(JSONB、配列、範囲型、地理空間型)、強力な拡張機能(PostGIS、pg_trgm など)、堅牢な MVCC によるトランザクション制御を特徴とし、Web アプリケーションから分析基盤まで幅広く利用されています。本チートシートは現行系列(PostgreSQL 16/17)を対象に、日常的に使う構文とコマンドを整理します。
psql 接続
| コマンド | 説明 |
|---|
psql -h localhost -U postgres -d mydb | ホスト・ユーザー・DB名を指定して接続 |
psql "postgresql://user:pass@localhost:5432/mydb" | 接続文字列で接続 |
psql -f schema.sql mydb | SQLファイルを実行 |
| オプション | 説明 |
|---|
-h <host> | 接続先ホスト |
-p <port> | ポート(既定 5432) |
-U <user> | 接続ユーザー |
-d <db> | データベース名 |
-W | パスワードを毎回入力 |
-c "SQL" | SQL を実行して終了 |
-f <file> | SQL ファイルを実行 |
-A | 整列せず出力 |
-t | ヘッダ・フッタを表示しない |
-v var=val | psql 変数を設定 |
psql メタコマンド
| コマンド | 説明 |
|---|
\l | データベース一覧 |
\c <db> | データベース切り替え |
\dn | スキーマ一覧 |
\dt | テーブル一覧 |
\dt+ | テーブル一覧(サイズ含む) |
\d <table> | テーブル構造 |
\d+ <table> | テーブル構造(詳細) |
\di | インデックス一覧 |
\dv | ビュー一覧 |
\df | 関数一覧 |
\du | ロール一覧 |
\dp | テーブル権限 |
\timing | クエリ実行時間表示の切替 |
\x | 拡張表示の切替 |
\e | エディタで編集 |
\i <file> | SQL ファイルを実行 |
\copy | クライアント側で COPY |
\q | 終了 |
\? | psql ヘルプ |
\h <SQL> | SQL コマンドのヘルプ |
データ型
| 型 | 説明 |
|---|
smallint / integer / bigint | 整数(2/4/8 バイト) |
numeric(p,s) / decimal | 任意精度数値 |
real / double precision | 浮動小数点 |
serial / bigserial | 自動採番(旧式) |
GENERATED ALWAYS AS IDENTITY | 推奨される自動採番 |
boolean | 真偽値 |
text / varchar(n) / char(n) | 文字列 |
bytea | バイナリ |
date / time / timestamp / timestamptz | 日時 |
interval | 期間 |
uuid | UUID |
json / jsonb | JSON(jsonb はバイナリ+インデックス可) |
array (int[], text[]) | 配列 |
inet / cidr / macaddr | ネットワーク |
tsvector / tsquery | 全文検索 |
int4range / tstzrange | 範囲型 |
ポイント: \d テーブル名でテーブル構造を即座に確認。\timingを有効にするとクエリの実行時間が常に表示され、パフォーマンス意識が高まります。
DDL(テーブル定義)
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL UNIQUE,
name text NOT NULL,
age integer CHECK (age >= 0),
is_active boolean NOT NULL DEFAULT true,
metadata jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_users_email ON users (email);
CREATE INDEX idx_users_metadata ON users USING gin (metadata);
変更系
ALTER TABLE users ADD COLUMN phone text;
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users ALTER COLUMN name TYPE varchar(100);
ALTER TABLE users ALTER COLUMN is_active SET DEFAULT false;
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE users RENAME TO accounts;
DROP TABLE IF EXISTS users CASCADE;
TRUNCATE TABLE users RESTART IDENTITY CASCADE;
外部キー
CREATE TABLE posts (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title text NOT NULL,
body text
);
実践メモ: 主キーはbigint GENERATED ALWAYS AS IDENTITYを使いましょう。serialは旧式で、権限管理やシーケンス所有の問題があります。
DML(CRUD)
INSERT INTO users (email, name) VALUES ('a@example.com', 'Alice');
INSERT INTO users (email, name) VALUES
('b@example.com', 'Bob'),
('c@example.com', 'Carol')
RETURNING id, created_at;
SELECT id, name FROM users WHERE is_active = true ORDER BY created_at DESC LIMIT 10;
UPDATE users SET name = 'Alicia', updated_at = now() WHERE id = 1;
DELETE FROM users WHERE is_active = false;
INSERT INTO users (email, name) VALUES ('a@example.com', 'Alice')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name;
JOIN
SELECT u.id, u.name, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
GROUP BY u.id
ORDER BY post_count DESC;
| JOIN | 説明 |
|---|
INNER JOIN | 両側にある行のみ |
LEFT JOIN | 左側を全て返す |
RIGHT JOIN | 右側を全て返す |
FULL JOIN | 両側を全て返す |
CROSS JOIN | デカルト積 |
LATERAL JOIN | 行ごとにサブクエリ |
集約・ウィンドウ関数
SELECT
user_id,
COUNT(*) AS posts,
AVG(LENGTH(body)) AS avg_len
FROM posts
GROUP BY user_id
HAVING COUNT(*) > 5;
SELECT
id,
user_id,
created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn,
RANK() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rnk,
LAG(title) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_title
FROM posts;
CTE(共通テーブル式)
WITH recent AS (
SELECT * FROM posts WHERE created_at > now() - interval '7 days'
)
SELECT user_id, COUNT(*) FROM recent GROUP BY user_id;
WITH RECURSIVE tree AS (
SELECT id, parent_id, name, 1 AS depth FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, c.name, t.depth + 1
FROM categories c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree;
注意: UPSERT(ON CONFLICT ... DO UPDATE)で使うEXCLUDEDは、挿入しようとした値を参照するキーワードです。既存行の値は通常のテーブル名で参照します。
JSONB 操作
| 演算子 | 説明 |
|---|
-> | キーで JSON 取得 |
->> | キーでテキスト取得 |
#> | パスで JSON 取得 |
#>> | パスでテキスト取得 |
@> | 包含 |
? | キーが存在 |
| `? | /?&` |
| ` | |
- | キー削除 |
SELECT metadata->>'role' AS role FROM users WHERE metadata @> '{"plan":"pro"}';
UPDATE users SET metadata = metadata || '{"verified":true}' WHERE id = 1;
CREATE INDEX idx_users_meta ON users USING gin (metadata jsonb_path_ops);
ポイント: JSONはjsonb型を使いましょう。json型はテキスト保存で毎回パースが必要ですが、jsonbはバイナリ保存でインデックスも張れます。
インデックス
CREATE INDEX idx_posts_user_created ON posts (user_id, created_at DESC);
CREATE UNIQUE INDEX idx_users_email_lower ON users (lower(email));
CREATE INDEX idx_posts_title_trgm ON posts USING gin (title gin_trgm_ops);
CREATE INDEX idx_posts_active ON posts (created_at) WHERE deleted_at IS NULL;
| インデックス種別 | 用途 |
|---|
BTREE(既定) | 等価 / 範囲検索 |
HASH | 等価検索専用 |
GIN | 配列 / JSONB / 全文検索 |
GIST | 範囲 / 地理空間 / 類似検索 |
BRIN | 連続値の巨大テーブル |
SP-GIST | 空間分割 |
注意: CREATE INDEXはテーブルをロックします。本番環境ではCREATE INDEX CONCURRENTLYを使って、書き込みをブロックせずにインデックスを作成しましょう。
実践メモ: 部分インデックス(WHERE deleted_at IS NULL)を使えば、不要な行を除外してインデックスサイズと検索性能を改善できます。
トランザクション
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
ROLLBACK;
BEGIN;
SAVEPOINT s1;
ROLLBACK TO s1;
COMMIT;
| 分離レベル | 説明 |
|---|
READ UNCOMMITTED | (PostgreSQL では READ COMMITTED と同等) |
READ COMMITTED | 既定。コミット済みのみ読む |
REPEATABLE READ | スナップショット分離 |
SERIALIZABLE | 直列化可能 |
BEGIN ISOLATION LEVEL SERIALIZABLE;
ロック
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE SKIP LOCKED;
SELECT * FROM accounts WHERE id = 1 FOR SHARE;
LOCK TABLE accounts IN SHARE ROW EXCLUSIVE MODE;
ロール / 権限
CREATE ROLE app_user LOGIN PASSWORD 'secret';
GRANT CONNECT ON DATABASE mydb TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user;
ALTER ROLE app_user WITH PASSWORD 'newsecret';
DROP ROLE app_user;
実用例
1. ページネーション(キーセット)
SELECT id, title, created_at FROM posts
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;
2. UPSERT で集計テーブル更新
INSERT INTO daily_stats (day, count) VALUES (CURRENT_DATE, 1)
ON CONFLICT (day) DO UPDATE SET count = daily_stats.count + 1;
3. JSON 配列を行に展開
SELECT id, tag
FROM posts, LATERAL jsonb_array_elements_text(metadata->'tags') AS tag;
4. 全文検索
ALTER TABLE posts ADD COLUMN tsv tsvector
GENERATED ALWAYS AS (to_tsvector('simple', coalesce(title,'') || ' ' || coalesce(body,''))) STORED;
CREATE INDEX idx_posts_tsv ON posts USING gin(tsv);
SELECT id, title FROM posts WHERE tsv @@ plainto_tsquery('simple', 'postgres tips');
5. ランダムサンプリング
SELECT * FROM users TABLESAMPLE BERNOULLI (1);
6. 重複削除
DELETE FROM users a USING users b
WHERE a.id > b.id AND a.email = b.email;
7. EXPLAIN
EXPLAIN ANALYZE
SELECT * FROM posts WHERE user_id = 42 ORDER BY created_at DESC LIMIT 10;
8. 一時テーブル
CREATE TEMP TABLE staging (id int, payload jsonb) ON COMMIT DROP;
9. COPY による高速ロード
\copy users(email, name) FROM 'users.csv' WITH (FORMAT csv, HEADER true)
10. パーティショニング
CREATE TABLE events (
id bigint, occurred_at timestamptz NOT NULL, payload jsonb
) PARTITION BY RANGE (occurred_at);
CREATE TABLE events_2026_04 PARTITION OF events
FOR VALUES FROM ('2026-04-01') TO ('2026-05-01');
11. マテリアライズドビュー
CREATE MATERIALIZED VIEW mv_user_stats AS
SELECT user_id, COUNT(*) AS posts FROM posts GROUP BY user_id;
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_user_stats;
12. トリガで updated_at を自動更新
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_users_updated
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION set_updated_at();
バックアップ / リストア
| コマンド | 説明 |
|---|
pg_dump -h localhost -U postgres -d mydb -F c -f mydb.dump | カスタム形式でバックアップ |
pg_dump -d mydb --schema-only -f schema.sql | スキーマのみダンプ |
pg_dump -d mydb --data-only -f data.sql | データのみダンプ |
pg_restore -h localhost -U postgres -d mydb mydb.dump | リストア |
psql -d mydb -f schema.sql | SQLファイルからリストア |
pg_dumpall -U postgres -f all.sql | 全クラスタをダンプ |
運用 SQL
| SQL | 説明 |
|---|
SELECT pid, usename, application_name, state, query FROM pg_stat_activity WHERE state <> 'idle'; | 接続中のセッション一覧 |
SELECT pg_cancel_backend(pid); | クエリをキャンセル |
SELECT pg_terminate_backend(pid); | セッションを終了 |
SELECT relname, pg_size_pretty(pg_total_relation_size(relid)) AS size FROM pg_catalog.pg_statio_user_tables ORDER BY pg_total_relation_size(relid) DESC; | テーブルサイズ一覧 |
ANALYZE users; | 統計情報を更新 |
VACUUM (VERBOSE, ANALYZE) users; | VACUUM と統計更新 |
REINDEX TABLE users; | インデックス再構築 |
よく使うオプション早見表
| ツール / オプション | 用途 |
|---|
psql -X | 起動時に .psqlrc を読まない |
psql -1 -f file.sql | 1 トランザクションで実行 |
psql --csv -c "SQL" | CSV 出力 |
pg_dump -F c | カスタム形式でダンプ |
pg_dump -j 4 | 並列ダンプ |
pg_restore -j 4 | 並列リストア |
pg_restore --clean | リストア前に既存オブジェクト削除 |
pg_basebackup -D backup | 物理バックアップ |
トラブルシューティング
| 症状 | 原因と対処 |
|---|
FATAL: password authentication failed | パスワードまたは pg_hba.conf の設定を確認 |
could not connect to server | ホスト・ポート・listen_addresses を確認 |
relation "x" does not exist | スキーマパスや search_path を確認 |
deadlock detected | ロック取得順を統一する。短いトランザクションを心がける |
out of shared memory | max_locks_per_transaction を増やす |
| インデックスが使われない | EXPLAIN ANALYZE で確認、ANALYZE で統計更新 |
disk full | 古い WAL を確認、vacuum full ではなく pg_repack を検討 |
| トランザクション ID 周回警告 | autovacuum を有効化、vacuum freeze を実施 |
Tips & ベストプラクティス
- 主キーは
bigint GENERATED ALWAYS AS IDENTITY を使う。serial は旧式。
- タイムゾーン付き
timestamptz を既定にし、timestamp は避ける。
- JSON は
jsonb を使い、必要に応じて GIN インデックスを張る。
- インデックスは複合キーの順序を意識する。
(user_id, created_at DESC) のように。
EXPLAIN (ANALYZE, BUFFERS) で実行計画とバッファ使用量を確認する。
- 大量更新は
CREATE INDEX CONCURRENTLY でロックを最小化。
- マイグレーションは 1 つのトランザクションで実行し、失敗時は自動ロールバックさせる。
- アプリ側ではプリペアドステートメント(パラメータバインド)を使い SQL インジェクションを防ぐ。
- 接続は PgBouncer などプーラで集約し、同時接続数を制御する。
- 監視には
pg_stat_statements 拡張を有効化してスロークエリを可視化する。
参考リソース
この技術を体系的に学びたいですか?
未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。
メールで無料相談する
← 一覧に戻る