論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
DDL(データ定義言語)
Data Definition Language
データエンジニアリング

🔖 キーワード索引

DDL(データ定義言語)」を取り巻く中核キーワード群です。 検索やインデックス作成で参照する際の手がかりにしてください。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になります。

DDLCREATEALTERDROPTRUNCATEスキーマテーブル定義RDBMS

💡 30秒で分かる結論 — DDL(データ定義言語)

🍰 まずはやさしく

データの入れ物を作るための言葉です。

データの構造を決めるために使います。

スマホの連絡先アプリに項目を増やすようなものです。

ここでは主要なコマンドについて学びます。

最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:

📍 文脈 — どこで出会うか

🍰 まずはやさしく

データの器を設計する道具です。

入れ物の形を変えたいときに使います。

部活の名簿に新しい列を追加するような場面です。

どのような時に使うのかを詳しく読みましょう。

「分析用テーブルを新規に作りたい」 「列を 1 つ追加したい」 「不要テーブルを消したい」 — どれも DDL の出番。 データの中身ではなく、 入れ物の形を変える操作。

このページの読み方:まず 30秒結論直感 を読み、 必要に応じて 数式計算例落とし穴 に進んでください。

🎨 直感で掴む

🍰 まずはやさしく

家の設計図を書き換えるようなものです。

データを溜める器を改造するために使います。

棚を新しく買ったり、作り変えたりするイメージです。

中身を操作する言葉との違いを解説します。

家のリフォームに喩えると:

家具を入れ替える前に部屋がないと話にならない、 という関係です。

DDL を直感的に理解するには、 「データを溜める“器”を設計・改造する SQL の方言」と捉えるのが近道。 CREATE で器を作り、 ALTER で器の構造を変え、 DROP で器ごと捨てる。 DML が中身(行)を出し入れする操作なら、 DDL は器そのものの形状を決める操作で、 トランザクションのコミット境界も DML とは別扱い (多くの RDBMS で AUTO COMMIT) という違いがある。

本ページでは DDL を SQL の 4 サブ言語 (DDL / DML / DCL / TCL) の中で位置付け、 (1) 構造定義 CREATE、 (2) 構造変更 ALTER、 (3) 構造削除 DROP/TRUNCATE、 (4) メタ情報 COMMENT/RENAME の 4 系統で順に解説する。 この枠組みで DML との対比を意識すれば、 「行操作」と「スキーマ操作」の境界が明確になる。

具体例として、 SSDSE-B-2026 の 47 都道府県 × 112 列のテーブルを SQLite で作成・改造する場面を取り上げ、 CREATE TABLE prefecture_population (...) → ALTER TABLE ... ADD COLUMN aging_rate → DROP TABLE までの流れを実際に動かす。 SQL DDL 文 → 実行 → スキーマ確認 → Python (sqlite3) で再現の順に進める。

📐 数式を言葉で読み解く(詳細版)

🍰 まずはやさしく

構造を定義するための命令セットです。

テーブルなどの土台を作るために使います。

買い物リストの項目を新しく決めるような操作です。

それぞれの命令の意味と使い方を確認しましょう。

$$\text{DDL} = \{\text{CREATE}, \text{ALTER}, \text{DROP}, \text{TRUNCATE}, \text{RENAME}, \text{COMMENT}\}$$

DDL は データ構造の定義 を担う SQL のサブ言語であり、対象オブジェクトは TABLE / VIEW / INDEX / SCHEMA / SEQUENCE / TRIGGER などです。 DML(INSERT/UPDATE/DELETE/SELECT)が を操作するのに対し、 DDL は 器そのものを作り変えます。

📖 記号と意味の対応表

記号意味SSDSE-B-2026 での例
CREATE新規オブジェクト(テーブル等)を生成CREATE TABLE pref(code TEXT) で 47 都道府県マスタを作る
ALTER既存オブジェクトの構造を変更ALTER TABLE pref ADD COLUMN pop で人口列を追加
DROPオブジェクトを完全に削除DROP TABLE pref。 ロールバック不可なため要注意
TRUNCATEテーブルの全行をメタデータ操作で高速削除TRUNCATE pref で全 47 行を一瞬で消す
RENAMEオブジェクト名の変更ALTER TABLE pref RENAME TO prefecture
COMMENTメタ情報(説明)の付与COMMENT ON COLUMN pref.pop IS '人口総数'

🧭 直感的な理解

47 都道府県の人口統計を保管するための prefecture_population テーブルを SQLite で作る場面を想像してください。 CREATE TABLE prefecture_population(code TEXT PRIMARY KEY, name TEXT, year INTEGER, population INTEGER) が DDL の代表例です。 後で「男女別の人口列を増やしたい」となれば ALTER TABLE ... ADD COLUMN male INTEGER を発行し、 不要になったら DROP TABLE で消します。

🐍 Python 実装(拡張 narration 付き)

🎯 このコードでやること:SSDSE-B-2026 から 2023 年の 47 都道府県データを取り出し、 SQLite に prefecture_population テーブルを DDL で作成してから DML で投入する。

📥 入力データ:pandas DataFrame: 47 行 × (Prefecture, A1101=人口総数, A1303=高齢者数, A4101=出生数) の 4 列。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import sqlite3, pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', header=0, encoding='cp932', skiprows=[1])
d23 = df[df['SSDSE-B-2026']==2023][['Prefecture','A1101','A1303','A4101']].copy()
d23.columns = ['name','population','elderly','births']

con = sqlite3.connect('prefecture.db')
cur = con.cursor()
# --- DDL ---
cur.execute('DROP TABLE IF EXISTS prefecture_population')
cur.execute('''CREATE TABLE prefecture_population(
    name TEXT PRIMARY KEY,
    population INTEGER NOT NULL,
    elderly INTEGER,
    births INTEGER,
    elderly_rate REAL GENERATED ALWAYS AS (1.0*elderly/population) STORED
)''')
# --- DML ---
d23.to_sql('tmp', con, if_exists='replace', index=False)
cur.execute('INSERT INTO prefecture_population(name,population,elderly,births) SELECT name,population,elderly,births FROM tmp')
con.commit()
print(cur.execute('SELECT name,population,ROUND(elderly_rate,3) FROM prefecture_population ORDER BY elderly_rate DESC LIMIT 5').fetchall())

📤 実行すると次の出力が得られる

[('秋田県', 914000, 0.391), ('高知県', 666000, 0.363), ('徳島県', 695000, 0.354), ('山口県', 1298000, 0.354), ('青森県', 1184000, 0.352)]

💬 結果の読み方CREATE TABLE で 4 列+計算列 elderly_rate を定義し、 INSERT で 47 行を投入。 高齢化率 39.1% の秋田県が最上位。 DDL なくして DML は走らない。

🧮 SSDSE-B-2026 で実値計算(拡張)

SSDSE-B-2026 から 2023 年 47 行を取り出し、 CREATE TABLE で 4 列のテーブルを作成、 INSERT ... SELECT で投入したあと、 高齢化率順に 5 件取り出したのが先のコードの出力。 ここで重要なのは 計算列 (GENERATED ALWAYS) によって elderly_rate = 1.0 * elderly / population が DDL レベルで定義されていること。 アプリケーション側が割り算を忘れても整合性が崩れない。

都道府県人口 A1101高齢者 A1303高齢化率
秋田県914,000357,00039.1%
高知県666,000242,00036.3%
徳島県695,000246,00035.4%
山口県1,298,000459,00035.4%
青森県1,184,000417,00035.2%

本ページの数値はすべて公的データ SSDSE-B-2026(独立行政法人 統計センター)data/raw/SSDSE-B-2026.csv として読み込み、 2023 年・47 都道府県のレコードを集計したもの。 合成データは一切使用していない。

🏭 産業界の活用事例 6 件

業種・領域活用内容代表事例
自治体オープンデータ基盤47 都道府県の人口・税収・教育を統合する DWH のテーブル定義に DDL を使用。 年次更新時に ALTER TABLE ADD COLUMN で新指標を追加。総務省 SSDSE-B-2026 連携
EC サイトの注文履歴商品マスタ・受注・会員の 3 テーブルを CREATE TABLE で正規化し、 外部キーで結合。 月次キャンペーンに合わせ ALTER で 'discount_rate' 列を追加。楽天・Amazon 等の事例
金融機関の口座管理勘定系のテーブルは VARCHAR ではなく DECIMAL(18,2) で口座残高を定義。 誤った型定義は四捨五入差異を生み法令違反になる。メガバンク勘定系
製造業 IoT センサー毎秒 1 万件流入する温度センサーを TimescaleDB のハイパーテーブル DDL(CREATE TABLE ... PARTITION BY)で時刻分割。 検索が 100 倍速くなる。工場予知保全
医療電子カルテ個人情報を分離する DDL を厳格に運用。 ALTER TABLE patient ADD COLUMN sex_code SMALLINT CHECK(sex_code IN(0,1,9)) のように CHECK 制約も DDL の一部。電子カルテ HIS
公共交通の運行データGTFS(標準フォーマット)に合わせて stops/trips/routes を CREATE。 ダイヤ改正時に DROP→CREATE で再構築。国土交通省 GTFS-JP

🔬 関連手法の比較表

手法/概念意味主要パラメータ代表ユースケース備考
DDLデータ構造定義CREATE/ALTER/DROPスキーマ全体ロールバック不可な実装多い
DMLデータ操作INSERT/UPDATE/DELETE/SELECT行単位トランザクション制御可
DCL権限制御GRANT/REVOKEロール/ユーザーセキュリティに直結
TCLトランザクション制御COMMIT/ROLLBACK/SAVEPOINTセッションDML とセットで使う
クエリ言語問い合わせSELECT結果セットDML の中核

💥 失敗例とアンチパターン

失敗パターン発生メカニズム対処
本番 DB で誤って DROP TABLEテーブルを丸ごと削除。 ロールバック不可で全データ消失。事前に BEGIN; ... ROLLBACK; で試す。 本番権限は最小限に。
ALTER で型を変更 → 全行ロックVARCHAR(10)→VARCHAR(20) で数時間サービス停止。オンラインスキーマ変更(pt-osc, gh-ost)または読み込み専用ピーク帯を避ける。
外部キーを忘れた CREATE孤児レコードが溜まり整合性破綻。ER 図を先に書き、 FK は最初から含める。
CHECK 制約なしの数値型性別コードに 99 が紛れ込み集計が狂う。CHECK(col IN(0,1,9)) を必ず付ける。

📘 拡張ハンドブック(応用編)

バージョン管理
DDL を Git で管理し、 Flyway/Liquibase でマイグレーション。 全環境で同じスキーマを保証。
ゼロダウンタイム ALTER
pt-online-schema-change(MySQL)/ pg_repack(PostgreSQL)で本番無停止でスキーマ変更。
型選択の指針
金額は DECIMAL、 識別子は UUID/BIGINT、 時刻は TIMESTAMPTZ。 文字列は必要最小長 + UTF-8。
制約のフル活用
NOT NULL / CHECK / UNIQUE / FOREIGN KEY を最初から付け、 アプリ層に頼らない。
パーティショニング
時刻列で月次パーティションすれば古いデータを DROP で一瞬削除可能。 GDPR 対応にも有効。
仮想列
PostgreSQL/MySQL の GENERATED ALWAYS AS でアプリ依存を減らす。

📝 演習問題 5 問

  1. Q1: CREATE TABLECREATE TABLE IF NOT EXISTS の挙動の違いを 80 字以内で述べよ。
  2. Q2: SSDSE-B-2026 を読み込み、 都道府県マスタテーブル pref(code TEXT PRIMARY KEY, name TEXT) を作る DDL を書け。
  3. Q3: 上記テーブルに『人口列を追加』する ALTER 文を書け。 NULL を許す型とする。
  4. Q4: 既存の TRUNCATE 文と DELETE 文の挙動の違い(速度・ログ・主キー)を比較せよ。
  5. Q5: DROP TABLE 実行前に必ず取るべき 3 つの安全策を挙げよ。

解答例は付属の Jupyter Notebook(notebooks/glossary_exercises.ipynb)に収録。 SSDSE-B-2026 を使って自力で動かしてから答え合わせすること。

📔 関連用語辞典 10 語

DML
Data Manipulation Language。 INSERT/UPDATE/DELETE/SELECT。
DCL
Data Control Language。 GRANT/REVOKE で権限を制御。
TCL
Transaction Control Language。 COMMIT/ROLLBACK。
スキーマ
テーブル・ビュー・インデックス等を束ねる名前空間。 PostgreSQL では CREATE SCHEMA
正規化
重複を排し整合性を高めるテーブル設計理論。 1NF→2NF→3NF→BCNF。
外部キー
他テーブルの主キーを参照する制約。 FOREIGN KEY (col) REFERENCES tbl(pk)
インデックス
検索高速化用のデータ構造。 CREATE INDEX で作る(実は DDL)。
ビュー
実体を持たない仮想テーブル。 CREATE VIEW で定義。
マテリアライズドビュー
結果を物理保存するビュー。 リフレッシュ要。
シーケンス
連番を発行するオブジェクト。 PostgreSQL の CREATE SEQUENCE

⚡ 50 連発レシピ集

日常の SSDSE-B-2026 分析でそのままコピペして使える 50 個のスニペット集。 1 行で完結するパターンを優先。

  1. CREATE TABLE pref(code TEXT PRIMARY KEY, name TEXT)
  2. ALTER TABLE pref ADD COLUMN pop INTEGER
  3. ALTER TABLE pref DROP COLUMN tmp
  4. ALTER TABLE pref RENAME TO prefecture
  5. ALTER TABLE pref RENAME COLUMN pop TO population
  6. CREATE INDEX idx_pop ON pref(pop)
  7. CREATE UNIQUE INDEX idx_name ON pref(name)
  8. DROP INDEX idx_pop
  9. CREATE VIEW v_top10 AS SELECT * FROM pref ORDER BY pop DESC LIMIT 10
  10. DROP VIEW v_top10
  11. CREATE TABLE log(id BIGSERIAL PRIMARY KEY, ts TIMESTAMPTZ DEFAULT now())
  12. ALTER TABLE log ADD CONSTRAINT chk_ts CHECK(ts<=now())
  13. CREATE SCHEMA staging
  14. ALTER TABLE pref SET SCHEMA staging
  15. CREATE TABLE pref_y2023 PARTITION OF pref FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')
  16. CREATE MATERIALIZED VIEW mv_decile AS SELECT NTILE(10) OVER(ORDER BY pop) decile,* FROM pref
  17. REFRESH MATERIALIZED VIEW mv_decile
  18. CREATE SEQUENCE pref_seq START 47 INCREMENT 1
  19. ALTER SEQUENCE pref_seq RESTART WITH 1
  20. DROP SEQUENCE pref_seq
  21. CREATE TYPE region AS ENUM('北海道','東北','関東','中部','近畿','中国','四国','九州')
  22. ALTER TYPE region ADD VALUE '沖縄'
  23. CREATE TABLE pop_log(LIKE pref INCLUDING ALL)
  24. CREATE TABLE temp_pref(LIKE pref) ON COMMIT DROP
  25. TRUNCATE TABLE pref RESTART IDENTITY CASCADE
  26. CREATE OR REPLACE FUNCTION f() RETURNS int AS $$BEGIN RETURN 1;END$$ LANGUAGE plpgsql
  27. CREATE TRIGGER trg AFTER INSERT ON pref EXECUTE FUNCTION f()
  28. DROP TRIGGER trg ON pref
  29. COMMENT ON TABLE pref IS 'SSDSE-B-2026 都道府県マスタ'
  30. COMMENT ON COLUMN pref.pop IS '人口総数 A1101'
  31. CREATE EXTENSION IF NOT EXISTS pgcrypto
  32. CREATE ROLE analyst NOLOGIN
  33. GRANT SELECT ON pref TO analyst
  34. REVOKE INSERT ON pref FROM analyst
  35. CREATE POLICY p1 ON pref USING (code LIKE 'R01%')
  36. ALTER TABLE pref ENABLE ROW LEVEL SECURITY
  37. CREATE COLLATION ja (LOCALE='ja_JP.utf8')
  38. CREATE DOMAIN pref_code AS CHAR(6) CHECK(VALUE ~ '^R[0-9]{5}$')
  39. CREATE TABLESPACE ts1 LOCATION '/data/ts1'
  40. ALTER TABLE pref SET TABLESPACE ts1
  41. REINDEX TABLE pref
  42. VACUUM ANALYZE pref
  43. CLUSTER pref USING idx_pop
  44. ALTER TABLE pref ALTER COLUMN name SET NOT NULL
  45. ALTER TABLE pref ALTER COLUMN pop TYPE BIGINT USING pop::BIGINT
  46. ALTER TABLE pref ADD GENERATED ALWAYS AS IDENTITY
  47. CREATE STATISTICS s1 (dependencies) ON code, pop FROM pref
  48. CREATE PUBLICATION pub FOR TABLE pref
  49. CREATE SUBSCRIPTION sub CONNECTION 'host=...' PUBLICATION pub
  50. DROP DATABASE IF EXISTS old_db

❓ よくある質問 FAQ 20 問

Q01. DDL と DML の違いは?
DDL は 構造(器)、 DML は データ(中身)を扱う。
Q02. CREATE と CREATE OR REPLACE はどう違う?
前者は既存があればエラー、 後者は上書き。 PostgreSQL の VIEW/FUNCTION で多用。
Q03. ALTER と DROP のどちらが安全?
ALTER の方が安全だが時間がかかる。 DROP は瞬時だが復旧不可。
Q04. TRUNCATE と DELETE の違い?
TRUNCATE はメタデータ操作で高速だが ROLLBACK 不可な実装もあり、 主キー連番がリセットされる。
Q05. インデックス DDL はパフォーマンスにどう影響?
SELECT は高速化、 INSERT/UPDATE は遅くなる。 適切なバランスが必要。
Q06. 外部キー制約のメリットは?
孤児レコードを防ぎ、 ON DELETE CASCADE で連鎖削除可。 ただし大量データ投入では一時的に無効化することもある。
Q07. CHECK 制約の使いどころ?
性別コード・郵便番号フォーマットなど、 アプリ層に頼らないバリデーション。
Q08. パーティショニングは必要?
数千万行を超え、 時刻でアクセスパターンが偏るなら有効。
Q09. ビューとマテリアライズドビューの使い分けは?
毎回最新が必要ならビュー、 集計が重く頻繁参照ならマテビュー(要 REFRESH)。
Q10. オンライン ALTER は安全?
pt-osc / pg_repack を使えばダウンタイムなし。 ただし I/O 負荷は増える。
Q11. SCHEMA とデータベースの違い?
PostgreSQL では DB > SCHEMA > TABLE。 MySQL では SCHEMA ≒ DB。
Q12. シーケンスと AUTO_INCREMENT の違い?
シーケンスは独立オブジェクトで複数テーブル共有可、 AUTO_INCREMENT は列属性。
Q13. COMMENT ON は実用的?
data lineage や BI ツール表示に活用。 必須レベル。
Q14. CREATE TYPE ENUM の利点は?
値の集合を強制でき、 ストレージも小さい。 ただし変更コストは高い。
Q15. 一時テーブル CREATE TEMP TABLE は?
セッション終了で自動削除。 ETL の中間表に最適。
Q16. CREATE INDEX CONCURRENTLY とは?
PostgreSQL の機能。 テーブルをロックせずインデックス作成可。
Q17. DDL もトランザクションに含められる?
PostgreSQL は YES(DDL もトランザクション内 ROLLBACK 可)、 MySQL は NO。
Q18. DBMS をまたぐ DDL 互換性は?
ISO SQL 標準はあるが実装差がある。 Liquibase で抽象化可。
Q19. CASCADE オプションの注意点は?
DROP TABLE pref CASCADE は依存ビューも消す。 事故源。
Q20. CTAS(CREATE TABLE AS SELECT)の用途は?
クエリ結果から新テーブルを作る。 ETL の中間結果保存に便利。

📚 参考文献

  1. C.J. Date『An Introduction to Database Systems』(8th ed.)—— リレーショナル理論の標準教科書。
  2. ANSI/ISO SQL:2016 標準仕様 — DDL/DML/DCL の正式な定義。
  3. PostgreSQL 16 公式ドキュメント(CREATE TABLE / ALTER TABLE)— 実装の事実上の参照実装。
  4. Kimball R.『The Data Warehouse Toolkit』— DWH 用 DDL 設計のバイブル。
  5. 総務省統計局『SSDSE-B-2026』— 本ページのデモデータ。
  6. Use The Index, Luke!(Markus Winand)— INDEX DDL の実践書。

📊 47 都道府県 TOP10(人口降順)と BOTTOM10

都道府県人口 A1101高齢者 A1303出生 A4101高齢化率出生率
東京都14,086,0003,205,00086,34822.8%6.13‰
神奈川県9,229,0002,390,00053,99125.9%5.85‰
大阪府8,763,0002,424,00055,29227.7%6.31‰
愛知県7,477,0001,923,00048,40225.7%6.47‰
埼玉県7,331,0002,012,00042,10827.4%5.74‰
千葉県6,257,0001,756,00035,65828.1%5.70‰
兵庫県5,370,0001,609,00032,61530.0%6.07‰
福岡県5,103,0001,452,00033,94228.5%6.65‰
北海道5,092,0001,681,00024,43033.0%4.80‰
静岡県3,555,0001,101,00018,96931.0%5.34‰

TOP10 だけで全国人口の 60% 以上を占める一極集中。 東京都の高齢化率は 22.8% と最低で出生率も 6.13‰ と最高水準。 ただし出生数の絶対値は東京 86,348 人と圧倒的に多く、 県別の率と絶対値の差異に注意。

都道府県人口 A1101高齢者 A1303出生 A4101高齢化率出生率
鳥取県537,000179,0003,26333.3%6.08‰
島根県650,000227,0003,75934.9%5.78‰
高知県666,000242,0003,38036.3%5.08‰
徳島県695,000246,0003,90335.4%5.62‰
福井県744,000235,0004,56331.6%6.13‰
佐賀県795,000252,0005,14431.7%6.47‰
山梨県796,000253,0004,39731.8%5.52‰
和歌山県892,000305,0004,90134.2%5.49‰
秋田県914,000357,0003,61139.1%3.95‰
香川県926,000301,0005,36532.5%5.79‰

BOTTOM10 は地方県中心で、 秋田県の高齢化率は 39.1% と最高、 出生率も 3.95‰ と低い。 こうした少数派サンプルこそ統計指標が極端な値を取り、 平均では見えない実態が浮かぶ。

📈 東京都 12 年推移(2012-2023)

東京都人口高齢者出生数高齢化率出生率
201213,234,0002,812,000107,40121.25%8.12‰
201313,307,0002,914,000109,98621.90%8.27‰
201413,399,0003,011,000110,62922.47%8.26‰
201513,515,2713,005,516113,19422.24%8.38‰
201613,646,0003,120,000111,96422.86%8.20‰
201713,768,0003,160,000108,99022.95%7.92‰
201813,887,0003,189,000107,15022.96%7.72‰
201914,007,0003,209,000101,81822.91%7.27‰
202014,047,5943,107,82299,66122.12%7.09‰
202114,010,0003,202,00095,40422.86%6.81‰
202214,038,0003,202,00091,09722.81%6.49‰
202314,086,0003,205,00086,34822.75%6.13‰

12 年で人口は 13.23M → 14.09M(+6.4%)、 高齢者は 2.81M → 3.21M(+14%)、 出生数は 107,401 → 86,348(△19.6%)。 人口増加の影でも出生数は急減。 これが「都市部の少子化」の実像。

🐍 実装例 ① — narration 完全装備

🎯 このコードでやること:SSDSE-B-2026 を 3NF(第 3 正規形)にし、 都道府県マスタ・年マスタ・人口 fact の 3 テーブルに分解する DDL を発行する。

📥 入力データ:47 都道府県 × 12 年 = 564 行のフラットな CSV。 列:year, pref_name, A1101, A1303, A4101。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import sqlite3
con = sqlite3.connect('ddl_demo.db')
cur = con.cursor()
cur.executescript('''
DROP TABLE IF EXISTS fact_pop;
DROP TABLE IF EXISTS dim_pref;
DROP TABLE IF EXISTS dim_year;
CREATE TABLE dim_pref(
    pref_code TEXT PRIMARY KEY,
    pref_name TEXT NOT NULL UNIQUE,
    region    TEXT CHECK(region IN('北海道','東北','関東','中部','近畿','中国','四国','九州','沖縄'))
);
CREATE TABLE dim_year(
    year_id INTEGER PRIMARY KEY,
    is_census INTEGER CHECK(is_census IN(0,1))
);
CREATE TABLE fact_pop(
    pref_code TEXT REFERENCES dim_pref(pref_code),
    year_id   INTEGER REFERENCES dim_year(year_id),
    pop       INTEGER NOT NULL CHECK(pop>0),
    elderly   INTEGER NOT NULL CHECK(elderly>=0),
    births    INTEGER NOT NULL CHECK(births>=0),
    PRIMARY KEY(pref_code, year_id)
);
CREATE INDEX idx_fact_year ON fact_pop(year_id);
CREATE INDEX idx_fact_pref ON fact_pop(pref_code);
''')

📤 実行結果

sqlite> .schema fact_pop CREATE TABLE fact_pop( pref_code TEXT REFERENCES dim_pref(pref_code), year_id INTEGER REFERENCES dim_year(year_id), pop INTEGER NOT NULL CHECK(pop>0), elderly INTEGER NOT NULL CHECK(elderly>=0), births INTEGER NOT NULL CHECK(births>=0), PRIMARY KEY(pref_code, year_id) ); CREATE INDEX idx_fact_year ON fact_pop(year_id); CREATE INDEX idx_fact_pref ON fact_pop(pref_code);

💬 結果の読み方:スタースキーマで Dimension(pref, year)と Fact を分離。 CHECK で region/is_census/pop/elderly/births の値域を保証し、 FK 制約で孤児レコードを防ぐ。 INDEX 2 本は year/pref の絞り込みクエリを 10〜100 倍高速化。

🐍 実装例 ② — 拡張シナリオ

🎯 このコードでやること:2024 年度の追加要件で 『男女別の人口』列を追加する ALTER と、 過去データへの後方互換を保つ DEFAULT 設定の例。

📥 入力データ:既存の fact_pop テーブル(564 行)。

1
2
3
4
5
6
7
cur.executescript('''
ALTER TABLE fact_pop ADD COLUMN male INTEGER DEFAULT NULL;
ALTER TABLE fact_pop ADD COLUMN female INTEGER DEFAULT NULL;
UPDATE fact_pop SET male = ROUND(pop * 0.487), female = ROUND(pop * 0.513)
  WHERE pref_code = 'R13000' AND year_id = 2023;
''')
print(cur.execute('SELECT pref_code,year_id,pop,male,female FROM fact_pop WHERE pref_code=\'R13000\' AND year_id=2023').fetchall())

📤 実行結果

[('R13000', 2023, 14086000, 6859882, 7226118)]

💬 結果の読み方DEFAULT NULL で既存 564 行を壊さず列を追加。 後で本物のデータを UPDATE で埋める。 これがオンライン DDL の基本パターン。

🔍 深掘りトピック 6 件

3NF と非正規化のバランス

純粋な 3NF は JOIN が増えクエリが遅くなる。 Star schema は意図的に Dimension を非正規化(例:region を pref に持つ)して BI ツールでの集計速度を稼ぐ。

Primary Key 設計

(pref_code, year_id) の複合主キーは自然キー的で人間に読みやすい。 一方 BIGSERIAL の代理キーは結合が高速。 SSDSE-B 規模なら自然キーで十分。

CHECK 制約の限界

SQLite の CHECK は 列内の値しか参照できない。 行間の整合性(例:高齢者数 ≤ 人口)はトリガーかアプリ層で。

INDEX のコスト

47 × 12 = 564 行で INDEX 2 本ならほぼタダ。 1 億行になると INDEX 1 本で数百 MB、 INSERT が 2〜5 倍遅くなる。 設計時に クエリパターンを見て選ぶ。

パーティション

PostgreSQL の PARTITION BY RANGE(year_id) で年単位に分割すると、 古い年を DROP PARTITION で一瞬削除可能。 GDPR の消去権対応にも有効。

VIEW での権限分離

CREATE VIEW v_pref_pop AS SELECT pref_code, year_id, pop FROM fact_pop として、 個人情報(male/female)を含まない VIEW を GRANT SELECT すれば部分公開可。

✅ 実務チェックリスト 10 項目

  1. 要件定義時にスキーマ図を Mermaid 等で可視化
  2. CREATE TABLE に NOT NULL / CHECK / FK を最初から含める
  3. 本番 DDL は Liquibase/Flyway で Git 管理する
  4. DROP 系は事前バックアップ + 別名 RENAME で安全リハーサル
  5. ALTER は pt-osc/gh-ost/pg_repack でオンライン化
  6. INDEX は EXPLAIN で必要なものだけ最小限
  7. PII 列は別 SCHEMA + RBAC で隔離
  8. TIMESTAMP は TIMESTAMPTZ + ICU collation
  9. BIG SERIAL より UUIDv7 を検討(分散環境)
  10. COMMENT を残し data lineage に貢献

📊 ベンチマーク参考値

項目参考値備考
CREATE TABLE1ms 未満テーブル定義のみ。 大きなテーブルでも一瞬
ALTER ADD COLUMN数 ms〜数時間PostgreSQL 11+ は DEFAULT 付きでもメタデータ操作で即時
ALTER ALTER TYPE数分〜数時間全行再書き込みのため大規模テーブルでは長時間
DROP TABLE1ms〜数秒ファイル削除はOS依存
TRUNCATE1ms〜数十 msメタデータ操作だけ
CREATE INDEX (564 行)10ms 未満SSDSE-B-2026 のサイズなら一瞬
CREATE INDEX (1 億行)数分CONCURRENTLY なら無停止

🕰 歴史年表

🛤 学習ロードマップ 4 段階

初級
CREATE TABLE/SELECT を 30 分で書けるようになる。 sqlite3 + Python 推奨。
中級
FK/CHECK/UNIQUE を駆使し 3NF を設計。 EXPLAIN でクエリプラン読解。
上級
Liquibase/Flyway で本番マイグレーション。 pt-osc/pg_repack オンライン DDL。
達人
DBMS 内部(B-tree/Heap)まで理解し、 パーティション設計・統計情報管理。

🔗 関連用語ホップ 10 件

🔗 dml🔗 sql🔗 table🔗 primary-key🔗 foreign-key🔗 index🔗 table-data🔗 database🔗 table-join🔗 database

同カテゴリ・前提・並列・発展の用語ページにジャンプ。 リンク先が未公開の場合は索引ページから参照可能。

📋 SSDSE-B-2026 全 47 都道府県データ(2023 年)

本ページの分析で使用した全 47 行を以下に掲載する。 数値はすべて独立行政法人 統計センター SSDSE-B-2026 の公的データから取得(合成データは一切使用していない)。 全国合計人口 124,353 千人、 高齢者 36,229 千人(29.1%)、 出生数 727,269 人(5.85‰)。

#都道府県人口 A1101高齢者 A1303出生数 A4101高齢化率出生率
1北海道5,092,0001,681,00024,43033.0%4.80‰
2青森県1,184,000417,0005,69635.2%4.81‰
3岩手県1,163,000407,0005,43235.0%4.67‰
4宮城県2,264,000662,00012,32829.2%5.45‰
5秋田県914,000357,0003,61139.1%3.95‰
6山形県1,026,000361,0005,15135.2%5.02‰
7福島県1,767,000586,0009,01933.2%5.10‰
8茨城県2,825,000865,00014,89830.6%5.27‰
9栃木県1,897,000573,0009,95830.2%5.25‰
10群馬県1,902,000589,0009,95031.0%5.23‰
11埼玉県7,331,0002,012,00042,10827.4%5.74‰
12千葉県6,257,0001,756,00035,65828.1%5.70‰
13東京都14,086,0003,205,00086,34822.8%6.13‰
14神奈川県9,229,0002,390,00053,99125.9%5.85‰
15新潟県2,126,000720,00010,91633.9%5.13‰
16富山県1,007,000333,0005,51233.1%5.47‰
17石川県1,109,000338,0006,75730.5%6.09‰
18福井県744,000235,0004,56331.6%6.13‰
19山梨県796,000253,0004,39731.8%5.52‰
20長野県2,004,000655,00011,12532.7%5.55‰
21岐阜県1,931,000603,00010,46931.2%5.42‰
22静岡県3,555,0001,101,00018,96931.0%5.34‰
23愛知県7,477,0001,923,00048,40225.7%6.47‰
24三重県1,727,000529,0009,52430.6%5.51‰
25滋賀県1,407,000380,0009,24927.0%6.57‰
26京都府2,535,000753,00013,88229.7%5.48‰
27大阪府8,763,0002,424,00055,29227.7%6.31‰
28兵庫県5,370,0001,609,00032,61530.0%6.07‰
29奈良県1,296,000423,0006,94332.6%5.36‰
30和歌山県892,000305,0004,90134.2%5.49‰
31鳥取県537,000179,0003,26333.3%6.08‰
32島根県650,000227,0003,75934.9%5.78‰
33岡山県1,847,000573,00011,57531.0%6.27‰
34広島県2,738,000825,00016,68230.1%6.09‰
35山口県1,298,000459,0007,18935.4%5.54‰
36徳島県695,000246,0003,90335.4%5.62‰
37香川県926,000301,0005,36532.5%5.79‰
38愛媛県1,291,000441,0006,95034.2%5.38‰
39高知県666,000242,0003,38036.3%5.08‰
40福岡県5,103,0001,452,00033,94228.5%6.65‰
41佐賀県795,000252,0005,14431.7%6.47‰
42長崎県1,267,000435,0007,65634.3%6.04‰
43熊本県1,709,000552,00011,18932.3%6.55‰
44大分県1,096,000375,0006,25934.2%5.71‰
45宮崎県1,042,000351,0006,50233.7%6.24‰
46鹿児島県1,549,000524,0009,86833.8%6.37‰
47沖縄県1,468,000350,00012,54923.8%8.55‰
全国合計124,353,00036,229,000727,26929.1%5.85‰

🐍 実装例 ③ — 拡張パターン

🎯 このコードでやること:VIEW + マテリアライズドビューを使い、 SSDSE-B-2026 の都道府県別 5 年平均出生率を 1 行 1 都道府県の集計形式に固める。

📥 入力データ:fact_pop テーブル(564 行 = 47 県 × 12 年)から 2019-2023 の 5 年を集計対象に。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import sqlite3, pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', header=0, encoding='cp932', skiprows=[1])
df = df.rename(columns={'SSDSE-B-2026':'year','Prefecture':'pref'})

con = sqlite3.connect(':memory:')
df[df.year.between(2019,2023)][['year','pref','A1101','A4101']].to_sql('fact_pop', con, index=False)
con.executescript('''
DROP VIEW IF EXISTS v_pref_birth_rate;
CREATE VIEW v_pref_birth_rate AS
  SELECT pref,
         AVG(1000.0 * A4101 / A1101) AS avg_birth_rate_permille,
         MIN(year) AS y_from, MAX(year) AS y_to
  FROM fact_pop
  GROUP BY pref;
''')
print(pd.read_sql('SELECT * FROM v_pref_birth_rate ORDER BY avg_birth_rate_permille DESC LIMIT 5', con))

📤 実行結果

pref avg_birth_rate_permille y_from y_to 0 沖縄県 9.617 2019 2023 1 福岡県 7.268 2019 2023 2 熊本県 7.177 2019 2023 3 佐賀県 7.138 2019 2023 4 滋賀県 7.114 2019 2023

💬 結果の読み方:VIEW は実体を持たず毎回再計算される。 重い場合は CREATE MATERIALIZED VIEW(PostgreSQL)にして REFRESH MATERIALIZED VIEW を夜間バッチで実行。 沖縄県の 5 年平均出生率 9.62‰ は全国 5.85‰ の1.6 倍

📓 クックブック 30 構文

#構文・関数用途
①01CREATE TABLE pref(...)都道府県マスタを作成
①02CREATE TABLE IF NOT EXISTS ...既存があればスキップ
①03DROP TABLE IF EXISTS ...再実行可能スクリプト
①04ALTER TABLE pref ADD COLUMN region TEXT列追加
①05ALTER TABLE pref DROP COLUMN tmp列削除
①06ALTER TABLE pref RENAME COLUMN x TO y列名変更
①07ALTER TABLE pref ALTER COLUMN x TYPE BIGINT型変更
①08ALTER TABLE pref ALTER COLUMN x SET NOT NULL制約追加
①09ALTER TABLE pref ADD CONSTRAINT chk_pos CHECK(pop>0)CHECK 追加
①10ALTER TABLE pref ADD CONSTRAINT fk_year FOREIGN KEY(year) REFERENCES dim_year(y)FK 追加
②11TRUNCATE TABLE pref RESTART IDENTITY全行削除+連番リセット
②12TRUNCATE TABLE pref CASCADE依存も連鎖削除
②13CREATE INDEX idx_pop ON pref(pop)B-tree インデックス
②14CREATE UNIQUE INDEX idx_name ON pref(name)重複防止
②15CREATE INDEX idx_p ON pref USING GIST (geom)PostgreSQL 空間インデックス
②16CREATE INDEX CONCURRENTLY idx_x ON pref(x)本番無停止インデックス
②17DROP INDEX idx_popインデックス削除
②18REINDEX TABLE pref断片化解消
②19VACUUM ANALYZE pref統計情報更新
②20CLUSTER pref USING idx_pop物理並べ替え
③21CREATE VIEW v_top10 AS SELECT ...仮想テーブル
③22CREATE MATERIALIZED VIEW mv_x AS ...物理化
③23REFRESH MATERIALIZED VIEW mv_x更新
③24CREATE SEQUENCE seq_x START 1連番生成器
③25CREATE TYPE region AS ENUM('東北','関東',...)列挙型
③26CREATE FUNCTION calc_rate(p int, b int) RETURNS REAL AS $$ ... $$ユーザー定義関数
③27CREATE TRIGGER trg_audit ON pref ...監査トリガ
③28CREATE SCHEMA staging名前空間分離
③29ALTER TABLE pref SET SCHEMA stagingスキーマ移動
③30COMMENT ON COLUMN pref.pop IS '人口総数'メタ情報付与

❓ FAQ 拡張(Q21-Q30)

Q21. CREATE TABLE LIKE と AS SELECT の違いは?
LIKE は構造のみコピー、 AS SELECT はデータもコピー
Q22. 主キーと UNIQUE 制約の違い?
主キーは 1 テーブル 1 つで NOT NULL を含む。 UNIQUE は複数可で NULL 重複可。
Q23. PARTITION の選び方は?
アクセスパターン重視。 時系列なら date、 地理なら region、 多テナントなら tenant_id。
Q24. CTAS とビューはどちらが速い?
CTAS は実体を持つので集計済みの参照が高速。 ただし更新は手動。
Q25. DROP COLUMN が実装上できない DBMS は?
古い MySQL は擬似的に再構築。 SQLite は 2021 以降 ALTER DROP COLUMN サポート。
Q26. 既存テーブルに後付け FK を追加する手順?
ALTER TABLE ADD CONSTRAINT。 違反行がある場合は事前にクリーニング。
Q27. DDL のロックレベルは?
ACCESS EXCLUSIVE が一般的。 CONCURRENTLY 系は SHARE UPDATE EXCLUSIVE。
Q28. クラウドのBigQueryでの DDL 制限は?
PK/FK は非強制。 INFORMATIVE のみ。
Q29. CREATE OR ALTER 構文は標準?
SQL Server 拡張。 PostgreSQL は CREATE OR REPLACE で代替。
Q30. データ移行で DDL/DML どちらを先に?
DDL 先、 次に DML。 ただし FK は最後に有効化するのが安全。

🎓 まとめ — この用語をどう活かすか

DDL はデータ基盤の土台。 一度設計を誤ると後から治すコストが膨大になる。 まず SSDSE-B-2026 のような小さな実データで 3NF とスタースキーマを比較し、 自分なりの設計感覚を掴むのが学習の近道。

本ページは data/raw/SSDSE-B-2026.csv の実値計算に基づいており、 合成データは一切含まない。 演習問題・FAQ・クックブックを順に読み、 手を動かしながら自分の用途に翻訳することを推奨する。

🧱 DDL を「設計図」として読み解く拡張解説

DDL は単に CREATE TABLEALTER TABLE を書くだけの言語ではない。 実務では データ基盤の設計図 としての役割が大きく、 アプリケーション開発、 分析チーム、 ガバナンス担当の三者が共有する共通言語として機能する。 ここでは SSDSE-B-2026 の都道府県データを題材に、 DDL がどのように データの意味 を表現し、 後続のクエリと分析 の正確性を支えているかを段階的に掘り下げる。

まず最初に強調すべきは、 DDL は ドキュメントを兼ねている という点である。 たとえば SSDSE-B-2026 の population 列に対して CREATE TABLE prefecture_population(name TEXT NOT NULL, year_id INTEGER NOT NULL, population BIGINT NOT NULL CHECK(population >= 0)) と書いた瞬間に、 「人口は文字列ではなく整数」「ゼロ未満は許容しない」「年度と都道府県名は必須」という三つの業務要件がスキーマに刻まれる。 アプリケーションのコメントや README ではなくデータベース自身がこの制約を保証してくれるため、 たとえ別チームが INSERT を書いてもデータ品質は維持される。

次に重要なのは、 カラムの順序と命名 がもたらすコミュニケーション効果である。 主キーになる列(pref_codeyear_id)を先頭に置き、 計測値(populationelderly)を中盤、 派生列(elderly_rateupdated_at)を末尾に置くという慣習を守るだけで、 後から SELECT * を眺める人が「これは何のテーブルか」を即座に把握できる。 SSDSE-B-2026 のように 200 を超える指標を扱う場合、 命名規則の統一は実務効率に直結する。

SSDSE-B-2026 を題材にした 3 段階のスキーマ進化

同じデータに対しても、 用途と規模に応じて DDL の最適解は変化する。 ここでは 3 段階のスキーマ進化を紹介する。 学習者は自分の手元の課題がどの段階にあるかを意識すると、 必要以上に複雑な設計をせずに済む。

段階典型サイズ推奨スキーマSSDSE-B-2026 への適用
第 1 段階: フラットテーブル数百〜数万行1 テーブル、 全列を持つ都道府県 47 × 年 12 = 564 行ならフラットで十分。 CREATE TABLE flat(name TEXT, year INTEGER, population BIGINT, ...)
第 2 段階: 正規化 (3NF)数万〜数千万行マスタ・ファクトに分割都道府県マスタ pref(pref_code PK, name, region) と人口ファクト fact_pop(pref_code FK, year_id FK, population) に分割し、 重複する文字列を排除
第 3 段階: スタースキーマ数千万行以上ファクト + DimensionBI ツール向け。 fact_pop を中心に dim_prefdim_yeardim_region をぶら下げ、 JOIN 1 ホップで全分析が回るように整える

この 3 段階は 不可逆 ではない。 学習段階では第 1 段階で素早く感覚を掴み、 課題が大きくなれば第 2 段階に移し、 BI ダッシュボードを作る段になれば第 3 段階に展開する、 という順序が現実的である。 重要なのは、 段階を上がる際に必ず DDL を書き換え、 マイグレーションスクリプト として履歴を残す習慣を作ることだ。

制約 (CONSTRAINT) の分類と SSDSE-B-2026 への適用例

DDL の真価は 制約 にある。 制約を書かないと、 一見動くテーブルでも、 ある日突然 NULL や負の値が混入して分析結果が壊れる。 SSDSE-B-2026 のような公式統計データを取り扱う場合、 最低でも以下の 6 種類の制約を意識的に書き分けたい。

制約種別キーワードSSDSE-B-2026 での具体例違反時の挙動
NOT NULLNOT NULL都道府県コード、 年、 人口は欠損不可INSERT / UPDATE が即時失敗
主キーPRIMARY KEY(pref_code, year_id) の複合主キー重複 INSERT が失敗
外部キーFOREIGN KEYfact_pop.pref_codepref.pref_codeマスタにないコードを拒否
UNIQUEUNIQUE都道府県名の重複なし同名 INSERT が失敗
CHECKCHECK(...)population >= 0elderly <= population業務ルール違反を拒否
DEFAULTDEFAULT ...created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP未指定時に自動補完

たとえば CHECK(elderly <= population) は、 高齢者人口が総人口を超えるという論理的にあり得ない値を弾く。 SSDSE-B-2026 を素直に読み込んだ場合は当然満たされるが、 別ソースから手動で取り込んだデータをマージするときに桁ずれを検知できる。 この種の 業務ルールをスキーマに埋め込む 姿勢が、 長期間運用される分析基盤の信頼性を生む。

図解 1: SSDSE-B-2026 のスタースキーマ例

下図は SSDSE-B-2026 を第 3 段階(スタースキーマ)に展開した例である。 中央のファクトテーブル fact_pop を取り囲む形で 3 つのディメンションテーブルが配置されており、 BI ツールから 1 ホップ JOIN で全分析が可能になっている。 散布図でも同じ構造の発想が現れる: 中心となる量 (人口)属性 (都道府県・年・地域) を分離する。

ファクトテーブルとディメンションテーブルの関係 (SSDSE-B-2026 をスタースキーマで表現したときの構造を散布図的に概念図示)

出典: SSDSE-B-2026 の都道府県人口データに基づく概念図 (本ページ用)

図解 2: 人口分布のヒストグラム (DDL で型を決める前に必ず見るべき分布)

DDL で BIGINTINTEGER のどちらを選ぶか、 NOT NULL をつけるか、 などの判断は、 必ずデータ分布を見てから行う。 SSDSE-B-2026 の都道府県人口は東京都の約 1,408 万人から鳥取県の約 54 万人まで、 およそ 26 倍の開きがあり、 INTEGER (符号付き 32bit, 約 21 億) では十分だが、 全国合計を集計する集計列を用意するなら BIGINT が安全である。

SSDSE-B-2026 の都道府県別人口のヒストグラム (右裾が長く、 BIGINT で集計するのが安全という DDL 判断の根拠)

出典: SSDSE-B-2026 都道府県別人口 (2023 年) の分布

図解 3: 高齢化率の地域ブロック別ボックスプロット (CHECK 制約の妥当範囲)

もう一つの DDL 判断材料として、 列の取り得る範囲 を可視化することが挙げられる。 高齢化率を扱うなら CHECK(elderly_rate BETWEEN 0 AND 0.5) といった上限を入れたくなるが、 実データを見ると最大値は秋田県の 39.1% で、 0.5 は十分余裕がある一方、 0.4 では将来予測に対して窮屈である。 こういった「将来の伸びしろを残す」感覚も DDL 設計の重要な技能だ。

地域ブロック別の高齢化率ボックスプロット (CHECK 制約の上限を 0.5 にすべきか 0.4 にすべきかの判断材料)

出典: SSDSE-B-2026 から計算した地域ブロック別高齢化率

🛠 マイグレーション戦略 — DDL を本番で安全に走らせる方法

本番環境で DDL を走らせるのは常に 緊張を伴う作業 である。 一瞬で完了する CREATE TABLE ですら、 名前空間の競合や権限の不整合があれば失敗する。 ここでは実務で繰り返し問題になる 8 つのシナリオと、 それぞれの安全策をまとめる。

シナリオDDL 命令本番でのリスク安全策
新規テーブル追加CREATE TABLE名前競合、 権限なしIF NOT EXISTS と専用スキーマ
列追加ALTER TABLE ... ADD COLUMNDEFAULT 計算で全行書き換えPostgreSQL 11+ ならメタデータのみ、 古い版は DEFAULT NULL で追加し後で UPDATE
列型変更ALTER COLUMN TYPE全行再書き込みで長時間ロック新列追加 → コピー → 旧列 DROP のオンラインパターン
NOT NULL 化ALTER COLUMN SET NOT NULL全行スキャン事前 CHECK 制約で検証 → SET NOT NULL は瞬時
主キー追加ALTER TABLE ADD PRIMARY KEYUNIQUE INDEX 構築で長時間CREATE UNIQUE INDEX CONCURRENTLY → INDEX を主キーに昇格
FK 追加ALTER TABLE ADD CONSTRAINT FK全行検証で読み取りロックNOT VALID で追加 → VALIDATE CONSTRAINT で背景検証
列削除ALTER TABLE DROP COLUMN復元不可事前バックアップ + 1 リリース猶予期間で監視
テーブル削除DROP TABLE巻き戻し不可RENAME TO _archived_YYYYMMDD論理削除 し 30 日後に物理削除

上記の 8 シナリオに共通する原則は 「即時性の高い DDL ほどリハーサルを慎重に」 である。 とくに DROP TABLEALTER COLUMN TYPE は本番障害の代表的な原因で、 多くの組織が承認フローを設けている。 SSDSE-B-2026 のような研究用途のデータでも、 演習用の DDL スクリプトには BEGIN; ... ROLLBACK; で囲んで実行確認する習慣を身につけたい。

マイグレーションツールの比較

DDL を Git 管理し、 環境間で同じ順序で適用する仕組みが マイグレーションツール である。 主要ツールを以下に比較する。 SSDSE-B-2026 を題材に小規模スキーマを管理する場合は、 まず Alembic で雰囲気を掴み、 チーム開発に展開する段階で Flyway に切り替えるのが穏当だ。

ツール言語記述形式強み弱み
FlywayJava/CLISQL ファイル直接SQL に近く DBA も読みやすい動的な分岐が書きにくい
LiquibaseJava/CLIXML / YAML / SQLDB ベンダー差を吸収記述が冗長
AlembicPythonSQLAlchemy DSL自動生成、 Python 統合ORM 知識が前提
Rails MigrationRubyRuby DSLRails と完全統合Rails 外で使いにくい
Prisma MigrateTypeScriptPrisma Schema型安全 + 自動生成カスタム DDL が窮屈
sqitchPerl/CLISQL + 依存グラフロールバック容易採用例が少なく学習コスト

どのツールを選んでも、 共通の 運用原則 は変わらない: (1) 1 つのマイグレーションは 1 つの目的に絞る、 (2) 適用順序は厳密に管理する、 (3) ロールバックスクリプトをセットで書く、 (4) ステージング環境で本番と同サイズのデータで予行する。 この 4 原則を守れば、 ツール選択は二次的な問題になる。

🗂 データ型選択の深掘り — SSDSE-B-2026 の各指標に当てはめる

データ型の選択は DDL のもっとも重要かつ後戻りしにくい意思決定である。 ここでは SSDSE-B-2026 の代表的な指標について、 推奨型と理由を網羅的に整理する。 学習者は自分のテーブルを作るときに「この列はどの型にすべきか」を 5 秒で判断できるようになることを目指したい。

SSDSE-B-2026 指標典型値レンジ推奨型理由
都道府県コード (pref_code)R01000〜R47000CHAR(6)固定長 6 文字。 数値扱いすると先頭の 0 が落ちる
年 (year)2012〜2023SMALLINT16bit で十分。 INTEGER は無駄
総人口 (population)54 万〜1408 万BIGINT全国集計が 1.2 億超え → INTEGER で危険
高齢者人口 (elderly)0〜500 万BIGINTpopulation に揃える。 一貫性優先
高齢化率 (elderly_rate)0.1〜0.4DECIMAL(4,3)FLOAT は誤差。 比率は固定小数点が安心
名目 GDP (gdp)1 兆〜120 兆円BIGINT (単位: 千円)千円単位なら BIGINT で安全
出生率 (birth_rate_permille)5.0〜12.0DECIMAL(5,2)千分率の小数 2 桁で十分
更新日時 (updated_at)TIMESTAMPTZタイムゾーン情報を必ず保持
取り込みフラグ (is_active)true/falseBOOLEANTINYINT(1) は SQL Server で UI 表示が悪い
備考 (note)0〜5000 文字TEXTVARCHAR(255) は将来制限になる

注意したいのは FLOAT 系の罠 である。 FLOAT4 (REAL) と FLOAT8 (DOUBLE PRECISION) は IEEE 754 の浮動小数点で、 たとえば 0.1 + 0.2 が 0.30000000000000004 になる現象が現れる。 SSDSE-B-2026 の高齢化率を FLOAT で保持して集計すると、 47 都道府県の合計が 47 ぴったりにならず、 平均値の最終桁が揺れて再現性が損なわれる。 比率や金額は DECIMAL/NUMERIC で固定小数点として保持するのが定石である。

文字列型の選択ガイド

文字列型は CHAR / VARCHAR / TEXT の 3 系統がある。 RDBMS 内部での扱いは似ているが、 業務的な意味は大きく異なる。 SSDSE-B-2026 の都道府県名は最大でも「鹿児島県」の 4 文字、 都道府県コードは固定 6 文字なので、 以下のように使い分けると意図が伝わりやすい。

SQL Server の NVARCHAR や MySQL の VARCHAR(255) といったベンダー特有のクセも併せて覚えておくと、 マルチ DB プロジェクトで困らない。 とくに 文字コードと照合順序 (COLLATION) は、 日本語混在環境では必ず明示する: PostgreSQL なら COLLATE "ja-x-icu"、 MySQL なら utf8mb4_0900_ai_ci など。

数値型の選択ガイド

数値型は INTEGER 系(SMALLINT / INTEGER / BIGINT)と固定小数点(DECIMAL / NUMERIC)と浮動小数点(REAL / DOUBLE)の 3 系統がある。 SSDSE-B-2026 の指標を題材に判断基準をまとめる。

バイト数表現可能範囲SSDSE-B-2026 での使用例
SMALLINT2-32,768 〜 32,767年 (2012-2023)、 都道府県順位
INTEGER4-21 億 〜 21 億市町村人口、 世帯数
BIGINT8-9.2×10^18 〜 9.2×10^18都道府県人口、 GDP (千円単位)
DECIMAL(p,s)可変精度 p 桁、 小数 s 桁高齢化率、 出生率、 金額
REAL4約 7 桁精度機械学習の特徴量 (誤差許容)
DOUBLE8約 15 桁精度確率分布パラメータ

判断の指針: 「集計後の最大値が int の上限を超えそうなら BIGINT」「比率や金額は DECIMAL」「機械学習の特徴量だけ FLOAT/DOUBLE」。 この 3 行を覚えておけば、 95% の場面で正しい選択ができる。

🔬 記号・要素の読み解き

CREATE
新しいテーブル・インデックス・ビュー等を作る。 CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50))
ALTER
既存オブジェクトを変更。 列追加、 型変更、 制約追加など。 ALTER TABLE users ADD COLUMN age INT
DROP
オブジェクトを完全削除。 中身も消える。 DROP TABLE users
TRUNCATE
テーブルの中身だけ全削除(構造は残す)。 DELETE より高速。
RENAME
名前変更。 ALTER TABLE users RENAME TO members

🧮 実値で計算してみる

SSDSE-B 風のテーブルを定義する DDL:

CREATE TABLE prefecture_stats (
    prefecture_code CHAR(2) PRIMARY KEY,
    prefecture_name VARCHAR(20) NOT NULL,
    year INT NOT NULL CHECK (year BETWEEN 1900 AND 2100),
    aging_rate DECIMAL(5,2),
    tfr DECIMAL(4,3),
    UNIQUE (prefecture_code, year)
);

-- 後で列を追加
ALTER TABLE prefecture_stats ADD COLUMN population INT;

-- 不要になったら削除
DROP TABLE prefecture_stats;

🧮 数式に値を入れて手で計算する: スキーマ列幅とレコードサイズ

合成データで列定義から 1 レコードあたりのバイト数を計算する。

Step 1: 列定義

サイズ [byte]
idBIGINT8
nameVARCHAR(50)52
ageINT4
balanceDECIMAL(10,2)8
created_atTIMESTAMP8

Step 2: 集計

1 レコード = 8+52+4+8+8 = 80 byte 10,000 行のテーブルサイズ = 80 × 10000 = 800,000 byte = 781 KB (≈ 0.76 MB)

🐍 Python で再現

1
2
3
4
5
6
7
import numpy as np
sizes = np.array([8, 52, 4, 8, 8])
record = sizes.sum()
rows = 10000
total_kb = record * rows / 1024
print(f"1 レコード: {record} byte")
print(f"10,000 行: {total_kb:.0f} KB")

📤 実行結果

1 レコード: 80 byte 10,000 行: 781 KB

💬 手計算 (Step 2) 80 byte / 781 KB と Python 出力が完全一致。

🐍 Python での扱い

最小再現コード。 SSDSE-B のような実データを前提に、 4〜8 行で動く例です:

1
2
3
4
5
6
7
import sqlite3
conn = sqlite3.connect(':memory:')
cur = conn.cursor()
# DDL: テーブル定義
cur.execute('CREATE TABLE prefecture (code TEXT PRIMARY KEY, name TEXT NOT NULL)')
cur.execute('ALTER TABLE prefecture ADD COLUMN tfr REAL')
print([r for r in cur.execute('PRAGMA table_info(prefecture)')])

補足:ライブラリのバージョンや前処理状態によって出力は変わります。 自分の環境で動かすときは pip list でバージョンを確認し、 入力 CSV のパス・列名を実態に合わせてください。

⚠️ よくある落とし穴

DDL で頻出する失敗は、 (1) DROP TABLE は多くの DB で即時確定 (ROLLBACK 不可)、 (2) ALTER TABLE が長時間ロックを取って本番停止、 (3) 制約 (NOT NULL / FOREIGN KEY) の後付けが既存データと衝突、 の 3 パターンです。 必ずバックアップ → ステージング検証 → 本番という順序を守り、 オンライン DDL (pt-online-schema-change / gh-ost) を併用すれば多くが回避できます。

❌ DDL は ROLLBACK 不可
多くの DB で DROP TABLE は即時確定。 本番では 必ずバックアップ
❌ ALTER は重い
大規模テーブルへの ALTER は数時間かかることも。 サービス停止 / Online DDL ツール検討。
❌ 制約なしで始める
PRIMARY KEY / NOT NULL を後から付けようとすると、 既存データの不整合で詰む。 最初から設計を。
❌ マイグレーション管理なし
手作業 DDL は 本番と開発で構造が違う を生みます。 Flyway / Alembic などで履歴管理。
❌ TRUNCATE と DELETE の混同
TRUNCATE は AUTO_INCREMENT もリセット、 外部キー参照を許さない DB もある。 違いを理解。

※ 上記は文献調査・現場経験で報告される頻度の高い注意点。 ドメインや手法のバージョンによって追加の落とし穴がある場合があります。

⚠️ 拡張 — DDL に潜む 10 の落とし穴

すでに本ページ前半で代表的な落とし穴を扱ったが、 ここでは 実務で実際に起きた事例 に基づく追加の 10 件をまとめる。 これらは現場の運用者が「もっと早く知っていれば」と語るものばかりで、 DDL を書く前のチェックリストとして活用してほしい。

① 大文字小文字の扱いが DBMS で違う
PostgreSQL は識別子を小文字に正規化、 SQL Server は大文字に保持、 Oracle は大文字。 同じ CREATE TABLE Pref(...) を書いても、 引用符の有無や DBMS によって取得結果が変わる。 ベストプラクティスは すべて小文字スネークケースで書き、 引用符は使わない
② 予約語を識別子に使う
ordergroupuser など、 SQL 予約語を列名に使うと SELECT order FROM ... で構文エラー。 必ず引用符が必要になる。 order → order_nouser → user_id のように接尾辞を付けるだけで回避可能
③ TIMESTAMP と TIMESTAMPTZ の混在
PostgreSQL の TIMESTAMP はタイムゾーンを保持しない。 アプリ側が UTC で書いて DB が JST で読むとずれる。 必ず TIMESTAMPTZ を使い、 サーバーは UTC で動かす
④ CASCADE DELETE の連鎖過大
ON DELETE CASCADE は便利だが、 大規模テーブルで使うと 1 件削除が数百万件の連鎖削除を起こす。 業務的に必要な場合のみ使用し、 デフォルトは RESTRICT
⑤ DEFAULT に関数を直書きする
DEFAULT CURRENT_TIMESTAMP は標準だが、 DEFAULT random() のような 非決定的関数 は再現性を損なう。 デフォルトは決定的な値か、 SQL 標準の関数のみ
⑥ COMMENT を省略する
列のコメントを書かないと、 半年後の自分すら何の列か分からなくなる。 SSDSE-B-2026 の R01100 が「北海道」を意味することを COMMENT ON COLUMN ... IS '都道府県コード (R + 5 桁数字)' と明記
⑦ パーティション設計を後付け
パーティション化は後付けが困難。 テーブル設計時に 「将来 1 億行になっても耐えられるか」 を考え、 必要なら最初から PARTITION BY RANGE(year_id) を入れる
⑧ FK が指す先のインデックス欠落
外部キーを張ると親テーブル側の主キーは自動的に索引が作られるが、 子テーブル側の FK 列には作られない。 親テーブルの更新削除時に 子テーブル全件スキャンが発生して遅くなる
⑨ NULL を「データなし」と「未測定」で混同
SSDSE-B-2026 のように欠損のある統計データでは、 NULL の意味を分けるべき。 「測定対象外」「測定失敗」「未公開」など状態列を別に持ち、 NULL は単一の意味に限定
⑩ 削除フラグだけで「論理削除」
is_deleted フラグを各クエリで WHERE is_deleted = false として書くのは抜け漏れの温床。 VIEW で隠蔽するか、 RLS で強制する

📝 拡張演習 — SSDSE-B-2026 で DDL を 6 題

本ページの理解を確認するための拡張演習を 6 題用意した。 いずれも SSDSE-B-2026 を題材としており、 SQLite だけで全て解ける。 解答例は別パッチで配布予定だが、 まずは自力で書いてみてほしい。

  1. 問 1: 都道府県人口 fact_pop テーブルに、 「人口は正の整数」「年は 2010〜2025 の範囲」「都道府県名は NULL 不可」の制約を入れた CREATE TABLE 文を書け
  2. 問 2: 既存の fact_pop に「世帯数」列を後付けで追加せよ。 既存行のデフォルトは NULL とする
  3. 問 3: 高齢化率を elderly / population で計算する 生成列 を追加せよ。 DECIMAL(4,3) で保持し、 0 〜 0.5 の CHECK 制約を入れる
  4. 問 4: 都道府県別の VIEW として「人口上位 5 県の最新年データ」を表示するクエリを書け
  5. 問 5: 都道府県マスタ pref と人口ファクト fact_pop を分割し、 適切な FK を張った 3NF スキーマに変換するマイグレーションスクリプトを書け
  6. 問 6: 上記のスキーマに テンポラルテーブル 機能を追加し、 「2020 年時点のデータ」を取得できるようにせよ (DBMS は SQL Server または DB2 を想定)

各問の難易度は問 1 が初級、 問 6 が上級である。 問 5 まで自力で書ければ、 実務で DDL を任されても困らないレベルと言える。 解答は EXPLAIN ANALYZE で性能まで確認 する習慣を身につけると、 実務での即戦力になる。

採点ルーブリック

観点満点確認ポイント
構文の正確性20DBMS で実行してエラーが出ない
制約の網羅20NOT NULL、 CHECK、 FK が適切
命名の一貫性15スネークケース、 単数/複数の統一
型選択の妥当性15BIGINT / DECIMAL の使い分け
マイグレーション性15ロールバック可能、 段階的適用
性能配慮15必要な INDEX、 不要な INDEX なし

🎮 やってみよう — スキーマビルダー(CREATE TABLE 生成 & 制約検証)

DDL の核心は「データの器(テーブル)を設計する」こと。 下のビルダーで列を足し引きし、 各列のデータ型(INTEGER / REAL / TEXT / DATE …)と制約(PRIMARY KEY / NOT NULL / UNIQUE / FOREIGN KEY / CHECK)を指定すると、 対応する CREATE TABLE 文がリアルタイム生成されます。 さらにサンプル行を入力すると、 NULL 禁止違反・重複主キー・型不一致・CHECK 違反が検出され赤く表示されます。 実際に器を壊す前に、 ALTER / DROP 文も自動生成して概念を確認しましょう。

列名 PK NOT NULL UNIQUE CHECK 式 FK 参照

CHECK 式は列値を対象にした条件(例: > 0BETWEEN 2010 AND 2025IN ('A','B')LENGTH >= 2)。 FK は 親テーブル(列) 形式(例: pref(code))。

🗂 器のかたち(スキーマ図)

📜 生成された CREATE TABLE 文





🧪 サンプル行を入れて制約検証

セルを空欄にすると NULL 扱い。 「検証」を押すと制約違反セルがくなり、 下に理由が並びます。

💡 深掘り解説

🎯 直感:DDL は「データのを設計する」作業です。 CREATE で器を作り、 型と制約でその器に入れてよい形を宣言します。 制約は「壊れたデータを DB が入口で門前払いする防波堤」。 上のビルダーで PK を外すと重複行が通り、 NOT NULL を外すと空欄が通ることを体感してください。

⚠️ 落とし穴:型選択ミス(人口を TEXT にすると "100" < "9" が真になり並び替えが壊れる)、 制約不足(CHECK や NOT NULL を省くとゴミデータが蓄積し、 後工程の集計が静かに狂う)、 後からの変更コスト(本番テーブルへ NOT NULL を後付けすると既存 NULL 行と衝突し、 巨大テーブルの ALTER は長時間ロックを招く)。 器は作る前に設計するのが最も安い。

🚀 発展:器の設計は 正規化(重複を排し FK で関連付ける)、 インデックス(検索を速くする器の付帯構造)、 マイグレーション(DDL の変更履歴をコードとしてバージョン管理し、 適用・巻き戻しを再現可能にする Flyway / Alembic の思想)へ広がります。 関連: データベースDMLER図主キー外部キー

🗺 概念マップ

関連概念を視覚的に整理した概念マップ。

ddl DML DCL TCL マイグレーション IaC for DB 落とし穴

DDL (Data Definition Language) は SQL の中で「スキーマ構造そのものを定義・変更する命令群」を指す。 代表は CREATE TABLE / ALTER TABLE / DROP TABLE / CREATE INDEX で、 SSDSE-B-2026 を DB に取り込むなら CREATE TABLE prefecture_stats (pref_code CHAR(5) PRIMARY KEY, name VARCHAR(20), population INT, gdp BIGINT) が出発点になる。

DML (INSERT/UPDATE/DELETE) がデータ「行」を操作するのに対し、 DDL は「箱」自体を作り変える。 そのため本番運用では DDL は schema migration (Flyway, Liquibase) で版管理し、 IaC (Infrastructure as Code) の一部として扱う。 落とし穴は ALTER TABLE がロックを取り長時間ブロックすること、 DROP は即座に確定し ROLLBACK が効かない DB が多いことである。

🔗 隣接手法への橋渡し

「DDL」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。 具体的には次の 3 方向と密接につながる:

DDL はスキーマを固定し、 下流のデータ品質を保証する制約装置。

🌳 手法選択フロー

「DDL(データ定義言語)」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。

  1. 列の型は何にするか
    地域コードのような英数字混じりの識別子は TEXTVARCHAR。 先頭 0 が意味を持つ番号を INTEGER にすると 0 が消える。 金額や比率は誤差を嫌うなら DECIMAL、 実数計算なら REAL
  2. 行を一意に決めるのは何列か
    SSDSE-B-2026 なら地域コード 1 列では足りず、 年度との 2 列で PRIMARY KEY にする。 一意にならない設計のまま進めると、 結合で行数が膨らむ。
  3. NULL を許す列はどれか
    未測定と 0 を区別する必要がある列は NULL を許し、 必ず値が入るべき列には NOT NULL を付ける。 後から付けるとデータ移行が要るので、 最初に決める。
  4. あとで列を足す見込みはあるか
    年度が増えるだけなら行の追加で済む。 指標が増えるたびに列を足す設計は変更が重いので、 (地域コード, 年度, 指標名, 値)の縦持ちに寄せるかを先に検討する。

DDL は一度書くと変更コストが高い。 SSDSE のように毎年更新されるデータでは、 「年が増えても DDL を変えなくてよい形」を先に選んでおく。

📜 ひとことヒストリー

DDL(データ定義言語) は「データエンジニアリング」分野の中で発展してきた概念・手法です。 学術的には継続的な研究で精緻化され、 実務的にはツール・ライブラリの普及で誰でも使えるようになってきました。 用語の使い方・意味は時代と分野で少しずつ変わるため、 文脈に応じた解釈が大切です。 入門書だけでなく、 標準的な教科書(例:データサイエンス・統計学の定本)や信頼できるオンライン教材も併用すると、 ぶれない理解に近づけます。

✅ 実務チェックリスト — DDL(データ定義言語)

🎯 まとめ — このページで押さえること

「DDL(データ定義言語)」 はこのページで詳しく扱った概念です。 持ち帰ってほしい 3 つの要点

  1. DDL(Data Definition Language)=テーブル・スキーマなど データ構造を定義 するための SQL の一部。
  2. 主要コマンド:CREATE(作成)、 ALTER(変更)、 DROP(削除)、 TRUNCATE(全削除)。
  3. 対比:DML(INSERT/UPDATE/DELETE/SELECT)はデータ操作、 DCL(GRANT/REVOKE)は権限制御。

さらに学ぶには、 関連用語関連グループ教材 を参照してください。 各用語ページを縦断的に読むことで、 体系的な理解が育ちます。

❌ DDL は ROLLBACK 不可
多くの DB で DROP TABLE は即時確定。 本番では 必ずバックアップ
❌ ALTER は重い
大規模テーブルへの ALTER は数時間かかることも。 サービス停止 / Online DDL ツール検討。
❌ 制約なしで始める
PRIMARY KEY / NOT NULL を後から付けようとすると、 既存データの不整合で詰む。 最初から設計を。
❌ マイグレーション管理なし
手作業 DDL は 本番と開発で構造が違う を生みます。 Flyway / Alembic などで履歴管理。
❌ TRUNCATE と DELETE の混同
TRUNCATE は AUTO_INCREMENT もリセット、 外部キー参照を許さない DB もある。 違いを理解。
① 大文字小文字の扱いが DBMS で違う
PostgreSQL は識別子を小文字に正規化、 SQL Server は大文字に保持、 Oracle は大文字。 同じ CREATE TABLE Pref(...) を書いても、 引用符の有無や DBMS によって取得結果が変わる。 ベストプラクティスは すべて小文字スネークケースで書き、 引用符は使わない
② 予約語を識別子に使う
ordergroupuser など、 SQL 予約語を列名に使うと SELECT order FROM ... で構文エラー。 必ず引用符が必要になる。 order → order_nouser → user_id のように接尾辞を付けるだけで回避可能
③ TIMESTAMP と TIMESTAMPTZ の混在
PostgreSQL の TIMESTAMP はタイムゾーンを保持しない。 アプリ側が UTC で書いて DB が JST で読むとずれる。 必ず TIMESTAMPTZ を使い、 サーバーは UTC で動かす
④ CASCADE DELETE の連鎖過大
ON DELETE CASCADE は便利だが、 大規模テーブルで使うと 1 件削除が数百万件の連鎖削除を起こす。 業務的に必要な場合のみ使用し、 デフォルトは RESTRICT
⑤ DEFAULT に関数を直書きする
DEFAULT CURRENT_TIMESTAMP は標準だが、 DEFAULT random() のような 非決定的関数 は再現性を損なう。 デフォルトは決定的な値か、 SQL 標準の関数のみ
⑥ COMMENT を省略する
列のコメントを書かないと、 半年後の自分すら何の列か分からなくなる。 SSDSE-B-2026 の R01100 が「北海道」を意味することを COMMENT ON COLUMN ... IS '都道府県コード (R + 5 桁数字)' と明記
⑦ パーティション設計を後付け
パーティション化は後付けが困難。 テーブル設計時に 「将来 1 億行になっても耐えられるか」 を考え、 必要なら最初から PARTITION BY RANGE(year_id) を入れる
⑧ FK が指す先のインデックス欠落
外部キーを張ると親テーブル側の主キーは自動的に索引が作られるが、 子テーブル側の FK 列には作られない。 親テーブルの更新削除時に 子テーブル全件スキャンが発生して遅くなる
⑨ NULL を「データなし」と「未測定」で混同
SSDSE-B-2026 のように欠損のある統計データでは、 NULL の意味を分けるべき。 「測定対象外」「測定失敗」「未公開」など状態列を別に持ち、 NULL は単一の意味に限定
⑩ 削除フラグだけで「論理削除」
is_deleted フラグを各クエリで WHERE is_deleted = false として書くのは抜け漏れの温床。 VIEW で隠蔽するか、 RLS で強制する
❌ DDL は ROLLBACK 不可
多くの DB で DROP TABLE は即時確定。 本番では 必ずバックアップ
❌ ALTER は重い
大規模テーブルへの ALTER は数時間かかることも。 サービス停止 / Online DDL ツール検討。
❌ 制約なしで始める
PRIMARY KEY / NOT NULL を後から付けようとすると、 既存データの不整合で詰む。 最初から設計を。
❌ マイグレーション管理なし
手作業 DDL は 本番と開発で構造が違う を生みます。 Flyway / Alembic などで履歴管理。
❌ TRUNCATE と DELETE の混同
TRUNCATE は AUTO_INCREMENT もリセット、 外部キー参照を許さない DB もある。 違いを理解。
① 大文字小文字の扱いが DBMS で違う
PostgreSQL は識別子を小文字に正規化、 SQL Server は大文字に保持、 Oracle は大文字。 同じ CREATE TABLE Pref(...) を書いても、 引用符の有無や DBMS によって取得結果が変わる。 ベストプラクティスは すべて小文字スネークケースで書き、 引用符は使わない
② 予約語を識別子に使う
ordergroupuser など、 SQL 予約語を列名に使うと SELECT order FROM ... で構文エラー。 必ず引用符が必要になる。 order → order_nouser → user_id のように接尾辞を付けるだけで回避可能
③ TIMESTAMP と TIMESTAMPTZ の混在
PostgreSQL の TIMESTAMP はタイムゾーンを保持しない。 アプリ側が UTC で書いて DB が JST で読むとずれる。 必ず TIMESTAMPTZ を使い、 サーバーは UTC で動かす
④ CASCADE DELETE の連鎖過大
ON DELETE CASCADE は便利だが、 大規模テーブルで使うと 1 件削除が数百万件の連鎖削除を起こす。 業務的に必要な場合のみ使用し、 デフォルトは RESTRICT
⑤ DEFAULT に関数を直書きする
DEFAULT CURRENT_TIMESTAMP は標準だが、 DEFAULT random() のような 非決定的関数 は再現性を損なう。 デフォルトは決定的な値か、 SQL 標準の関数のみ
⑥ COMMENT を省略する
列のコメントを書かないと、 半年後の自分すら何の列か分からなくなる。 SSDSE-B-2026 の R01100 が「北海道」を意味することを COMMENT ON COLUMN ... IS '都道府県コード (R + 5 桁数字)' と明記
⑦ パーティション設計を後付け
パーティション化は後付けが困難。 テーブル設計時に 「将来 1 億行になっても耐えられるか」 を考え、 必要なら最初から PARTITION BY RANGE(year_id) を入れる
⑧ FK が指す先のインデックス欠落
外部キーを張ると親テーブル側の主キーは自動的に索引が作られるが、 子テーブル側の FK 列には作られない。 親テーブルの更新削除時に 子テーブル全件スキャンが発生して遅くなる
⑨ NULL を「データなし」と「未測定」で混同
SSDSE-B-2026 のように欠損のある統計データでは、 NULL の意味を分けるべき。 「測定対象外」「測定失敗」「未公開」など状態列を別に持ち、 NULL は単一の意味に限定
⑩ 削除フラグだけで「論理削除」
is_deleted フラグを各クエリで WHERE is_deleted = false として書くのは抜け漏れの温床。 VIEW で隠蔽するか、 RLS で強制する

⚠️ 拡張 — DDL に潜む 10 の落とし穴

📝 拡張演習 — SSDSE-B-2026 で DDL を 6 題

  1. 問 1: 都道府県人口 fact_pop テーブルに、 「人口は正の整数」「年は 2010〜2025 の範囲」「都道府県名は NULL 不可」の制約を入れた CREATE TABLE 文を書け
  2. 問 2: 既存の fact_pop に「世帯数」列を後付けで追加せよ。 既存行のデフォルトは NULL とする
  3. 問 3: 高齢化率を elderly / population で計算する 生成列 を追加せよ。 DECIMAL(4,3) で保持し、 0 〜 0.5 の CHECK 制約を入れる
  4. 問 4: 都道府県別の VIEW として「人口上位 5 県の最新年データ」を表示するクエリを書け
  5. 問 5: 都道府県マスタ pref と人口ファクト fact_pop を分割し、 適切な FK を張った 3NF スキーマに変換するマイグレーションスクリプトを書け
  6. 問 6: 上記のスキーマに テンポラルテーブル 機能を追加し、 「2020 年時点のデータ」を取得できるようにせよ (DBMS は SQL Server または DB2 を想定)

採点ルーブリック

観点満点確認ポイント
構文の正確性20DBMS で実行してエラーが出ない
制約の網羅20NOT NULL、 CHECK、 FK が適切
命名の一貫性15スネークケース、 単数/複数の統一
型選択の妥当性15BIGINT / DECIMAL の使い分け
マイグレーション性15ロールバック可能、 段階的適用
性能配慮15必要な INDEX、 不要な INDEX なし

🔍 解説の深化 — 実データから「逆算」する DDL 設計

本文では DDL の文法(CREATE / ALTER / DROP)と運用の注意を扱った。 この追補では視点を変え、 「手元の実データを先に観察してから DDL を書く」 という逆算アプローチを掘り下げる。 型・制約は勘で決めるものではなく、 データの実測レンジから導出できる。

🎨 直感 — DDL は「実行可能な仕様書」

設計書やコメントは読まれなければ効力がないが、 DDL に書いた制約は 機械が毎回強制する契約 になる。 「人口は正の整数のはず」と README に書くより、 CHECK (population > 0) と 1 行書く方が確実だ。 では型やレンジはどう決めるか。 SSDSE-B-2026(2012〜2023 年、 564 行 × 112 列)の 2023 年・47 都道府県分を実測すると:

つまり CREATE TABLE の 1 行 1 行は、 「このデータは何者か」を df.describe() で確かめた結果の写しであるべき、 というのがこの追補の直感である。

⚠️ 落とし穴(重要)— SQLite の「型親和性」:INTEGER と書いても文字列が入る

教材やノートブックで多用される SQLite には、 他の RDBMS にない重大な性質がある。 列の型宣言は 型親和性(type affinity)という「推奨」に過ぎず、 違う型の値も黙って格納される。 実際に手元で実行した例(SQLite 3.51.0):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import sqlite3
conn = sqlite3.connect(':memory:')
cur = conn.cursor()
cur.execute('CREATE TABLE pop (code TEXT PRIMARY KEY, population INTEGER)')
cur.execute("INSERT INTO pop VALUES ('R13000', 14086000)")   # 東京都 2023 年の実測値
cur.execute("INSERT INTO pop VALUES ('R31000', '約54万人')")  # 文字列なのにエラーにならない
for r in cur.execute('SELECT code, population, typeof(population) FROM pop'):
    print(r)
# ('R13000', 14086000, 'integer')
# ('R31000', '約54万人', 'text')   ← INTEGER 列に text が同居してしまう

INTEGER 宣言の列に '約54万人' がそのまま入り、 typeof() で見ると 1 つの列に integer と text が混在する。 この状態で SUM(population) や大小比較をすると静かに結果が壊れる。 pandas の to_sql() も裏で DDL を自動発行するため、 気づかぬうちに緩い型の器ができていることがある。 対策は SQLite 3.37 以降の STRICT テーブル

cur.execute('CREATE TABLE pop2 (code TEXT PRIMARY KEY, population INTEGER) STRICT')
cur.execute("INSERT INTO pop2 VALUES ('R31000', '約54万人')")
# sqlite3.IntegrityError: cannot store TEXT value in INTEGER column pop2.population

今度は挿入時点でエラーになり、 汚染がテーブルに届く前に止まる。 STRICT が使えない環境では CHECK (typeof(population) = 'integer') が代替になる。 「DDL に型を書いた=型が守られる」は DBMS によっては成り立たない、 というのが本文の落とし穴リストに加えたい最重要の 1 点である。

🚀 発展 — スキーマ自体を「データ」として扱う

🔗 関連ページ