コマンド道場

正規化(重複を排して更新の矛盾を防ぐ)の使い方

1 つの名前を変えるのに、何行の更新が要るか

juchu_nama実行前

juchu_idshohin_idshohin_mei
110pen
111note
210pen

juchu_nama実行後

juchu_idshohin_idshohin_mei
510pen
110pen
111note
210pen

受注を 1 件足す

  • +の行= この文で表に増える行です
  • 実行前 8実行後 91増えます元の表そのものが変わります

受注を 1 件足すたびに、商品名 pen と単価 120 がまた 1 行増えます。商品の情報は 1 か所にあれば足りるのに、受注の数だけ写しが増えていきます。名前を変えるときは、この写しを全部直すことになります。

正規化は「同じことを 2 か所に書かない」ようにする作業です。 同じ値が何行にも散らばっていると、直すときに全部を直さなければならず、1 行でも忘れると食い違いが残ります。しかもデータベースは止めてくれません。⚠️ このページの juchu_namakaiin_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_idshohin_meigyosu
10pen3
11note2
12eraser2
13ruler1

4

商品は 4 種類しかないのに、pen は 3 行、noteeraser は 2 行に書かれています。1 か所で足りるものが、受注の数だけ写されています。

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_idshohin_meigyosu
10ballpen1
10pen2

2

商品番号 10 なのに ballpenpen の 2 通りができました。どちらが正しいのか、この表からは分かりません。 エラーも警告も出ていません。

02第 2 正規形: 主キーの一部だけで決まる列を切り出す

結論: 主キーが複数の列でできているとき、その一部だけで決まる列は別の表に移します。

juchu_nama の主キーは (juchu_id, shohin_id) の組です。ところが shohin_meitankashohin_id だけで決まります。どの受注かに関係なく、商品番号 10 なら名前は pen、単価は 120 です。これを部分関数従属と呼びます。

一方 suryo(juchu_id, shohin_id) の組で決まります。受注 1 のペンは 3 個、受注 2 のペンは 5 個。組で決まる列は、そのまま置いておきます。

見分け方は「主キーの一部を隠しても値が決まるか」です。 juchu_id を隠しても shohin_mei は決まる——だから切り出す。suryo は決まらない——だから残す。

⚠️ 主キーが 1 列だけの表は、この意味では必ず第 2 正規形を満たします。「一部」が存在しないためです。

切り出したあとの商品の表

できた表 shohin

NULL制約
shohin_idinteger不可主キー
shohin_meitext不可
tankanumeric(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_idshohin_meitanka
10pen120
11note200
12eraser80
13ruler350

4

8 行あった元の表から、商品は 4 行になりました。DISTINCT を書かないと同じ商品が何行も入り、主キーが重複してエラーになります。

⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表

03⚠️ よくある間違い:第 3 正規形: 主キー以外から決まる列を切り出す

結論: 主キーではない列から決まる列も、別の表に移します。

kaiin_nama の主キーは kaiin_id です。kaiin_meipref_code は会員番号で決まるので問題ありません。ところが pref_mei(都道府県名)は pref_code で決まりますkaiin_idpref_codepref_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_codepref_meininzu
13tokyo3
23aichi1
27osaka2

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_codepref_mei
13tokyo
23aichi
27osaka

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_idshohin_idshohin_meitankasuryo
110pen1203
111note2002
210pen1205
212eraser801
311note2004
312eraser802
313ruler3501
410pen1201

8

元の juchu_nama と同じ 8 行・同じ内容が出ました。分けても情報は失われていません。 違うのは、商品名と単価が 1 か所にしか書かれていないことだけです。

⚠️ この書き方は Oracle Database では使えません(text 型の列)。製品ごとの対応表

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

関連するトピック

  • 主キー・外部キー分けた表を結び付ける外部キーの説明はこちらです。
  • CREATE TABLE分けた先の表を作る CREATE TABLE そのものの説明です。
  • INNER JOIN分けた表を結合して取り出す書き方はこちらです。
  • DISTINCT重複を落として切り出すときに使う DISTINCT の説明です。

根拠(一次情報)