DDL(データ定義言語)

テーブル / インデックス / ビュー / スキーマの作成・変更・削除。 CREATE / ALTER / DROP / TRUNCATE / RENAME を一通り。

CREATE TABLE

CREATE TABLE users (
  id           BIGSERIAL PRIMARY KEY,
  email        TEXT NOT NULL UNIQUE,
  name         TEXT NOT NULL,
  age          INT CHECK (age >= 0),
  is_active    BOOLEAN NOT NULL DEFAULT TRUE,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at   TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

列の修飾子

主キーの書き方

-- BIGSERIAL(Postgres)
id BIGSERIAL PRIMARY KEY,

-- IDENTITY(標準 SQL)
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,

-- UUID
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),

-- 複合主キー
PRIMARY KEY (post_id, user_id)

外部キー

CREATE TABLE posts (
  id BIGSERIAL PRIMARY KEY,
  user_id BIGINT NOT NULL,
  title TEXT NOT NULL,

  -- インラインで宣言
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

-- または列のすぐ後ろ
CREATE TABLE posts2 (
  user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE
);

ON DELETE / ON UPDATE オプション

動作意味
CASCADE親が消えたら子も消す
SET NULL子の FK を NULL に
SET DEFAULT子の FK をデフォルト値に
RESTRICT子があれば親を消せない(即座に拒否)
NO ACTIONRESTRICT に似るがチェックは末尾

制約に名前を付ける(推奨)

CREATE TABLE products (
  id BIGSERIAL,
  sku TEXT NOT NULL,
  price NUMERIC NOT NULL,
  CONSTRAINT products_pkey PRIMARY KEY (id),
  CONSTRAINT products_sku_uniq UNIQUE (sku),
  CONSTRAINT products_price_check CHECK (price >= 0)
);

後から ALTER で操作するときに名前があると楽。

テーブルの ALTER

列追加

ALTER TABLE users ADD COLUMN birth_date DATE;
ALTER TABLE users ADD COLUMN country TEXT NOT NULL DEFAULT 'JP';

列の修正

ALTER TABLE users ALTER COLUMN name SET NOT NULL;
ALTER TABLE users ALTER COLUMN name DROP NOT NULL;
ALTER TABLE users ALTER COLUMN age SET DEFAULT 0;
ALTER TABLE users ALTER COLUMN age DROP DEFAULT;
ALTER TABLE users ALTER COLUMN bio TYPE TEXT USING bio::TEXT;

列の削除 / 改名

ALTER TABLE users DROP COLUMN birth_date;
ALTER TABLE users RENAME COLUMN name TO display_name;
ALTER TABLE users RENAME TO accounts;

制約の追加 / 削除

ALTER TABLE users
  ADD CONSTRAINT users_email_lower_check CHECK (email = LOWER(email));

ALTER TABLE users DROP CONSTRAINT users_email_lower_check;

NOT NULL を後から付ける

既存データに NULL があると失敗。3 段階で安全に:

-- 1. NULL を埋める
UPDATE users SET country = 'JP' WHERE country IS NULL;

-- 2. CHECK で違反検証(即時)
ALTER TABLE users ADD CONSTRAINT temp_check CHECK (country IS NOT NULL) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT temp_check;

-- 3. 本物の NOT NULL に変える
ALTER TABLE users ALTER COLUMN country SET NOT NULL;
ALTER TABLE users DROP CONSTRAINT temp_check;

インデックスの DDL

CREATE INDEX users_name_idx ON users (name);
CREATE UNIQUE INDEX users_email_uniq ON users (email);
CREATE INDEX users_email_lower ON users (LOWER(email));
CREATE INDEX posts_published_idx ON posts (created_at DESC) WHERE status = 'published';

DROP INDEX users_name_idx;
ALTER INDEX users_name_idx RENAME TO users_name_old;

本番では CREATE INDEX CONCURRENTLY でロックを避ける。詳細は インデックス

ビュー(VIEW)

SELECT 文に名前を付けたもの。テーブルのように使える:

CREATE VIEW active_users AS
SELECT id, email, name
FROM users
WHERE is_active = true;

SELECT * FROM active_users WHERE name LIKE 'A%';

DROP VIEW active_users;
CREATE OR REPLACE VIEW active_users AS ...;

マテリアライズドビュー

CREATE MATERIALIZED VIEW user_stats AS
SELECT user_id, count(*) AS post_count
FROM posts GROUP BY user_id;

CREATE UNIQUE INDEX user_stats_user_idx ON user_stats (user_id);

REFRESH MATERIALIZED VIEW CONCURRENTLY user_stats;

スキーマ(名前空間)

CREATE SCHEMA app;
CREATE TABLE app.users (...);
SELECT * FROM app.users;

-- 検索パス(自動補完される)
SET search_path TO app, public;

DROP SCHEMA app CASCADE;

ENUM 型(Postgres)

CREATE TYPE post_status AS ENUM ('draft', 'published', 'archived');

CREATE TABLE posts (
  ...
  status post_status NOT NULL DEFAULT 'draft'
);

-- 値追加
ALTER TYPE post_status ADD VALUE 'pending' BEFORE 'published';

値の削除や順序変更は不可。柔軟性が要るなら CHECK + TEXT or ルックアップテーブル。

シーケンス

BIGSERIAL の裏で動いている自動採番。手動でも作れる:

CREATE SEQUENCE order_no_seq START 1000 INCREMENT 1;

INSERT INTO orders (order_no, ...) VALUES (nextval('order_no_seq'), ...);

SELECT currval('order_no_seq');
SELECT setval('order_no_seq', 1500);

計算列(GENERATED)

CREATE TABLE products (
  id BIGSERIAL PRIMARY KEY,
  price NUMERIC NOT NULL,
  tax_rate NUMERIC NOT NULL DEFAULT 0.1,
  total NUMERIC GENERATED ALWAYS AS (price * (1 + tax_rate)) STORED
);

-- INSERT で total は指定しない

テーブルの一時的な作成

-- セッション内だけ
CREATE TEMP TABLE temp_results AS
SELECT user_id, count(*) FROM posts GROUP BY user_id;

-- WITH(CTE)でも近いことが可能

テーブルのコピー

-- 構造 + データ
CREATE TABLE posts_backup AS SELECT * FROM posts;

-- 構造だけ
CREATE TABLE posts_empty (LIKE posts INCLUDING ALL);

-- データだけ追加
INSERT INTO posts_empty SELECT * FROM posts;

DROP / TRUNCATE / DELETE

コマンド挙動
DELETE FROM t行削除(FK チェック / トリガー / WAL あり、遅い)
TRUNCATE t全行を超高速削除(DDL 扱い)
DROP TABLE tテーブル自体を削除
DROP TABLE IF EXISTS users CASCADE;
TRUNCATE TABLE logs RESTART IDENTITY CASCADE;
DELETE FROM users WHERE created_at < '2020-01-01';

RENAME

ALTER TABLE users RENAME TO accounts;
ALTER TABLE accounts RENAME COLUMN name TO display_name;
ALTER INDEX users_pkey RENAME TO accounts_pkey;

パーティショニング

CREATE TABLE events (
  id BIGSERIAL,
  created_at TIMESTAMPTZ NOT NULL,
  data JSONB
) PARTITION BY RANGE (created_at);

CREATE TABLE events_2026_01 PARTITION OF events
  FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');

CREATE TABLE events_2026_02 PARTITION OF events
  FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');

関数 / トリガー

CREATE OR REPLACE FUNCTION set_updated_at()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER posts_set_updated_at
  BEFORE UPDATE ON posts
  FOR EACH ROW EXECUTE FUNCTION set_updated_at();

EXTENSION

CREATE EXTENSION IF NOT EXISTS pgcrypto;        -- gen_random_uuid()
CREATE EXTENSION IF NOT EXISTS citext;           -- 大文字小文字無視
CREATE EXTENSION IF NOT EXISTS pg_trgm;          -- LIKE 高速化
CREATE EXTENSION IF NOT EXISTS vector;           -- pgvector
CREATE EXTENSION IF NOT EXISTS postgis;          -- 地理空間

権限(GRANT / REVOKE)

GRANT SELECT, INSERT, UPDATE ON users TO app_user;
GRANT ALL ON SCHEMA public TO admin_user;
GRANT EXECUTE ON FUNCTION set_updated_at() TO PUBLIC;

REVOKE INSERT ON users FROM app_user;

失敗パターン

症状対処
ALTER で全テーブルロックCONCURRENTLY 系 / 段階的に
NOT NULL 追加で停止DEFAULT 付き or 段階的に
ENUM が固いCHECK + TEXT に切替検討
制約名がランダムCONSTRAINT 名を明示
DROP CASCADE で連鎖PRINT で確認 / ステージで実験
DDL は履歴化

DDL はマイグレーションファイルとして残す(→ Prisma migrate / Supabase migrate)。 GUI で直接編集する習慣はチームを壊す。