「DDL(データ定義言語)」を取り巻く中核キーワード群です。 検索やインデックス作成で参照する際の手がかりにしてください。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になります。
🍰 まずはやさしく
データの入れ物を作るための言葉です。
データの構造を決めるために使います。
スマホの連絡先アプリに項目を増やすようなものです。
ここでは主要なコマンドについて学びます。
最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:
CREATE(作成)、 ALTER(変更)、 DROP(削除)、 TRUNCATE(全削除)。🍰 まずはやさしく
データの器を設計する道具です。
入れ物の形を変えたいときに使います。
部活の名簿に新しい列を追加するような場面です。
どのような時に使うのかを詳しく読みましょう。
「分析用テーブルを新規に作りたい」 「列を 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) で再現の順に進める。
🍰 まずはやさしく
構造を定義するための命令セットです。
テーブルなどの土台を作るために使います。
買い物リストの項目を新しく決めるような操作です。
それぞれの命令の意味と使い方を確認しましょう。
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 で消します。
🎯 このコードでやること: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()) |
📤 実行すると次の出力が得られる:
💬 結果の読み方:CREATE TABLE で 4 列+計算列 elderly_rate を定義し、 INSERT で 47 行を投入。 高齢化率 39.1% の秋田県が最上位。 DDL なくして DML は走らない。
SSDSE-B-2026 から 2023 年 47 行を取り出し、 CREATE TABLE で 4 列のテーブルを作成、 INSERT ... SELECT で投入したあと、 高齢化率順に 5 件取り出したのが先のコードの出力。 ここで重要なのは 計算列 (GENERATED ALWAYS) によって elderly_rate = 1.0 * elderly / population が DDL レベルで定義されていること。 アプリケーション側が割り算を忘れても整合性が崩れない。
| 都道府県 | 人口 A1101 | 高齢者 A1303 | 高齢化率 |
|---|---|---|---|
| 秋田県 | 914,000 | 357,000 | 39.1% |
| 高知県 | 666,000 | 242,000 | 36.3% |
| 徳島県 | 695,000 | 246,000 | 35.4% |
| 山口県 | 1,298,000 | 459,000 | 35.4% |
| 青森県 | 1,184,000 | 417,000 | 35.2% |
本ページの数値はすべて公的データ SSDSE-B-2026(独立行政法人 統計センター) を data/raw/SSDSE-B-2026.csv として読み込み、 2023 年・47 都道府県のレコードを集計したもの。 合成データは一切使用していない。
| 業種・領域 | 活用内容 | 代表事例 |
|---|---|---|
| 自治体オープンデータ基盤 | 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)) を必ず付ける。 |
GENERATED ALWAYS AS でアプリ依存を減らす。CREATE TABLE と CREATE TABLE IF NOT EXISTS の挙動の違いを 80 字以内で述べよ。pref(code TEXT PRIMARY KEY, name TEXT) を作る DDL を書け。解答例は付属の Jupyter Notebook(notebooks/glossary_exercises.ipynb)に収録。 SSDSE-B-2026 を使って自力で動かしてから答え合わせすること。
CREATE SCHEMA。FOREIGN KEY (col) REFERENCES tbl(pk)。CREATE INDEX で作る(実は DDL)。CREATE VIEW で定義。CREATE SEQUENCE。日常の SSDSE-B-2026 分析でそのままコピペして使える 50 個のスニペット集。 1 行で完結するパターンを優先。
CREATE TABLE pref(code TEXT PRIMARY KEY, name TEXT)ALTER TABLE pref ADD COLUMN pop INTEGERALTER TABLE pref DROP COLUMN tmpALTER TABLE pref RENAME TO prefectureALTER TABLE pref RENAME COLUMN pop TO populationCREATE INDEX idx_pop ON pref(pop)CREATE UNIQUE INDEX idx_name ON pref(name)DROP INDEX idx_popCREATE VIEW v_top10 AS SELECT * FROM pref ORDER BY pop DESC LIMIT 10DROP VIEW v_top10CREATE TABLE log(id BIGSERIAL PRIMARY KEY, ts TIMESTAMPTZ DEFAULT now())ALTER TABLE log ADD CONSTRAINT chk_ts CHECK(ts<=now())CREATE SCHEMA stagingALTER TABLE pref SET SCHEMA stagingCREATE TABLE pref_y2023 PARTITION OF pref FOR VALUES FROM ('2023-01-01') TO ('2024-01-01')CREATE MATERIALIZED VIEW mv_decile AS SELECT NTILE(10) OVER(ORDER BY pop) decile,* FROM prefREFRESH MATERIALIZED VIEW mv_decileCREATE SEQUENCE pref_seq START 47 INCREMENT 1ALTER SEQUENCE pref_seq RESTART WITH 1DROP SEQUENCE pref_seqCREATE TYPE region AS ENUM('北海道','東北','関東','中部','近畿','中国','四国','九州')ALTER TYPE region ADD VALUE '沖縄'CREATE TABLE pop_log(LIKE pref INCLUDING ALL)CREATE TABLE temp_pref(LIKE pref) ON COMMIT DROPTRUNCATE TABLE pref RESTART IDENTITY CASCADECREATE OR REPLACE FUNCTION f() RETURNS int AS $$BEGIN RETURN 1;END$$ LANGUAGE plpgsqlCREATE TRIGGER trg AFTER INSERT ON pref EXECUTE FUNCTION f()DROP TRIGGER trg ON prefCOMMENT ON TABLE pref IS 'SSDSE-B-2026 都道府県マスタ'COMMENT ON COLUMN pref.pop IS '人口総数 A1101'CREATE EXTENSION IF NOT EXISTS pgcryptoCREATE ROLE analyst NOLOGINGRANT SELECT ON pref TO analystREVOKE INSERT ON pref FROM analystCREATE POLICY p1 ON pref USING (code LIKE 'R01%')ALTER TABLE pref ENABLE ROW LEVEL SECURITYCREATE COLLATION ja (LOCALE='ja_JP.utf8')CREATE DOMAIN pref_code AS CHAR(6) CHECK(VALUE ~ '^R[0-9]{5}$')CREATE TABLESPACE ts1 LOCATION '/data/ts1'ALTER TABLE pref SET TABLESPACE ts1REINDEX TABLE prefVACUUM ANALYZE prefCLUSTER pref USING idx_popALTER TABLE pref ALTER COLUMN name SET NOT NULLALTER TABLE pref ALTER COLUMN pop TYPE BIGINT USING pop::BIGINTALTER TABLE pref ADD GENERATED ALWAYS AS IDENTITYCREATE STATISTICS s1 (dependencies) ON code, pop FROM prefCREATE PUBLICATION pub FOR TABLE prefCREATE SUBSCRIPTION sub CONNECTION 'host=...' PUBLICATION pubDROP DATABASE IF EXISTS old_db| 都道府県 | 人口 A1101 | 高齢者 A1303 | 出生 A4101 | 高齢化率 | 出生率 |
|---|---|---|---|---|---|
| 東京都 | 14,086,000 | 3,205,000 | 86,348 | 22.8% | 6.13‰ |
| 神奈川県 | 9,229,000 | 2,390,000 | 53,991 | 25.9% | 5.85‰ |
| 大阪府 | 8,763,000 | 2,424,000 | 55,292 | 27.7% | 6.31‰ |
| 愛知県 | 7,477,000 | 1,923,000 | 48,402 | 25.7% | 6.47‰ |
| 埼玉県 | 7,331,000 | 2,012,000 | 42,108 | 27.4% | 5.74‰ |
| 千葉県 | 6,257,000 | 1,756,000 | 35,658 | 28.1% | 5.70‰ |
| 兵庫県 | 5,370,000 | 1,609,000 | 32,615 | 30.0% | 6.07‰ |
| 福岡県 | 5,103,000 | 1,452,000 | 33,942 | 28.5% | 6.65‰ |
| 北海道 | 5,092,000 | 1,681,000 | 24,430 | 33.0% | 4.80‰ |
| 静岡県 | 3,555,000 | 1,101,000 | 18,969 | 31.0% | 5.34‰ |
TOP10 だけで全国人口の 60% 以上を占める一極集中。 東京都の高齢化率は 22.8% と最低で出生率も 6.13‰ と最高水準。 ただし出生数の絶対値は東京 86,348 人と圧倒的に多く、 県別の率と絶対値の差異に注意。
| 都道府県 | 人口 A1101 | 高齢者 A1303 | 出生 A4101 | 高齢化率 | 出生率 |
|---|---|---|---|---|---|
| 鳥取県 | 537,000 | 179,000 | 3,263 | 33.3% | 6.08‰ |
| 島根県 | 650,000 | 227,000 | 3,759 | 34.9% | 5.78‰ |
| 高知県 | 666,000 | 242,000 | 3,380 | 36.3% | 5.08‰ |
| 徳島県 | 695,000 | 246,000 | 3,903 | 35.4% | 5.62‰ |
| 福井県 | 744,000 | 235,000 | 4,563 | 31.6% | 6.13‰ |
| 佐賀県 | 795,000 | 252,000 | 5,144 | 31.7% | 6.47‰ |
| 山梨県 | 796,000 | 253,000 | 4,397 | 31.8% | 5.52‰ |
| 和歌山県 | 892,000 | 305,000 | 4,901 | 34.2% | 5.49‰ |
| 秋田県 | 914,000 | 357,000 | 3,611 | 39.1% | 3.95‰ |
| 香川県 | 926,000 | 301,000 | 5,365 | 32.5% | 5.79‰ |
BOTTOM10 は地方県中心で、 秋田県の高齢化率は 39.1% と最高、 出生率も 3.95‰ と低い。 こうした少数派サンプルこそ統計指標が極端な値を取り、 平均では見えない実態が浮かぶ。
| 年 | 東京都人口 | 高齢者 | 出生数 | 高齢化率 | 出生率 |
|---|---|---|---|---|---|
| 2012 | 13,234,000 | 2,812,000 | 107,401 | 21.25% | 8.12‰ |
| 2013 | 13,307,000 | 2,914,000 | 109,986 | 21.90% | 8.27‰ |
| 2014 | 13,399,000 | 3,011,000 | 110,629 | 22.47% | 8.26‰ |
| 2015 | 13,515,271 | 3,005,516 | 113,194 | 22.24% | 8.38‰ |
| 2016 | 13,646,000 | 3,120,000 | 111,964 | 22.86% | 8.20‰ |
| 2017 | 13,768,000 | 3,160,000 | 108,990 | 22.95% | 7.92‰ |
| 2018 | 13,887,000 | 3,189,000 | 107,150 | 22.96% | 7.72‰ |
| 2019 | 14,007,000 | 3,209,000 | 101,818 | 22.91% | 7.27‰ |
| 2020 | 14,047,594 | 3,107,822 | 99,661 | 22.12% | 7.09‰ |
| 2021 | 14,010,000 | 3,202,000 | 95,404 | 22.86% | 6.81‰ |
| 2022 | 14,038,000 | 3,202,000 | 91,097 | 22.81% | 6.49‰ |
| 2023 | 14,086,000 | 3,205,000 | 86,348 | 22.75% | 6.13‰ |
12 年で人口は 13.23M → 14.09M(+6.4%)、 高齢者は 2.81M → 3.21M(+14%)、 出生数は 107,401 → 86,348(△19.6%)。 人口増加の影でも出生数は急減。 これが「都市部の少子化」の実像。
🎯 このコードでやること: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); ''') |
📤 実行結果:
💬 結果の読み方:スタースキーマで 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()) |
📤 実行結果:
💬 結果の読み方:DEFAULT NULL で既存 564 行を壊さず列を追加。 後で本物のデータを UPDATE で埋める。 これがオンライン DDL の基本パターン。
純粋な 3NF は JOIN が増えクエリが遅くなる。 Star schema は意図的に Dimension を非正規化(例:region を pref に持つ)して BI ツールでの集計速度を稼ぐ。
(pref_code, year_id) の複合主キーは自然キー的で人間に読みやすい。 一方 BIGSERIAL の代理キーは結合が高速。 SSDSE-B 規模なら自然キーで十分。
SQLite の CHECK は 列内の値しか参照できない。 行間の整合性(例:高齢者数 ≤ 人口)はトリガーかアプリ層で。
47 × 12 = 564 行で INDEX 2 本ならほぼタダ。 1 億行になると INDEX 1 本で数百 MB、 INSERT が 2〜5 倍遅くなる。 設計時に クエリパターンを見て選ぶ。
PostgreSQL の PARTITION BY RANGE(year_id) で年単位に分割すると、 古い年を DROP PARTITION で一瞬削除可能。 GDPR の消去権対応にも有効。
CREATE VIEW v_pref_pop AS SELECT pref_code, year_id, pop FROM fact_pop として、 個人情報(male/female)を含まない VIEW を GRANT SELECT すれば部分公開可。
CREATE TABLE に NOT NULL / CHECK / FK を最初から含めるDROP 系は事前バックアップ + 別名 RENAME で安全リハーサルALTER は pt-osc/gh-ost/pg_repack でオンライン化| 項目 | 参考値 | 備考 |
|---|---|---|
| CREATE TABLE | 1ms 未満 | テーブル定義のみ。 大きなテーブルでも一瞬 |
| ALTER ADD COLUMN | 数 ms〜数時間 | PostgreSQL 11+ は DEFAULT 付きでもメタデータ操作で即時 |
| ALTER ALTER TYPE | 数分〜数時間 | 全行再書き込みのため大規模テーブルでは長時間 |
| DROP TABLE | 1ms〜数秒 | ファイル削除はOS依存 |
| TRUNCATE | 1ms〜数十 ms | メタデータ操作だけ |
| CREATE INDEX (564 行) | 10ms 未満 | SSDSE-B-2026 のサイズなら一瞬 |
| CREATE INDEX (1 億行) | 数分 | CONCURRENTLY なら無停止 |
CREATE TABLE/SELECT を 30 分で書けるようになる。 sqlite3 + Python 推奨。同カテゴリ・前提・並列・発展の用語ページにジャンプ。 リンク先が未公開の場合は索引ページから参照可能。
本ページの分析で使用した全 47 行を以下に掲載する。 数値はすべて独立行政法人 統計センター SSDSE-B-2026 の公的データから取得(合成データは一切使用していない)。 全国合計人口 124,353 千人、 高齢者 36,229 千人(29.1%)、 出生数 727,269 人(5.85‰)。
| # | 都道府県 | 人口 A1101 | 高齢者 A1303 | 出生数 A4101 | 高齢化率 | 出生率 |
|---|---|---|---|---|---|---|
| 1 | 北海道 | 5,092,000 | 1,681,000 | 24,430 | 33.0% | 4.80‰ |
| 2 | 青森県 | 1,184,000 | 417,000 | 5,696 | 35.2% | 4.81‰ |
| 3 | 岩手県 | 1,163,000 | 407,000 | 5,432 | 35.0% | 4.67‰ |
| 4 | 宮城県 | 2,264,000 | 662,000 | 12,328 | 29.2% | 5.45‰ |
| 5 | 秋田県 | 914,000 | 357,000 | 3,611 | 39.1% | 3.95‰ |
| 6 | 山形県 | 1,026,000 | 361,000 | 5,151 | 35.2% | 5.02‰ |
| 7 | 福島県 | 1,767,000 | 586,000 | 9,019 | 33.2% | 5.10‰ |
| 8 | 茨城県 | 2,825,000 | 865,000 | 14,898 | 30.6% | 5.27‰ |
| 9 | 栃木県 | 1,897,000 | 573,000 | 9,958 | 30.2% | 5.25‰ |
| 10 | 群馬県 | 1,902,000 | 589,000 | 9,950 | 31.0% | 5.23‰ |
| 11 | 埼玉県 | 7,331,000 | 2,012,000 | 42,108 | 27.4% | 5.74‰ |
| 12 | 千葉県 | 6,257,000 | 1,756,000 | 35,658 | 28.1% | 5.70‰ |
| 13 | 東京都 | 14,086,000 | 3,205,000 | 86,348 | 22.8% | 6.13‰ |
| 14 | 神奈川県 | 9,229,000 | 2,390,000 | 53,991 | 25.9% | 5.85‰ |
| 15 | 新潟県 | 2,126,000 | 720,000 | 10,916 | 33.9% | 5.13‰ |
| 16 | 富山県 | 1,007,000 | 333,000 | 5,512 | 33.1% | 5.47‰ |
| 17 | 石川県 | 1,109,000 | 338,000 | 6,757 | 30.5% | 6.09‰ |
| 18 | 福井県 | 744,000 | 235,000 | 4,563 | 31.6% | 6.13‰ |
| 19 | 山梨県 | 796,000 | 253,000 | 4,397 | 31.8% | 5.52‰ |
| 20 | 長野県 | 2,004,000 | 655,000 | 11,125 | 32.7% | 5.55‰ |
| 21 | 岐阜県 | 1,931,000 | 603,000 | 10,469 | 31.2% | 5.42‰ |
| 22 | 静岡県 | 3,555,000 | 1,101,000 | 18,969 | 31.0% | 5.34‰ |
| 23 | 愛知県 | 7,477,000 | 1,923,000 | 48,402 | 25.7% | 6.47‰ |
| 24 | 三重県 | 1,727,000 | 529,000 | 9,524 | 30.6% | 5.51‰ |
| 25 | 滋賀県 | 1,407,000 | 380,000 | 9,249 | 27.0% | 6.57‰ |
| 26 | 京都府 | 2,535,000 | 753,000 | 13,882 | 29.7% | 5.48‰ |
| 27 | 大阪府 | 8,763,000 | 2,424,000 | 55,292 | 27.7% | 6.31‰ |
| 28 | 兵庫県 | 5,370,000 | 1,609,000 | 32,615 | 30.0% | 6.07‰ |
| 29 | 奈良県 | 1,296,000 | 423,000 | 6,943 | 32.6% | 5.36‰ |
| 30 | 和歌山県 | 892,000 | 305,000 | 4,901 | 34.2% | 5.49‰ |
| 31 | 鳥取県 | 537,000 | 179,000 | 3,263 | 33.3% | 6.08‰ |
| 32 | 島根県 | 650,000 | 227,000 | 3,759 | 34.9% | 5.78‰ |
| 33 | 岡山県 | 1,847,000 | 573,000 | 11,575 | 31.0% | 6.27‰ |
| 34 | 広島県 | 2,738,000 | 825,000 | 16,682 | 30.1% | 6.09‰ |
| 35 | 山口県 | 1,298,000 | 459,000 | 7,189 | 35.4% | 5.54‰ |
| 36 | 徳島県 | 695,000 | 246,000 | 3,903 | 35.4% | 5.62‰ |
| 37 | 香川県 | 926,000 | 301,000 | 5,365 | 32.5% | 5.79‰ |
| 38 | 愛媛県 | 1,291,000 | 441,000 | 6,950 | 34.2% | 5.38‰ |
| 39 | 高知県 | 666,000 | 242,000 | 3,380 | 36.3% | 5.08‰ |
| 40 | 福岡県 | 5,103,000 | 1,452,000 | 33,942 | 28.5% | 6.65‰ |
| 41 | 佐賀県 | 795,000 | 252,000 | 5,144 | 31.7% | 6.47‰ |
| 42 | 長崎県 | 1,267,000 | 435,000 | 7,656 | 34.3% | 6.04‰ |
| 43 | 熊本県 | 1,709,000 | 552,000 | 11,189 | 32.3% | 6.55‰ |
| 44 | 大分県 | 1,096,000 | 375,000 | 6,259 | 34.2% | 5.71‰ |
| 45 | 宮崎県 | 1,042,000 | 351,000 | 6,502 | 33.7% | 6.24‰ |
| 46 | 鹿児島県 | 1,549,000 | 524,000 | 9,868 | 33.8% | 6.37‰ |
| 47 | 沖縄県 | 1,468,000 | 350,000 | 12,549 | 23.8% | 8.55‰ |
| 全国合計 | 124,353,000 | 36,229,000 | 727,269 | 29.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)) |
📤 実行結果:
💬 結果の読み方:VIEW は実体を持たず毎回再計算される。 重い場合は CREATE MATERIALIZED VIEW(PostgreSQL)にして REFRESH MATERIALIZED VIEW を夜間バッチで実行。 沖縄県の 5 年平均出生率 9.62‰ は全国 5.85‰ の1.6 倍。
| # | 構文・関数 | 用途 |
|---|---|---|
| ①01 | CREATE TABLE pref(...) | 都道府県マスタを作成 |
| ①02 | CREATE TABLE IF NOT EXISTS ... | 既存があればスキップ |
| ①03 | DROP TABLE IF EXISTS ... | 再実行可能スクリプト |
| ①04 | ALTER TABLE pref ADD COLUMN region TEXT | 列追加 |
| ①05 | ALTER TABLE pref DROP COLUMN tmp | 列削除 |
| ①06 | ALTER TABLE pref RENAME COLUMN x TO y | 列名変更 |
| ①07 | ALTER TABLE pref ALTER COLUMN x TYPE BIGINT | 型変更 |
| ①08 | ALTER TABLE pref ALTER COLUMN x SET NOT NULL | 制約追加 |
| ①09 | ALTER TABLE pref ADD CONSTRAINT chk_pos CHECK(pop>0) | CHECK 追加 |
| ①10 | ALTER TABLE pref ADD CONSTRAINT fk_year FOREIGN KEY(year) REFERENCES dim_year(y) | FK 追加 |
| ②11 | TRUNCATE TABLE pref RESTART IDENTITY | 全行削除+連番リセット |
| ②12 | TRUNCATE TABLE pref CASCADE | 依存も連鎖削除 |
| ②13 | CREATE INDEX idx_pop ON pref(pop) | B-tree インデックス |
| ②14 | CREATE UNIQUE INDEX idx_name ON pref(name) | 重複防止 |
| ②15 | CREATE INDEX idx_p ON pref USING GIST (geom) | PostgreSQL 空間インデックス |
| ②16 | CREATE INDEX CONCURRENTLY idx_x ON pref(x) | 本番無停止インデックス |
| ②17 | DROP INDEX idx_pop | インデックス削除 |
| ②18 | REINDEX TABLE pref | 断片化解消 |
| ②19 | VACUUM ANALYZE pref | 統計情報更新 |
| ②20 | CLUSTER pref USING idx_pop | 物理並べ替え |
| ③21 | CREATE VIEW v_top10 AS SELECT ... | 仮想テーブル |
| ③22 | CREATE MATERIALIZED VIEW mv_x AS ... | 物理化 |
| ③23 | REFRESH MATERIALIZED VIEW mv_x | 更新 |
| ③24 | CREATE SEQUENCE seq_x START 1 | 連番生成器 |
| ③25 | CREATE TYPE region AS ENUM('東北','関東',...) | 列挙型 |
| ③26 | CREATE FUNCTION calc_rate(p int, b int) RETURNS REAL AS $$ ... $$ | ユーザー定義関数 |
| ③27 | CREATE TRIGGER trg_audit ON pref ... | 監査トリガ |
| ③28 | CREATE SCHEMA staging | 名前空間分離 |
| ③29 | ALTER TABLE pref SET SCHEMA staging | スキーマ移動 |
| ③30 | COMMENT ON COLUMN pref.pop IS '人口総数' | メタ情報付与 |
ALTER TABLE ADD CONSTRAINT。 違反行がある場合は事前にクリーニング。CREATE OR ALTER 構文は標準?DDL はデータ基盤の土台。 一度設計を誤ると後から治すコストが膨大になる。 まず SSDSE-B-2026 のような小さな実データで 3NF とスタースキーマを比較し、 自分なりの設計感覚を掴むのが学習の近道。
本ページは data/raw/SSDSE-B-2026.csv の実値計算に基づいており、 合成データは一切含まない。 演習問題・FAQ・クックブックを順に読み、 手を動かしながら自分の用途に翻訳することを推奨する。
DDL は単に CREATE TABLE と ALTER 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_code、 year_id)を先頭に置き、 計測値(population、 elderly)を中盤、 派生列(elderly_rate、 updated_at)を末尾に置くという慣習を守るだけで、 後から SELECT * を眺める人が「これは何のテーブルか」を即座に把握できる。 SSDSE-B-2026 のように 200 を超える指標を扱う場合、 命名規則の統一は実務効率に直結する。
同じデータに対しても、 用途と規模に応じて 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 段階: スタースキーマ | 数千万行以上 | ファクト + Dimension | BI ツール向け。 fact_pop を中心に dim_pref、 dim_year、 dim_region をぶら下げ、 JOIN 1 ホップで全分析が回るように整える |
この 3 段階は 不可逆 ではない。 学習段階では第 1 段階で素早く感覚を掴み、 課題が大きくなれば第 2 段階に移し、 BI ダッシュボードを作る段になれば第 3 段階に展開する、 という順序が現実的である。 重要なのは、 段階を上がる際に必ず DDL を書き換え、 マイグレーションスクリプト として履歴を残す習慣を作ることだ。
DDL の真価は 制約 にある。 制約を書かないと、 一見動くテーブルでも、 ある日突然 NULL や負の値が混入して分析結果が壊れる。 SSDSE-B-2026 のような公式統計データを取り扱う場合、 最低でも以下の 6 種類の制約を意識的に書き分けたい。
| 制約種別 | キーワード | SSDSE-B-2026 での具体例 | 違反時の挙動 |
|---|---|---|---|
| NOT NULL | NOT NULL | 都道府県コード、 年、 人口は欠損不可 | INSERT / UPDATE が即時失敗 |
| 主キー | PRIMARY KEY | (pref_code, year_id) の複合主キー | 重複 INSERT が失敗 |
| 外部キー | FOREIGN KEY | fact_pop.pref_code → pref.pref_code | マスタにないコードを拒否 |
| UNIQUE | UNIQUE | 都道府県名の重複なし | 同名 INSERT が失敗 |
| CHECK | CHECK(...) | population >= 0、 elderly <= population | 業務ルール違反を拒否 |
| DEFAULT | DEFAULT ... | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP | 未指定時に自動補完 |
たとえば CHECK(elderly <= population) は、 高齢者人口が総人口を超えるという論理的にあり得ない値を弾く。 SSDSE-B-2026 を素直に読み込んだ場合は当然満たされるが、 別ソースから手動で取り込んだデータをマージするときに桁ずれを検知できる。 この種の 業務ルールをスキーマに埋め込む 姿勢が、 長期間運用される分析基盤の信頼性を生む。
下図は SSDSE-B-2026 を第 3 段階(スタースキーマ)に展開した例である。 中央のファクトテーブル fact_pop を取り囲む形で 3 つのディメンションテーブルが配置されており、 BI ツールから 1 ホップ JOIN で全分析が可能になっている。 散布図でも同じ構造の発想が現れる: 中心となる量 (人口) と 属性 (都道府県・年・地域) を分離する。

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

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

出典: SSDSE-B-2026 から計算した地域ブロック別高齢化率
本番環境で DDL を走らせるのは常に 緊張を伴う作業 である。 一瞬で完了する CREATE TABLE ですら、 名前空間の競合や権限の不整合があれば失敗する。 ここでは実務で繰り返し問題になる 8 つのシナリオと、 それぞれの安全策をまとめる。
| シナリオ | DDL 命令 | 本番でのリスク | 安全策 |
|---|---|---|---|
| 新規テーブル追加 | CREATE TABLE | 名前競合、 権限なし | IF NOT EXISTS と専用スキーマ |
| 列追加 | ALTER TABLE ... ADD COLUMN | DEFAULT 計算で全行書き換え | PostgreSQL 11+ ならメタデータのみ、 古い版は DEFAULT NULL で追加し後で UPDATE |
| 列型変更 | ALTER COLUMN TYPE | 全行再書き込みで長時間ロック | 新列追加 → コピー → 旧列 DROP のオンラインパターン |
| NOT NULL 化 | ALTER COLUMN SET NOT NULL | 全行スキャン | 事前 CHECK 制約で検証 → SET NOT NULL は瞬時 |
| 主キー追加 | ALTER TABLE ADD PRIMARY KEY | UNIQUE 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 TABLE と ALTER COLUMN TYPE は本番障害の代表的な原因で、 多くの組織が承認フローを設けている。 SSDSE-B-2026 のような研究用途のデータでも、 演習用の DDL スクリプトには BEGIN; ... ROLLBACK; で囲んで実行確認する習慣を身につけたい。
DDL を Git 管理し、 環境間で同じ順序で適用する仕組みが マイグレーションツール である。 主要ツールを以下に比較する。 SSDSE-B-2026 を題材に小規模スキーマを管理する場合は、 まず Alembic で雰囲気を掴み、 チーム開発に展開する段階で Flyway に切り替えるのが穏当だ。
| ツール | 言語 | 記述形式 | 強み | 弱み |
|---|---|---|---|---|
| Flyway | Java/CLI | SQL ファイル直接 | SQL に近く DBA も読みやすい | 動的な分岐が書きにくい |
| Liquibase | Java/CLI | XML / YAML / SQL | DB ベンダー差を吸収 | 記述が冗長 |
| Alembic | Python | SQLAlchemy DSL | 自動生成、 Python 統合 | ORM 知識が前提 |
| Rails Migration | Ruby | Ruby DSL | Rails と完全統合 | Rails 外で使いにくい |
| Prisma Migrate | TypeScript | Prisma Schema | 型安全 + 自動生成 | カスタム DDL が窮屈 |
| sqitch | Perl/CLI | SQL + 依存グラフ | ロールバック容易 | 採用例が少なく学習コスト |
どのツールを選んでも、 共通の 運用原則 は変わらない: (1) 1 つのマイグレーションは 1 つの目的に絞る、 (2) 適用順序は厳密に管理する、 (3) ロールバックスクリプトをセットで書く、 (4) ステージング環境で本番と同サイズのデータで予行する。 この 4 原則を守れば、 ツール選択は二次的な問題になる。
データ型の選択は DDL のもっとも重要かつ後戻りしにくい意思決定である。 ここでは SSDSE-B-2026 の代表的な指標について、 推奨型と理由を網羅的に整理する。 学習者は自分のテーブルを作るときに「この列はどの型にすべきか」を 5 秒で判断できるようになることを目指したい。
| SSDSE-B-2026 指標 | 典型値レンジ | 推奨型 | 理由 |
|---|---|---|---|
| 都道府県コード (pref_code) | R01000〜R47000 | CHAR(6) | 固定長 6 文字。 数値扱いすると先頭の 0 が落ちる |
| 年 (year) | 2012〜2023 | SMALLINT | 16bit で十分。 INTEGER は無駄 |
| 総人口 (population) | 54 万〜1408 万 | BIGINT | 全国集計が 1.2 億超え → INTEGER で危険 |
| 高齢者人口 (elderly) | 0〜500 万 | BIGINT | population に揃える。 一貫性優先 |
| 高齢化率 (elderly_rate) | 0.1〜0.4 | DECIMAL(4,3) | FLOAT は誤差。 比率は固定小数点が安心 |
| 名目 GDP (gdp) | 1 兆〜120 兆円 | BIGINT (単位: 千円) | 千円単位なら BIGINT で安全 |
| 出生率 (birth_rate_permille) | 5.0〜12.0 | DECIMAL(5,2) | 千分率の小数 2 桁で十分 |
| 更新日時 (updated_at) | — | TIMESTAMPTZ | タイムゾーン情報を必ず保持 |
| 取り込みフラグ (is_active) | true/false | BOOLEAN | TINYINT(1) は SQL Server で UI 表示が悪い |
| 備考 (note) | 0〜5000 文字 | TEXT | VARCHAR(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 での使用例 |
|---|---|---|---|
| SMALLINT | 2 | -32,768 〜 32,767 | 年 (2012-2023)、 都道府県順位 |
| INTEGER | 4 | -21 億 〜 21 億 | 市町村人口、 世帯数 |
| BIGINT | 8 | -9.2×10^18 〜 9.2×10^18 | 都道府県人口、 GDP (千円単位) |
| DECIMAL(p,s) | 可変 | 精度 p 桁、 小数 s 桁 | 高齢化率、 出生率、 金額 |
| REAL | 4 | 約 7 桁精度 | 機械学習の特徴量 (誤差許容) |
| DOUBLE | 8 | 約 15 桁精度 | 確率分布パラメータ |
判断の指針: 「集計後の最大値が int の上限を超えそうなら BIGINT」「比率や金額は DECIMAL」「機械学習の特徴量だけ FLOAT/DOUBLE」。 この 3 行を覚えておけば、 95% の場面で正しい選択ができる。
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50))ALTER TABLE users ADD COLUMN age INTDROP TABLE usersALTER TABLE users RENAME TO membersSSDSE-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 レコードあたりのバイト数を計算する。
| 列 | 型 | サイズ [byte] |
|---|---|---|
| id | BIGINT | 8 |
| name | VARCHAR(50) | 52 |
| age | INT | 4 |
| balance | DECIMAL(10,2) | 8 |
| created_at | TIMESTAMP | 8 |
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") |
💬 手計算 (Step 2) 80 byte / 781 KB と 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) を併用すれば多くが回避できます。
DROP TABLE は即時確定。 本番では 必ずバックアップ。※ 上記は文献調査・現場経験で報告される頻度の高い注意点。 ドメインや手法のバージョンによって追加の落とし穴がある場合があります。
すでに本ページ前半で代表的な落とし穴を扱ったが、 ここでは 実務で実際に起きた事例 に基づく追加の 10 件をまとめる。 これらは現場の運用者が「もっと早く知っていれば」と語るものばかりで、 DDL を書く前のチェックリストとして活用してほしい。
CREATE TABLE Pref(...) を書いても、 引用符の有無や DBMS によって取得結果が変わる。 ベストプラクティスは すべて小文字スネークケースで書き、 引用符は使わないorder、 group、 user など、 SQL 予約語を列名に使うと SELECT order FROM ... で構文エラー。 必ず引用符が必要になる。 order → order_no、 user → user_id のように接尾辞を付けるだけで回避可能TIMESTAMP はタイムゾーンを保持しない。 アプリ側が UTC で書いて DB が JST で読むとずれる。 必ず TIMESTAMPTZ を使い、 サーバーは UTC で動かすON DELETE CASCADE は便利だが、 大規模テーブルで使うと 1 件削除が数百万件の連鎖削除を起こす。 業務的に必要な場合のみ使用し、 デフォルトは RESTRICTDEFAULT CURRENT_TIMESTAMP は標準だが、 DEFAULT random() のような 非決定的関数 は再現性を損なう。 デフォルトは決定的な値か、 SQL 標準の関数のみR01100 が「北海道」を意味することを COMMENT ON COLUMN ... IS '都道府県コード (R + 5 桁数字)' と明記PARTITION BY RANGE(year_id) を入れるis_deleted フラグを各クエリで WHERE is_deleted = false として書くのは抜け漏れの温床。 VIEW で隠蔽するか、 RLS で強制する本ページの理解を確認するための拡張演習を 6 題用意した。 いずれも SSDSE-B-2026 を題材としており、 SQLite だけで全て解ける。 解答例は別パッチで配布予定だが、 まずは自力で書いてみてほしい。
fact_pop テーブルに、 「人口は正の整数」「年は 2010〜2025 の範囲」「都道府県名は NULL 不可」の制約を入れた CREATE TABLE 文を書けfact_pop に「世帯数」列を後付けで追加せよ。 既存行のデフォルトは NULL とするelderly / population で計算する 生成列 を追加せよ。 DECIMAL(4,3) で保持し、 0 〜 0.5 の CHECK 制約を入れるpref と人口ファクト fact_pop を分割し、 適切な FK を張った 3NF スキーマに変換するマイグレーションスクリプトを書け各問の難易度は問 1 が初級、 問 6 が上級である。 問 5 まで自力で書ければ、 実務で DDL を任されても困らないレベルと言える。 解答は EXPLAIN ANALYZE で性能まで確認 する習慣を身につけると、 実務での即戦力になる。
| 観点 | 満点 | 確認ポイント |
|---|---|---|
| 構文の正確性 | 20 | DBMS で実行してエラーが出ない |
| 制約の網羅 | 20 | NOT NULL、 CHECK、 FK が適切 |
| 命名の一貫性 | 15 | スネークケース、 単数/複数の統一 |
| 型選択の妥当性 | 15 | BIGINT / DECIMAL の使い分け |
| マイグレーション性 | 15 | ロールバック可能、 段階的適用 |
| 性能配慮 | 15 | 必要な INDEX、 不要な INDEX なし |
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 式は列値を対象にした条件(例: > 0、 BETWEEN 2010 AND 2025、 IN ('A','B')、 LENGTH >= 2)。 FK は 親テーブル(列) 形式(例: pref(code))。
セルを空欄にすると NULL 扱い。 「検証」を押すと制約違反セルが赤くなり、 下に理由が並びます。
🎯 直感:DDL は「データの器を設計する」作業です。 CREATE で器を作り、 型と制約でその器に入れてよい形を宣言します。 制約は「壊れたデータを DB が入口で門前払いする防波堤」。 上のビルダーで PK を外すと重複行が通り、 NOT NULL を外すと空欄が通ることを体感してください。
⚠️ 落とし穴:型選択ミス(人口を TEXT にすると "100" < "9" が真になり並び替えが壊れる)、 制約不足(CHECK や NOT NULL を省くとゴミデータが蓄積し、 後工程の集計が静かに狂う)、 後からの変更コスト(本番テーブルへ NOT NULL を後付けすると既存 NULL 行と衝突し、 巨大テーブルの ALTER は長時間ロックを招く)。 器は作る前に設計するのが最も安い。
関連概念を視覚的に整理した概念マップ。
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(データ定義言語)」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。
TEXT/VARCHAR。 先頭 0 が意味を持つ番号を INTEGER にすると 0 が消える。 金額や比率は誤差を嫌うなら DECIMAL、 実数計算なら REAL。PRIMARY KEY にする。 一意にならない設計のまま進めると、 結合で行数が膨らむ。NULL を許し、 必ず値が入るべき列には NOT NULL を付ける。 後から付けるとデータ移行が要るので、 最初に決める。DDL は一度書くと変更コストが高い。 SSDSE のように毎年更新されるデータでは、 「年が増えても DDL を変えなくてよい形」を先に選んでおく。
DDL(データ定義言語) は「データエンジニアリング」分野の中で発展してきた概念・手法です。 学術的には継続的な研究で精緻化され、 実務的にはツール・ライブラリの普及で誰でも使えるようになってきました。 用語の使い方・意味は時代と分野で少しずつ変わるため、 文脈に応じた解釈が大切です。 入門書だけでなく、 標準的な教科書(例:データサイエンス・統計学の定本)や信頼できるオンライン教材も併用すると、 ぶれない理解に近づけます。
「DDL(データ定義言語)」 はこのページで詳しく扱った概念です。 持ち帰ってほしい 3 つの要点:
CREATE(作成)、 ALTER(変更)、 DROP(削除)、 TRUNCATE(全削除)。さらに学ぶには、 関連用語 や 関連グループ教材 を参照してください。 各用語ページを縦断的に読むことで、 体系的な理解が育ちます。
DROP TABLE は即時確定。 本番では 必ずバックアップ。CREATE TABLE Pref(...) を書いても、 引用符の有無や DBMS によって取得結果が変わる。 ベストプラクティスは すべて小文字スネークケースで書き、 引用符は使わないorder、 group、 user など、 SQL 予約語を列名に使うと SELECT order FROM ... で構文エラー。 必ず引用符が必要になる。 order → order_no、 user → user_id のように接尾辞を付けるだけで回避可能TIMESTAMP はタイムゾーンを保持しない。 アプリ側が UTC で書いて DB が JST で読むとずれる。 必ず TIMESTAMPTZ を使い、 サーバーは UTC で動かすON DELETE CASCADE は便利だが、 大規模テーブルで使うと 1 件削除が数百万件の連鎖削除を起こす。 業務的に必要な場合のみ使用し、 デフォルトは RESTRICTDEFAULT CURRENT_TIMESTAMP は標準だが、 DEFAULT random() のような 非決定的関数 は再現性を損なう。 デフォルトは決定的な値か、 SQL 標準の関数のみR01100 が「北海道」を意味することを COMMENT ON COLUMN ... IS '都道府県コード (R + 5 桁数字)' と明記PARTITION BY RANGE(year_id) を入れるis_deleted フラグを各クエリで WHERE is_deleted = false として書くのは抜け漏れの温床。 VIEW で隠蔽するか、 RLS で強制するDROP TABLE は即時確定。 本番では 必ずバックアップ。CREATE TABLE Pref(...) を書いても、 引用符の有無や DBMS によって取得結果が変わる。 ベストプラクティスは すべて小文字スネークケースで書き、 引用符は使わないorder、 group、 user など、 SQL 予約語を列名に使うと SELECT order FROM ... で構文エラー。 必ず引用符が必要になる。 order → order_no、 user → user_id のように接尾辞を付けるだけで回避可能TIMESTAMP はタイムゾーンを保持しない。 アプリ側が UTC で書いて DB が JST で読むとずれる。 必ず TIMESTAMPTZ を使い、 サーバーは UTC で動かすON DELETE CASCADE は便利だが、 大規模テーブルで使うと 1 件削除が数百万件の連鎖削除を起こす。 業務的に必要な場合のみ使用し、 デフォルトは RESTRICTDEFAULT CURRENT_TIMESTAMP は標準だが、 DEFAULT random() のような 非決定的関数 は再現性を損なう。 デフォルトは決定的な値か、 SQL 標準の関数のみR01100 が「北海道」を意味することを COMMENT ON COLUMN ... IS '都道府県コード (R + 5 桁数字)' と明記PARTITION BY RANGE(year_id) を入れるis_deleted フラグを各クエリで WHERE is_deleted = false として書くのは抜け漏れの温床。 VIEW で隠蔽するか、 RLS で強制するfact_pop テーブルに、 「人口は正の整数」「年は 2010〜2025 の範囲」「都道府県名は NULL 不可」の制約を入れた CREATE TABLE 文を書けfact_pop に「世帯数」列を後付けで追加せよ。 既存行のデフォルトは NULL とするelderly / population で計算する 生成列 を追加せよ。 DECIMAL(4,3) で保持し、 0 〜 0.5 の CHECK 制約を入れるpref と人口ファクト fact_pop を分割し、 適切な FK を張った 3NF スキーマに変換するマイグレーションスクリプトを書け| 観点 | 満点 | 確認ポイント |
|---|---|---|
| 構文の正確性 | 20 | DBMS で実行してエラーが出ない |
| 制約の網羅 | 20 | NOT NULL、 CHECK、 FK が適切 |
| 命名の一貫性 | 15 | スネークケース、 単数/複数の統一 |
| 型選択の妥当性 | 15 | BIGINT / DECIMAL の使い分け |
| マイグレーション性 | 15 | ロールバック可能、 段階的適用 |
| 性能配慮 | 15 | 必要な INDEX、 不要な INDEX なし |
本文では DDL の文法(CREATE / ALTER / DROP)と運用の注意を扱った。 この追補では視点を変え、 「手元の実データを先に観察してから DDL を書く」 という逆算アプローチを掘り下げる。 型・制約は勘で決めるものではなく、 データの実測レンジから導出できる。
設計書やコメントは読まれなければ効力がないが、 DDL に書いた制約は 機械が毎回強制する契約 になる。 「人口は正の整数のはず」と README に書くより、 CHECK (population > 0) と 1 行書く方が確実だ。 では型やレンジはどう決めるか。 SSDSE-B-2026(2012〜2023 年、 564 行 × 112 列)の 2023 年・47 都道府県分を実測すると:
DECIMAL(4,3) + CHECK (aging_rate > 0 AND aging_rate < 1) が実測レンジと整合する。R13000 のように英字を含む固定長文字列。 数値に見える識別子も、 演算対象でなければ TEXT(CHAR) にするのが原則(先頭ゼロ・記号の保持)。つまり CREATE TABLE の 1 行 1 行は、 「このデータは何者か」を df.describe() で確かめた結果の写しであるべき、 というのがこの追補の直感である。
教材やノートブックで多用される 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 点である。
PRAGMA table_info(...)、 PostgreSQL 等なら information_schema.columns で SELECT できるデータとして読める。 「実 DB のスキーマ」と「リポジトリの DDL ファイル」を突き合わせるドリフト検知は、 この仕組みの応用。aging_rate REAL GENERATED ALWAYS AS (elderly * 1.0 / population) と DDL 側で定義すれば、 アプリ側の計算漏れ・不一致を構造的に防げる。