🔰

正規化について勉強したことをまとめてみる 〜その1〜

2024/04/22に公開

経緯

最近、達人に学ぶDB設計という本を読みました。
大変読みやすく、仕事中もすぐ手に取れる場所に置くぐらいには気に入っています。

で、せっかく読んだのでアウトプットしたいと思まして、手始めに「正規化」について自分の言葉でまとめてみようと思います。
もし筆が乗ったら他のトピックについても記事にするかもしれません。予定は未定。

対象者

  • DB設計を勉強している、または興味がある人
  • 第1〜3正規形までを勉強したい人(これ以降は、長くなったので別の記事にします)
  • 達人に学ぶDB設計を読んだことがある、または興味がある人

記事の内容

この記事で書くこと

正規化・正規形について、以下のような形でまとめます。

  • 「正規化」とは何か
  • 第1〜3正規形について(順番に章を分けてまとめます)

また、以下はこの記事で扱うキーワードです。


キーワードの定義

  1. 以下のような表のことを「テーブル」と呼ぶ
項目1 項目2 項目3
データA データB データC
  1. ↑のテーブルの縦列または「項目1」のようなデータの項目のことを「カラム」と呼ぶ
  2. ↑のテーブルの横列のことを「レコード」と呼ぶ
  3. ↑のテーブルの「データA」のような値が入るマスを「セル」と呼ぶ

本文

「正規化」「正規形」とは

論理設計において、特に理解を深めるべき概念のことを正規化と呼びます。
正規化を行うことで作られるのが、正規形です。

これらは、RDBでの論理設計をマスターするための重要概念になります。

「論理設計」とは

論理設計とは、データの形式、すなわちテーブルのレイアウトを決める設計のこと。

https://tackblog.net/logical_design/
https://zenn.dev/arsaga/articles/a92601e76a9cad

正規形の定義

正規形を一言で表すと、

データベースで保持するデータの冗長性を排除し、一貫性と効率性を保持するためのデータ形式

となります。

「冗長性」とは、例えば、1つの情報が複数のテーブルに点在しているような状態を指します。
このような状態は、無駄なデータ領域を食ったり更新作業が複雑になったりするため、あまり良しとされていません。

また、こうした冗長なデータは、更新処理のタイムラグによりデータの不整合を誘発したり、そもそもデータの登録が不可能なテーブルを作ってしまったりもします。
これを「非一貫性」と言います。

こうした冗長性や非一貫性の問題を解決するために考案された方法論が、正規化なのです。

正規形には何段階かレベルがあり、一般的に知られているのは第1〜5正規形です。
数字が増えるほど正規化のレベルが上がっていきます。

正規化は常にすべきか

正規化は、原則として第3正規形までは行うべきとされています。

よって、第3正規形まではある程度使い方を理解しておくのが望ましいと思います。

それ以降のレベルの正規化は「高次正規形」と呼ばれ、普段はあまり意識しておく必要がありません。

一方で、データベースが複雑な設計になるような場合には、必要になることもあります。

なので、こちらも名前と定義くらいは頭の片隅に入れておいて、必要そうなパターンに遭遇した時にすぐ気づけるようになっておくと良さそうです。

正規化の利点・欠点

何でもかんでも正規化すればいいのかというと、そう言うわけでもありません。
何事にも、良い方面と悪い方面があるものです。

ということで、以下に正規化の利点と欠点をまとめます。

【利点】

  1. データの冗長性が排除され、更新時の不整合を防止できる
  2. テーブルの持つ意味が明確になり、開発者が理解しやすい

【欠点】

  • 必要なテーブル数が増えるため、SQLで結合を多用することになり、パフォーマンスの低下を招く

利点を活かすためにどんどん正規化をしていくと、同時にパフォーマンスはどんどん低下していきます。

全部きっちり正規化にしようと躍起になるのではなく、パフォーマンスにも目を向けてちょうどいい塩梅を探る必要があるということですね。

第1正規形

この章からは、第1〜5正規形について順番にまとめていきます。

さっそく第1正規形の紹介から始めていきます。

第1正規形が何なのかというと、ずばり、リレーショナルデータベース(RDB)でテーブルを作る際に最低限守らなければならないルールです。

第1正規形が守られていない状態では、テーブルが成り立たなくなると言っても過言ではありません。

それでは、第1正規形の定義を紹介します。

1つのセルの中には1つの値しか含まない

「セル」は、先ほど定義した通り、テーブルの中のデータが入るマスのことです。

文字だけ見てもイメージが湧かないので、具体的なテーブルを使って説明しますね。

以下は、架空の会社に勤める社員に関するテーブルです。
社員ID社員名、そして社員の子供のカラムが存在します。

社員ID 社員名 子供
001 山田 太郎 次郎、三郎、四郎
002 佐藤 花子 桜子、百合子
003 中村 和夫 玲子
004 藤井 春子

社員IDのカラムと社員名のカラムは、全てのレコードのセルが1つだけ値を含んでいます。
一方で、子供のカラムには、複数の値を含むものもあります。

これは、スプレッドシートやエクセルで管理される表ではよく見る形ですよね。
その場合は何ら問題なく利用することが可能なはずです。

しかし、RDBにおいては重大な規則違反をしています。
第1正規形の定義から外れてしまっているのです。

のカラムに注目してください。
セルの一部に複数の値を含んでいるレコードがありますね。

ここを第1正規形に則った形で修正してあげる必要があります。

以下、修正例です。

例1)に含む必要がある値の数だけカラムを増やす

社員ID 社員名 子1 子2 子3
001 山田 太郎 次郎 三郎 四郎
002 佐藤 花子 桜子 百合子
003 中村 和夫 優子
004 藤井 春子

例2)に含む必要がある値の数だけレコードを増やす

社員ID 社員名
001 山田 太郎 次郎
001 山田 太郎 三郎
001 山田 太郎 四郎
002 佐藤 花子 桜子
002 佐藤 花子 百合子
003 中村 和夫 玲子
004 藤井 春子

どちらも全てのセルが1つだけ値を含んでいるため、第1正規形に則った形になりました。

このように、1つのセルに1つだけの値が含まれているとき、この値のことを「スカラ値(scalar value)」と呼んだりもします。

第1正規形の問題を解決する

ところで、例2のテーブルですが、2つほど致命的な問題を含んでおり、実用的ではありません。

何が問題になるのか、以下のセクションで説明していきます。


主キーを決められない

言葉だけ見ても分かりにくいので具体例を出します。

例2のテーブルで1つのレコードを特定しようとすると、{社員ID, 社員名, }の3カラム(つまりテーブル内の全カラム)を指定せざるを得ないと思います。

例えば、「山田次郎」さんを特定しようとすると、{社員ID=001, 社員名=山田 太郎, 子=次郎}のように指定する必要がありますね。

この、レコードを特定するために指定されるカラムのことを、主キーと呼びます。

全てのレコードが上記の例のように指定できるのなら問題ないのですが、例2のテーブルに登録されたデータを見る限りそうもいかなさそうです。

社員ID=004の藤井さんがを持っていないのがわかるでしょうか。
この場合、のカラムはNULLになります。

しかし、主キーは定義上、その一部のカラムであってもNULLにすることは許されません
ここで矛盾が生じます。

テーブルの意味やレコードの単位をすぐに理解できない

これはテーブルの中に「社員」と「扶養者」という2つのエンティティの情報が含まれているのが問題です。

これでは、テーブルの意味やレコードの単位がすぐには分かりません。
テーブルは、一目で意味を特定できることが重要です。


以上が、例2のテーブルが有する重大な問題点です。

これらを解決するためには、以下のようにテーブルを分割することが必要になります。

例3)「社員」テーブルと「扶養者」テーブルに分割する

  • 社員

    社員ID 社員名
    001 山田 太郎
    002 佐藤 花子
    003 中村 和夫
    004 藤井 春子
  • 扶養者

    社員ID
    001 次郎
    001 三郎
    001 四郎
    002 桜子
    002 百合子
    003 玲子

こうすることで、キーがnullになることがなくなります。

更に、テーブル内のエンティティが1つになったため、テーブルの意味が分かりやすくなりました。
テーブルの意味が分かりやすくなると、名前も付けやすくなってますます整理がしやすくなりますね。

関数従属性

ここまで、テーブルがスカラ値のみになるよう設計することの重要性を説明してきました。

これは正規形全体を理解するための鍵となるもので、関数従属性と呼ばれています。

「関数」とは、中学校で習うY=f(X)と同じものを指します。

この関数は、「Xに対するYは1つだけである」ことを示しており、
これを「YはXに従属する」と表現するのです。

RDBでは、このXとYの関係を以下のように表します。

{X} -> {Y}

そして、正規化とは、テーブル内の全ての列が関数従属性を満たすように整理していくことなのです。

例3の社員テーブルの場合、関数従属性は以下のように表すことができます。

{社員ID} -> {社員名}

扶養者テーブルの場合は社員IDの2つで主キーになりますが、主キーは主キーに従属するため、これも関数従属性を満たせているのです。

{社員ID, 子} -> {社員ID}
{社員ID, 子} -> {子}

第2正規形

続いて、第2正規形の話に進みます。

ここでも第1正規形で例にした社員テーブルを使います。
カラムはいくつか増やしています。

  • 社員
    会社コード 会社名 社員ID 社員名 年齢 部署コード 部署名
    c-1 A商事 001 山田 太郎 50 d01 営業
    c-2 B工業 002 佐藤 花子 37 d02 人事
    c-2 B工業 003 中村 和夫 23 d03 開発
    c-1 A商事 004 藤井 春子 41 d04 総務

このテーブルは第1正規形の定義を満たしています。
カラムは全てスカラ値になっており、第1正規形の定義に違反している部分はありません。

また、主キーは{会社コード, 社員ID}です。

それでは、この第1正規形のテーブルを第2正規形にしていきます。

以下は、第2正規形の定義です。

テーブルが"完全関数従属"のみで構成されている

第1正規形の章で「関数従属性」について説明しましたが、ここでは「完全関数従属」というワードが新たに登場しています。

ということで、先にこの新しいワードについて説明していきます。

部分関数従属と完全関数従属

関数従属性について話すので、まずは社員テーブルの関数従属性を洗い出してみましょう。

{会社コード, 社員ID} -> {会社コード}
{会社コード} -> {会社名}
{会社コード, 社員ID} -> {社員ID}
{会社コード, 社員ID} -> {社員名}
{会社コード, 社員ID} -> {年齢}
{会社コード, 社員ID} -> {部署コード}
{会社コード, 社員ID} -> {部署名}

こんな感じになるかと思います。

さて、よく見ると、この中に1つだけ{会社コード}のみに従属しているカラムがありますね。

{会社コード} -> {会社名}

この会社名カラムのように「主キーの一部に対して従属する」ことを部分関数従属と呼びます。

これに対して「主キーを構成する全てのカラムに従属性がある」ことを、完全関数従属と呼びます。
社員テーブルの場合、会社名カラム以外は全て完全関数従属ですね。

{会社コード, 社員ID} -> {会社コード}  など


さて、第2正規形の定義は「テーブルが"完全関数従属"のみで構成されている」というものでした。

ということは、部分関数従属を含む社員テーブルは、このままでは第2正規形になれません。
テーブル内の部分関数従属を全て解消し、完全関数従属のみにする必要があります。

それでは、実際に部分関数従属を解消して第2正規形のテーブルを作ってみましょう。

やることとしては、第1正規形の時と同様にテーブルの分割を行います。

具体的には、会社コード社員IDに従属するグループと、会社コードにのみ従属するグループで分けます。

  • 社員

    会社コード 社員ID 社員名 年齢 部署コード 部署名
    c-1 001 山田 太郎 50 d01 営業
    c-2 002 佐藤 花子 37 d02 人事
    c-2 003 中村 和夫 23 d03 開発
    c-1 004 藤井 春子 41 d04 総務
  • 会社

    会社コード 会社名
    c-1 A商事
    c-2 B工業
    c-2 B工業
    c-1 A商事

こんな感じで、社員テーブルと会社テーブルの2つに分割しました。

これで、全てのカラムが主キーに完全関数従属するようになっていることがわかると思います。
第2正規形のテーブルの完成です。

さて、ここまで淡々と第2正規形の定義や具体例をまとめてきましたが、第2正規形にしなかったらどうなるのでしょうか。

例えば、社員テーブルに「新しい会社名」を追加したい場合を考えてみてください。

会社名を追加するということは、レコードが追加されるということです。
そして、レコードが追加されるということは、会社名以外のカラムの値も必要になってくるということです。

もし会社名以外の情報が全くないと、以下のような状態になってしまいレコードを追加できません。
これでは困りますね。

会社コード 会社名 社員ID 社員名 年齢 部署コード 部署名
c-1 A商事 001 山田 太郎 50 d01 営業
c-2 B工業 002 佐藤 花子 37 d02 人事
c-2 B工業 003 中村 和夫 23 d03 開発
c-1 A商事 004 藤井 春子 41 d04 総務
c-3 C重工 ??? ??? ??? ??? ???


また、このテーブルでは会社名が一意にならない可能性もあります。

上から数えて1つ目と4つ目のレコードは、会社コードと会社名が同じです。
しかし、このテーブルだと、以下のようにそれぞれ別の会社名を登録することも可能になってしまっているのです。

会社コード 会社名 社員ID 社員名 年齢 部署コード 部署名
c-1 A商事 001 山田 太郎 50 d01 営業
c-1 A商社 004 藤井 春子 41 d04 総務

現時点ではレコードの数も少ないため目視で修正できそうですが、これが100,200と増えていったら人間の力だけで管理するのは難しいはずです。

第2正規形のテーブルでは、このような不都合は起こり得ません。

新しい会社が追加された時には会社テーブルのレコードのみ増やせばいいし、
会社名は一度しか登録されないため同じ会社を表す異なる名前が存在することもあり得ません。

これは、冗長性を解消したことによるメリットです。

このような観点から、第2正規形は、「会社」と「社員」という異なるレベルの実態(エンティティ)をテーブルとしても分離してあげる作業として見ることもできますね。

無損失分解

この章では、第2正規形を得るためにはテーブルの分割が必要ということを説明し、実際に社員テーブルを2つに分割してきました。

それに加えて、第2正規形には、必ず正規化する前の状態に戻すことができるという特徴があります。

「正規化する前の状態に戻す」とはどういうことかと言うと、テーブルを分割する前と後で失われる情報がないということです。

この特徴を、可逆的と呼びます。

第2正規形は可逆的です。

分割した社員テーブルと会社テーブルは、必ず元の社員テーブルに戻すことが可能です。

このように、情報を完全に保持したままで正規化することを、無損失分解と呼びます。

第3正規形

続いて、第3正規形についてです。

第2正規形では、異なるレベルの実態をテーブルとしても分離させることで、データの冗長性を無くすことができました。

しかし、社員テーブルには、まだ別の問題が残っています。

推移的関数従属

それでは、もう一度、社員テーブルと会社テーブルに登場してもらいましょう。

  • 社員

    会社コード 社員ID 社員名 年齢 部署コード 部署名
    c-1 001 山田 太郎 50 d01 営業
    c-2 002 佐藤 花子 37 d02 人事
    c-2 003 中村 和夫 23 d03 開発
    c-1 004 藤井 春子 41 d04 総務
  • 会社

    会社コード 会社名
    c-1 A商事
    c-2 B工業
    c-2 B工業
    c-1 A商事

社員テーブルの部署名のカラムに注目してください。
このカラムは、第2正規形の章で以下のような従属関係にあることを確認しました。

{会社コード, 社員ID} -> {部署名}

一方で、この部署名はもう1つ別のカラムにも従属しています。
部署コードカラムです。

{部署コード} -> {部署名}

さらに、部署コードカラムは会社コードカラムと社員IDカラムに従属しています。

{会社コード, 社員ID} -> {部署コード}



これらをまとめると、次のような従属関係が見えてきます。

{会社コード, 社員ID} -> {部署コード} -> {部署名}

部署コード会社コード社員IDに従属し、部署名部署コードに従属している、という2段階の従属関係になっていますね。

このように、1つのテーブル内にある段階的な従属関係のことを、推移的関数従属と呼びます。

推移的関数従属も、やはり部分関数従属と同様にデータの冗長性を生むことになります。

この推移的関数従属を解消することで、第3正規化を行うことができるのです。

それでは早速、社員テーブルに対して第3正規化を実施してみましょう。

推移的関数従属を解消する必要があるのは社員テーブルの部署名カラムなので、部署コード部署名を別のテーブルに分離させます。

  • 社員

    会社コード 社員ID 社員名 年齢 部署コード
    c-1 001 山田 太郎 50 d01
    c-2 002 佐藤 花子 37 d02
    c-2 003 中村 和夫 23 d03
    c-1 004 藤井 春子 41 d04
  • 会社

    会社コード 会社名
    c-1 A商事
    c-2 B工業
    c-2 B工業
    c-1 A商事
  • 部署

    部署コード 部署名
    d01 営業
    d02 人事
    d03 開発
    d04 総務

こんな感じになるかと思います。

こうすることで、推移的関数従属は解消され、すべてのテーブル内のカラムが完全関数従属となりました。

これで第3正規形は完成です。

第3正規形は無損失分解でもあります。
テーブルを再度結合しても、正規化する前と同じ状態に戻すことができるというやつですね。

エンティティで分離しよう

第2正規形、第3正規形と進めてきて気づいた方もいるかも知れませんが、どうやら1つのテーブル内に複数のエンティティが存在するのがあまり望ましくない状態のようです。

その主な理由は、複数のエンティティが共存することで冗長性のあるデータが発生してしまうからでしたね。

ここまで、長々とあれこれ説明してきましたが、以下のように覚えておくとスッキリしていいかも知れません。

冗長性のあるデータの発生を防ぐために、1つのテーブルには
1つのエンティティだけが存在するように設計すること

最後に

この記事では、達人に学ぶDB設計を参考に、第1〜3正規形についてまとめました。

最後に、記事の中で例にしてきたテーブルを、第1正規形から順に並べてみようと思います!

【正規化前】

社員ID 社員名 子供
001 山田 太郎 次郎、三郎、四郎
002 佐藤 花子 桜子、百合子
003 中村 和夫 玲子
004 藤井 春子


【第1正規形】

社員ID 社員名 子1 子2 子3
001 山田 太郎 次郎 三郎 四郎
002 佐藤 花子 桜子 百合子
003 中村 和夫 優子
004 藤井 春子

または

社員ID 社員名
001 山田 太郎
002 佐藤 花子
003 中村 和夫
004 藤井 春子
社員ID
001 次郎
001 三郎
001 四郎
002 桜子
002 百合子
003 玲子


【第2正規形】

会社コード 社員ID 社員名 年齢 部署コード 部署名
c-1 001 山田 太郎 50 d01 営業
c-2 002 佐藤 花子 37 d02 人事
c-2 003 中村 和夫 23 d03 開発
c-1 004 藤井 春子 41 d04 総務
会社コード 会社名
c-1 A商事
c-2 B工業
c-2 B工業
c-1 A商事


【第3正規形】

会社コード 社員ID 社員名 年齢 部署コード
c-1 001 山田 太郎 50 d01
c-2 002 佐藤 花子 37 d02
c-2 003 中村 和夫 23 d03
c-1 004 藤井 春子 41 d04
会社コード 会社名
c-1 A商事
c-2 B工業
c-2 B工業
c-1 A商事
部署コード 部署名
d01 営業
d02 人事
d03 開発
d04 総務


正規化前のテーブルと比べて、正規化後の方が1つ1つのテーブルがスッキリしていて可読性も上がっているように感じませんか?

説明の中ではテーブルの冗長性やデータの不整合について触れることが多かったと思いますが、実は正規化は人間が見た時の可読性にも影響するものなんです。

次回の記事では、残り3つの正規形を紹介しようと思います。

この記事を読んで正規化に興味を持ってくれた方は、ぜひそちらも読んでいただけると嬉しいです。

また、この記事よりも詳細な説明やコラムが載っている達人に学ぶDB設計もぜひ読んでみてください。

正規化以外にも、データベースのパフォーマンスについてや、バッドノウハウなどもまとめられていて大変参考になる本です。

カラビナテクノロジー デベロッパーブログ

Discussion