プログラムと美しさ：第八章　正規化　―Excel 表の困ったを解決してみる―

～ プログラミングって面白そうと思った方に向けた連載エッセイ ～

　正規化という概念がある。プログラムの世界では「データベースの正規化」がよく知られている。この正規化とプログラムにおける美しさの概念は、体感的に符合するところが多い。芸術・美術の世界にはない評価軸のようなので、プログラムに特徴的な美的基準と考えてよいだろう。

　データベースの正規化では、第一から第五までの「正規形」が段階的に定義されている。実務では第三正規形までを一応の完成形とすることが多い。

　「データベース」にもいくつか種類があるが、ここでは現在主流の「リレーショナル・データベース」を前提に説明する。「リレーショナル」とは、わかりやすく言うと「行と列の関係（リレーション）たる表で構成されている」という意味である。表同士の関連（リレーションシップ）も定義、操作できるのが一般的である。

　実例で見てみよう。

　Excel のような表計算ソフトを使って売上を管理することにする。表計算ソフトの使用経験がない方は、手書きの台帳をイメージしていただいてもかまわない。

　必要な情報は何だろう。どの商品をいつ、誰が、何個購入したか。複数の商品を同時に購入した場合、それらは一つの伝票で管理したい。

　次のように項目定義してみる。

| 売上伝票番号 | 売上日 | 顧客コード | 顧客氏名 | 顧客住所 | 商品コード | 商品名 | 単価 | 数量 | 合計金額 |
|:------------:|:------:|:----------:|:--------:|:--------:|:----------:|:------:|:----:|:----:|:--------:|
| 1 | 2019/01/01 | C0001 | Nick Kim | 千葉県3番地 | P0001<br />P0002 | サンドバッグ<br />グローブ | 20,000<br />10,000 | 1<br />2 | 40,000 |

　説明を単純化するため、消費税、値引きなどは考慮していない。最低限の定義である。

　さて、実際に帳簿をつけ始めると不都合に感じてくることがある。

　商品コード、商品名、単価、数量は1つのセルに改行区切りで収められている。これでは計算式で簡単に扱うことができない。商品ごとに集計したりできないのも問題である。

　そこで1つのセルには1つの値しか収めないことにする。

| 売上伝票番号 | 売上日 | 顧客コード | 顧客氏名 | 顧客住所 | 商品コード | 商品名 | 単価 | 数量 | 合計金額 |
|:------------:|:------:|:----------:|:--------:|:--------:|:----------:|:------:|:----:|:----:|:--------:|
| 1 | 2019/01/01 | C0001 | Nick Kim | 千葉県3番地 | P0001 | サンドバッグ | 20,000 | 1 | 20,000 |
| 1 | 2019/01/01 | C0001 | Nick Kim | 千葉県3番地 | P0002 | グローブ | 10,000 | 2 | 20,000 |

　項目定義は変わっていないが、行が商品明細単位に分かれた。

　別案として、「商品コード1」「商品名1」…「商品コード2」…のように横に繰り返す方法も考えられる。しかし、明細の数は不定なので扱いづらい。実際にそのような設計（想定される最大数分の列をあらかじめ確保しておく）を見かけることもあるが、好ましい解決策とは言えないだろう。ここでは検討しないことにする。
　
　これで1つのセルに1つの値、という形式的要件は達成された。データとして扱いやすくなる。が、洗練された解決策とは言えまい。実害もある。

　たとえば、三つの商品を同時に購入した場合、売上伝票番号、売上日、顧客氏名、顧客住所は同じなのに三行分別々に登録しなくてはならない。表計算ソフトならコピー＆ペーストも使えるが、無駄ではないか。労力もファイルサイズも。「コピペ」は害悪と耳にしたこともある。

　無駄な繰り返しをなくすため、顧客住所までを別のシートに分離しよう。

**[売上伝票]**

| 売上伝票番号 | 売上日 | 顧客コード | 顧客氏名 | 顧客住所 |
|:------------:|:------:|:----------:|:--------:|:--------:|
| 1 | 2019/01/01 | C0001 | Nick Kim | 千葉県3番地 |

**[売上明細]**

| 売上伝票番号 | 商品コード | 商品名 | 単価 | 数量 |
|:------------:|:----------:|:------:|:----:|:----:|
| 1 | P0001 | サンドバッグ | 20,000 | 1 |
| 1 | P0002 | グローブ | 10,000 | 2 |

　伝票、明細ときれいに階層化された。大きな成果は、売上情報の無駄な繰り返しが一行に集約されたことである。一つの伝票に対して商品ごとの明細があり、それらは売上伝票番号によって関連づけられている。

　合計金額は削除した。商品コードから単価がわかり、数量を掛けることで自動的に算出できるからだ。

　これが「**第一正規形**」である。

　いやすっきりしたと運用を続けていると、あるとき営業担当者から依頼を受ける。

　「この商品、まだ売上立ってないけど先に登録しておいてくれる？」

　「はい！」と応じて帳簿に向かうも、登録する場所がないことに気づく。商品情報は売上明細にある。売上が立たないことには明細は作れない。どうしよう…とデータを眺めていると、変な商品名を見つけた。

　「怖えプロ店員」。よく売れたな。違う。「ホエイプロテイン」だ。直さなきゃ。ほかにもあったらどうしよう。商品名はあちこちの明細に散らばっている。ああ、混乱する。

　でも大丈夫。それらを一気に解決する方法がある。

**[商品情報]**

| 商品コード | 商品名 | 単価 |
|:-:|:-:|:-:|
| P0001 | サンドバッグ | 20,000 |
| P0001 | グローブ | 10,000 |

**[売上伝票]**

| 売上伝票番号 | 売上日 | 顧客コード | 顧客氏名 | 顧客住所 |
|:-:|:-:|:-:|:-:|:-:|
| 1 | 2019/01/01 | C0001 | Nick Kim | 千葉県3番地 |

**[売上明細]**

| 売上伝票番号 | 商品コード | 数量 | 合計金額 |
|:-:|:-:|:-:|:-:|
| 1 | P0001 | 1 | 20,000 |
| 1 | P0002 | 2 | 20,000 |

　何が変わっただろうか。

　「商品情報」というシートが増えた。売上明細から独立したことで、売上が立つ前に商品情報を登録できるようになった。一商品一行なので、同じ商品の名称や単価があちこち点在する状態が解消された。商品名は一か所直せばすべてに反映される。同じ商品に対して商品名の同一性が保証されるので、データの信頼性も高まるだろう。

　売上明細の一行を特定するのは、売上伝票番号と商品コードの組み合わせである。となると、その組み合わせに紐づく情報のみがそこに存在するのが美しい。ところが商品情報は商品コードのみに依存していた。それを今回別のシートに切り離したわけである。

　これが「**第二正規形**」である。

　いやすっきりしたと運用を続けていると、あるときまた営業担当者から依頼を受ける。

　「このお客さん、まだ売上立ってないけど先に登録しておいてくれる？」

　「はい！」と応じて帳簿に向かうも、登録する場所がないことに気づく。顧客情報は売上伝票にある。売上が立たないことには伝票は作れない。どうしよう…とデータを眺めていると、変な顧客名を見つけた。

　「いやまだだろう」。よく買ってくれたな。違う。「山田 太郎」さんだ。直さなきゃ。ほかにもあったらどうしよう。顧客名はあちこちの伝票に散らばっている。ああ、混乱する。

　でも大丈夫。これも解決する方法がある。

**[顧客情報]**

| 顧客コード | 顧客氏名 | 顧客住所 |
|:-:|:-:|:-:|
| C0001 | Nick Kim | 千葉県3番地 |

**[商品情報]**

| 商品コード | 商品名 | 単価 |
|:-:|:-:|:-:|
| P0001 | サンドバッグ | 20,000 |
| P0002 | グローブ | 10,000 |

**[売上伝票]**

| 売上伝票番号 | 顧客コード | 売上日 |
|:-:|:-:|:-:|
| 1 | C0001 | 2019/01/01 |

**[売上明細]**

| 売上伝票番号 | 商品コード | 数量 |
|:-:|:-:|:-:|
| 1 | P0001 | 1 |
| 1 | P0002 | 2 |

　今度は「顧客情報」というシートが増えた。第二正規形の商品情報と同様、売上が立つ前に登録できるようになり、同じ顧客の氏名や住所があちこち点在する状態が解消された。

　売上伝票の一行を特定するのは売上伝票番号である。となると、それに直接紐づく情報のみがそこに存在するのが美しい。ところが顧客情報は、顧客コードを通して間接的に売上伝票番号に依存していた。それを今回別のシートに切り離したわけである。

　これが「**第三正規形**」、ひとまずの完成形となる。

　正規形はまず目指すべき基本の形であるが、パフォーマンスや使いやすさの観点から逆正規化、つまり非正規形に戻すこともある。

　たとえば、売上統計に顧客氏名を載せるとする。欲しいのは氏名だけ。住所までは必要ないので、わざわざ顧客情報を参照するのは処理時間のロスである。顧客氏名は売上伝票にも重複して持つことにしよう――と、これは現場でもよく見かける例である。

　この場合に気をつけなければいけないのは、重複して持った二つの氏名の整合性である。結婚して姓が変わった場合、顧客情報の氏名だけしか変更しないと、売上統計に出る氏名が旧姓のままになる。それを防ぐためには、必ずどちらも漏れなく変更するよう、データ更新処理をルール化してきちんと守る必要がある。そしてそれを完璧に守り切るのは、実はそれほど簡単なことではない。一度崩れた整合性を復元するのもたいへんなことである。

　正規化を崩そうとする際には、こうした管理コストや整合性リスクも考慮し、慎重に検討した方がよいだろう。もっとも、売上伝票の氏名は購入時のもので足りるということであれば、そのような心配は不要となる。そうした運用を実務で見かけることも少なくない。

　第一正規形で削除した合計金額のような計算項目は、あえて残しておくこともある。その方が毎回計算するより楽だし、処理時間も短縮できる。ただしこれは参照する際の都合である。手動で管理する場合、「数量を変更したのに合計に反映するのを忘れた」など不注意で整合性が崩れてしまう心配がある。表計算ソフトの計算式のように自動的に変更が反映される仕組みも多くのデータベース製品でサポートされているので、活用を検討するとよいだろう。

　第二正規形で商品情報に移動した単価は、時期や顧客、数量によって変動することがあるため、実際には売上明細にも保持しておくのが一般的である。ただし、定価で販売するとは限らないという前提で、商品情報の方を「定価」、売上明細の方を「売上単価」と位置づけるなら、これは逆正規化ではなく、要件に基づく適正化ということになるだろう。

　正規化で美しく整理されるのはわかったが、情報が分かれてしまうと見づらくならないか。一覧のよさは、まさに情報を一覧できるところであり、いちいちたくさんの表を照らし合わせるのは手間である。そのとおりだが、心配はいらない。リレーショナル・データベースには、情報を結合し、再構成できる機能が用意されているので、個々の要件に応じて組み直せばよい。

　ここまでデータベースの正規化について述べてきたが、「プログラムと美しさ」である。プログラムの正規化はないのか。

　ない――明文化、体系化されたものとしては。少なくとも普及はしていない。
　ある――暗黙的には。プログラムにもその理念は適用できる。

　正規化の理念は、美しいプログラムを追求するうえでも避けて通ることのできない、肝要なものである。データベースの正規化とは違った形で、独自に発展、体系化することができるだろうし、たとえ言い尽くされたことの再構成になるとしても、それは有用な試みだと思う。

　たとえば以下のような視点が考えられる。

- 冗長性の排除
- 混在の排除
- 点在の排除
- 不整合の排除
- 不完全性の排除
- 無用なものの排除
- アンバランスの排除
- あいまい性の排除
- 対象範囲の適正化

　面白そうなテーマであるが、詳細は別の機会に譲りたい。

---

[序章](https://qiita.com/CodeOne/items/01b4e49a7d5a84d48c71)
[第一章　その美の特徴](https://qiita.com/CodeOne/items/1969ec81d04529fbdb34)
[第二章　見た目と冗長性](https://qiita.com/CodeOne/items/96df6d1140301347156a)
[第三章　ロジック](https://qiita.com/CodeOne/items/3009027a2f8b892e2b6b)
[第四章　命名](https://qiita.com/CodeOne/items/dfc3ace22c46273dc22c)
[第五章　アーキテクチャ](https://qiita.com/CodeOne/items/469b9f421e5d914f1ffb)
[第六章　リファクタリング](https://qiita.com/CodeOne/items/df50fad6bb0bd2f0096f)
[第七章　デザインパターン](https://qiita.com/CodeOne/items/0ac628d6be24328ecc10)
第八章　正規化
[終章](https://qiita.com/CodeOne/items/12ad8d54329bc946b85c)