正規化(重複を排して更新の矛盾を防ぐ)の使い方
juchu_nama実行前
| juchu_id | shohin_id | shohin_mei |
|---|---|---|
| 1 | 10 | pen |
| 1 | 11 | note |
| 2 | 10 | pen |
| … | ||
juchu_nama実行後
| juchu_id | shohin_id | shohin_mei |
|---|---|---|
| 5 | 10 | pen+ |
| 1 | 10 | pen |
| 1 | 11 | note |
| 2 | 10 | pen |
| … | ||
受注を 1 件足す
- +の行= この文で表に増える行です
- 実行前 8 行 → 実行後 9 行(1 行増えます。元の表そのものが変わります)
受注を 1 件足すたびに、商品名 pen と単価 120 がまた 1 行増えます。商品の情報は 1 か所にあれば足りるのに、受注の数だけ写しが増えていきます。名前を変えるときは、この写しを全部直すことになります。
正規化は「同じことを 2 か所に書かない」ようにする作業です。 同じ値が何行にも散らばっていると、直すときに全部を直さなければならず、1 行でも忘れると食い違いが残ります。しかもデータベースは止めてくれません。⚠️ このページの juchu_nama と kaiin_nama はわざと崩してある表です。手を動かして壊してから、直してください。
この書き方が使えない製品があります そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server
- Oracle Database では使えません(text 型の列) / この問題のほかの構文は未確認です
未確認: IBM Db2
このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。
01同じ値が散らばると、直すのが高くつく
結論: 正規化の目的は、直す場所を 1 か所にすることです。
juchu_nama では商品名 pen が 3 行に書かれています。名前を変えるなら 3 行とも直さなければなりません。行が増えれば直す数も増え、1 行でも忘れると、同じ商品番号なのに名前が違う行が残ります。データベースはそれを止めてくれませんし、あとから見てどちらが正しいのかも分かりません。
この「直し忘れが起こりうる形」を、更新時異常と呼びます。ほかにも次の困りごとが起きます。
- 消すと消えすぎる … その商品を含む受注をすべて消すと、商品の情報まで消える
- 入れられない … まだ 1 件も受注が無い商品を登録できない(受注の行が無いと書く場所が無い)
⚠️ 速度のために「あえて崩す」判断もあります(非正規化)。ただしそれは、崩れていない形を作れる人が、理由を持って選ぶもので、最初から崩れているのとは別の話です。
SELECT shohin_id, shohin_mei, count(*) AS gyosu
FROM juchu_nama
GROUP BY shohin_id, shohin_mei
ORDER BY shohin_id;| shohin_id | shohin_mei | gyosu |
|---|---|---|
| 10 | pen | 3 |
| 11 | note | 2 |
| 12 | eraser | 2 |
| 13 | ruler | 1 |
(4 行)
商品は 4 種類しかないのに、pen は 3 行、note と eraser は 2 行に書かれています。1 か所で足りるものが、受注の数だけ写されています。
UPDATE juchu_nama SET shohin_mei = 'ballpen' WHERE juchu_id = 1 AND shohin_id = 10;
SELECT shohin_id, shohin_mei, count(*) AS gyosu
FROM juchu_nama
WHERE shohin_id = 10
GROUP BY shohin_id, shohin_mei
ORDER BY shohin_mei;| shohin_id | shohin_mei | gyosu |
|---|---|---|
| 10 | ballpen | 1 |
| 10 | pen | 2 |
(2 行)
商品番号 10 なのに ballpen と pen の 2 通りができました。どちらが正しいのか、この表からは分かりません。 エラーも警告も出ていません。
02第 2 正規形: 主キーの一部だけで決まる列を切り出す
結論: 主キーが複数の列でできているとき、その一部だけで決まる列は別の表に移します。
juchu_nama の主キーは (juchu_id, shohin_id) の組です。ところが shohin_mei と tanka は shohin_id だけで決まります。どの受注かに関係なく、商品番号 10 なら名前は pen、単価は 120 です。これを部分関数従属と呼びます。
一方 suryo は (juchu_id, shohin_id) の組で決まります。受注 1 のペンは 3 個、受注 2 のペンは 5 個。組で決まる列は、そのまま置いておきます。
見分け方は「主キーの一部を隠しても値が決まるか」です。 juchu_id を隠しても shohin_mei は決まる——だから切り出す。suryo は決まらない——だから残す。
⚠️ 主キーが 1 列だけの表は、この意味では必ず第 2 正規形を満たします。「一部」が存在しないためです。
できた表 shohin
| 列 | 型 | NULL | 制約 |
|---|---|---|---|
| shohin_id | integer | 不可 | 主キー |
| shohin_mei | text | 不可 | |
| tanka | numeric(10,0) | 不可 |
商品ごとに 1 行だけになります。名前や単価を直すのは、この表の 1 行だけです。受注が何万件あっても変わりません。
CREATE TABLE shohin (
shohin_id int PRIMARY KEY,
shohin_mei text NOT NULL,
tanka numeric(10,0) NOT NULL
);
INSERT INTO shohin (shohin_id, shohin_mei, tanka)
SELECT DISTINCT shohin_id, shohin_mei, tanka FROM juchu_nama;
SELECT shohin_id, shohin_mei, tanka FROM shohin ORDER BY shohin_id;| shohin_id | shohin_mei | tanka |
|---|---|---|
| 10 | pen | 120 |
| 11 | note | 200 |
| 12 | eraser | 80 |
| 13 | ruler | 350 |
(4 行)
8 行あった元の表から、商品は 4 行になりました。DISTINCT を書かないと同じ商品が何行も入り、主キーが重複してエラーになります。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
03⚠️ よくある間違い:第 3 正規形: 主キー以外から決まる列を切り出す
結論: 主キーではない列から決まる列も、別の表に移します。
kaiin_nama の主キーは kaiin_id です。kaiin_mei と pref_code は会員番号で決まるので問題ありません。ところが pref_mei(都道府県名)は pref_code で決まります。kaiin_id → pref_code → pref_mei と 2 段になっているので、推移的関数従属と呼びます。
決まり方が 2 段になっていたら切り出す、と覚えてください。第 2 正規形は「主キーの一部から決まる列」、第 3 正規形は「主キー以外から決まる列」を追い出す作業です。
都道府県コードのような外の世界で決まっている値は、切り出すと特に効きます。コード表として独立させておけば、表記を直すのも 1 か所です。
⚠️ 第 3 正規形まで済ませれば、実務で困る重複はたいてい消えます。さらに上の正規形もありますが、まずここまでを確実にするほうが役に立ちます。
SELECT pref_code, pref_mei, count(*) AS ninzu
FROM kaiin_nama
GROUP BY pref_code, pref_mei
ORDER BY pref_code;| pref_code | pref_mei | ninzu |
|---|---|---|
| 13 | tokyo | 3 |
| 23 | aichi | 1 |
| 27 | osaka | 2 |
(3 行)
都道府県は 3 つしかないのに、tokyo は 3 行、osaka は 2 行に書かれています。表記を直すなら、その行数ぶん直すことになります。
CREATE TABLE todofuken (
pref_code int PRIMARY KEY,
pref_mei text NOT NULL
);
INSERT INTO todofuken (pref_code, pref_mei)
SELECT DISTINCT pref_code, pref_mei FROM kaiin_nama;
SELECT pref_code, pref_mei FROM todofuken ORDER BY pref_code;| pref_code | pref_mei |
|---|---|
| 13 | tokyo |
| 23 | aichi |
| 27 | osaka |
(3 行)
6 人ぶんの行から、都道府県は 3 行になりました。会員の側は pref_code だけを持ち、名前は持ちません。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
04分けても情報は失われない
結論: 正しく分ければ、結合して元どおりの内容を取り出せます。
表を分けると聞くと「情報が減るのでは」と心配になりますが、減りません。分ける前に同じ行へ書いてあった値は、結合で元の組み合わせに戻せます。 元に戻せる分け方を、情報無損失分解と呼びます。
戻せることを確かめる方法は簡単で、分解した表を結合して、元の表と同じ行数・同じ内容が出るかを見ます。行数が増えたら結合の条件が足りず、減ったら対応しない行があるということです。
⚠️ 分けたあとは外部キーを必ず付けてください。分ける前は「同じ行に書いてあるから必ず一致する」ことが保証されていました。分けた時点でその保証は消えるので、外部キーで取り戻します。付けないと、存在しない商品番号の受注が入れられてしまいます。
CREATE TABLE shohin (
shohin_id int PRIMARY KEY,
shohin_mei text NOT NULL,
tanka numeric(10,0) NOT NULL
);
CREATE TABLE juchu (
juchu_id int,
shohin_id int REFERENCES shohin(shohin_id),
suryo int NOT NULL,
PRIMARY KEY (juchu_id, shohin_id)
);
INSERT INTO shohin (shohin_id, shohin_mei, tanka)
SELECT DISTINCT shohin_id, shohin_mei, tanka FROM juchu_nama;
INSERT INTO juchu (juchu_id, shohin_id, suryo)
SELECT juchu_id, shohin_id, suryo FROM juchu_nama;
SELECT j.juchu_id, j.shohin_id, s.shohin_mei, s.tanka, j.suryo
FROM juchu j
JOIN shohin s ON s.shohin_id = j.shohin_id
ORDER BY j.juchu_id, j.shohin_id;| juchu_id | shohin_id | shohin_mei | tanka | suryo |
|---|---|---|---|---|
| 1 | 10 | pen | 120 | 3 |
| 1 | 11 | note | 200 | 2 |
| 2 | 10 | pen | 120 | 5 |
| 2 | 12 | eraser | 80 | 1 |
| 3 | 11 | note | 200 | 4 |
| 3 | 12 | eraser | 80 | 2 |
| 3 | 13 | ruler | 350 | 1 |
| 4 | 10 | pen | 120 | 1 |
(8 行)
元の juchu_nama と同じ 8 行・同じ内容が出ました。分けても情報は失われていません。 違うのは、商品名と単価が 1 か所にしか書かれていないことだけです。
⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表
自分で打ってみる
このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。
押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。
解いてみる
読んだだけでは書けるようになりません。実行結果で採点します。
関連するトピック
- 主キー・外部キー分けた表を結び付ける外部キーの説明はこちらです。
- CREATE TABLE分けた先の表を作る CREATE TABLE そのものの説明です。
- INNER JOIN分けた表を結合して取り出す書き方はこちらです。
- DISTINCT重複を落として切り出すときに使う DISTINCT の説明です。
根拠(一次情報)
- PostgreSQL 18 マニュアル: CREATE TABLE一次情報・確認 2026-09-03
- PostgreSQL 18 マニュアル: 制約(主キー・外部キー)一次情報・確認 2026-09-03
- PostgreSQL 18 マニュアル: INSERT(問い合わせの結果を入れる)一次情報・確認 2026-09-03