識別子にまつわるエトセトラ
コンピュータ・システムは一般的に、それが扱うレコード (データのまとまり) が個別に正しく扱われるようにするために、その各々に対して「ID (identifier = 識別子)」と呼ばれる値を割り当てます。
IDは、システムの時間的・空間的な運用範囲において、個々のレコード一意に識別することができるように設計しなければなりません。 これらのIDが利用される範囲は、即ち、そのシステムが影響を及ぼすことのできる範囲に等しく、故にこれは、そのシステムの「規模」そのものであると考えることができるでしょう。 従って、IDに関する設計 (デザイン) は、そのシステムの在り方、そして品質を大きく左右する要素となります。 しかしながら、所謂プロの開発者の中にさえ、このIDの設計に関して杜撰あるいは無頓着な人が多いように観察されます。
そんなわけで、このエントリでは、IDをどう設計すべきか (あるいは、どう設計すべきでないか) について、具体的な失敗の事例を交えつつ、私の考える「勘所」について述べていきたいと思います。 (案件の特定を防ぐため、細部は微妙に変更しました。)
ケース1
私の知り合いが設計・構築したシステムのお話。 そのシステムは業務支援系のもので、データベースには顧客が東北全域に展開していた「店舗」および、その競合店の情報が格納されています。 各店舗にはIDとして一意な整数が割り当てられるわけですが、その発行の仕方には次のようなルールがありました。
店舗の所在地 | ID子の範囲 | |
---|---|---|
青森県 | 1000 ~ 1999 | |
秋田県 | 2000 ~ 2999 | |
岩手県 | 3000 ~ 3999 | |
山形県 | 4000 ~ 4999 | |
宮城県 | 仙台市: 青葉区 | 5000 ~ 5999 |
仙台市: 泉区・宮城野 | 6000 ~ 6999 | |
仙台市: 太白区・若林区 | 7000 ~ 7999 | |
仙台市外 | 8000 ~ 8999 | |
福島県 | 9000 ~ 9999 |
宮城県が他よりも細かく区分けされているのは、その顧客が宮城県を中心に自社店舗を展開しているため。 (必然的に、競合相手として認識する必要のある他社店舗の数も多くなります。) 「このようなルールを設定しておけば、IDでソートすればエリア順に並ぶし、また連番でIDを発行すれば、各エリアに現在いくつの店舗があるか分かりやすい。」 設計者はそんなことを考えていたのかもしれません。
ところで、一度発行したIDは、その店舗が撤退するなどして消えてしまった場合であっても、これを回収して新しく登場した店舗に割り当てることはできません。 何故なら、このシステムは過去の営業データを出力する機能を備えているため、現在存在しない店舗についても、その情報を削除するわけにはいかないからです。 ……ここまでの説明を聞いた時点で、システム設計に詳しくない読書の方々も、イヤ~な予感がしてきたのではないでしょうか。
一つ目の問題点は、このルールに基づいたIDの発行がプログラムによってではなく、このシステムを運用するユーザの手作業によって管理されていたこと。 そのため、ユーザは新しく店舗が増えた場合、まずその所在地を確認し、次に該当するエリアにおいて最後に発行されたIDを確認、さらにこれに1を加えた値をシステムに指定してやらなければならなりません。 しかし、いくら気をつけて作業しているといっても、そこには人間の注意力の限界というものがあります。 値が1つ2つ飛ばされてしまうことも珍しくなく、また、別件の作業などで忙殺されているときなどは、前回発行の番号を調べる手間を省くために、キリの良い値までスキップしてこれを指定するといった横着もしばしば起こりました。 さらには、「飛ばされた番号を憶えておいて、次の登録時はそれを使う」という涙ぐましい努力によって、その運用はさらに複雑なものとなっていたのです。
二つ目の問題点は、皆様のご想像の通り、番号の枯渇。 この業界の激戦区である岩手県および仙台市青葉区は、日々新しい店舗ができては消えていくという、戦国乱世の様相を呈していました。 そのため、十年は安泰だろうと思われた1000個のIDストックが、わずか3年ほとで底をついてしまったのです。
そんなわけで、このシステムはエリアに関係なく連番でIDを発行する仕様に改修され、現在に至っています。 この改修によって、ID発行の運用管理に払われた努力はすべてが水の泡と消えてしまったわけですが、何がいけなかったのしょうか? それは、IDに「識別」以外の機能を持たせてしまったこと。 本来、この「エリア」のような情報は、それを格納する専用のフィールドを設けるなり、所在地のフィールド値から自動判定するような仕様とすべきだったのです。 ひとつの値から複数の情報を読み取れるような設計をすると、「一石二鳥!オレって頭良い!」なんて考えてしまいがちですが、「大抵の場合、それは地獄の入り口である」と心得ておくのがよいでしょう。
ケース2
次に紹介するのは、私の後輩に当たる人物が設計したシステム。 このシステムは、音楽の情報を扱うもので、携帯端末から利用されることを想定しているため、サーバと遣り取りするデータ量をできるだけ小さくしたいという要求がありました。 特に問題となるのが、端末に保存されている曲の情報と、サーバに保持されている曲の情報との照合。 曲情報には「曲名」の他、「アーティスト名」「ジャンル」「演奏時間」「テンポ(BPM)」など、検索・同定のためのフィールドが含まれています。 これらがすべて一致するものを「同じ曲」として認識するわけですが、サーバが保持しているこれらの情報をすべてダウンロードするには相当な時間が掛かってしまい、アプリケーションとしては使い物になりません。 かといって、クライアントが保持するすべての曲情報をアップロードする、というのも、サーバの負荷を考えると回避したいところです。
そこで後輩が思いついたのは、 全てのフィールドからハッシュ値を計算し、これを曲データの「フィンガプリント (fingerprint)」として使う方法。 これならば、フィールド値全部を受け渡しするよりははるかに軽量ですし、そのデータ長を充分に取れば、値の衝突による誤認識の可能性も実用上問題にならない程度まで低減させることができます。 なんと冴えたやり方でしょう! 実際、ここまでの設計には何の問題もありません。 むしろ、かなり上手くやった方だと思います。
ところが、彼はここからさらに進んで、このフィンガプリントをサーバ側のデータベースにおけるID、即ち「主キー (primary key)」として使おうと思い立ちました。 曰く、「事実上衝突する可能性がゼロなのであれば、それは一意な識別子として使うことができるはず。 そうすれば、別にID値を保持するよりもディスクスペースを節約できる。」 ……なるほど、それはなかなかに合理的な考え方であるようにも思えますが、いったい何が問題なのでしょう?
繰り返しになりますが、IDとはそれが指す実体を一意に識別するためのものです。 例えば、私という個人の情報を表すレコードがあったとしましょう。 そのレコードの「内容」は、システムの運用中に変わる可能性があります。 例えば、結婚・離婚によって「姓」が変わったり、引越しによって「住所」が変わったり。 しかしながら、私という実体は連続して存在するものであるため、それは以前と変わらないIDによって識別されなければ困るわけです。
さて、以上の考察から、フィンガプリントをIDにすることの問題が見えてきました。 フィンガプリントは、その曲データが保持する (この場合はすべての) フィールドの値から計算される値。 従って、例えば「曲名」が1文字でも変われば、たとえそれが、誤字・脱字の修正であっても、その同一性を保持することはできません。 さらに、フィールドの種類が増えた場合、例えば、「歌詞の言語」などが新しく追加されたら、登録されているすべて曲についてフィンガプリントが変わってしまう事態となります。 一言で表現するなら、「フィンガプリントには、IDに要求される永続性が欠落している。」といったところでしょうか。
ではこの場合どうすればよいのかと言えば、話はごく簡単。 IDとは別に、フィンガプリントのフィールドを設ければよいだけです。 実際、システムの設計もそのように変更されました。 これはケース1と共通するところがありますが、要は、IDに「識別」以外の機能を持たせるな・期待するなということなんですね。
まとめ
私の経験からするに、システム設計というものは、データ構造を決めた時点でその7割が完了しています。 それ以外の部分、例えばユーザインターフェイスなどの表面的な挙動については、実装のフェイズで対応できる部分が殆ど。 (まれにそうでない部分もありますが。) そして、データ構造が上手く計画されているかどうかは、テーブルの間の参照関係、即ちIDの使われ方を見れば、おおよその成否を把握することができます。 ここで述べたようなものの他にも、セッションIDを連番で割り当てたために、他のユーザのセッションが推測されてしまい、これを乗っ取らてしまったケースなど、IDの不適切な設計・運用の例は枚挙に暇 (いとま) がありません。
そんなわけで、IDというのは、データ構造の基礎にして究極のテーマ。 システム開発の初心者だけでなく、中級者以上のレベルに達した人であっても、自分のID設計を見直してみると、色々と気付くことがあるかも知れませんよ。
コメント