関係データベースにおける配列の表現
以前のエントリで「次回は履歴を保存する方法について解説します」と書いたのですが、その前に気になるテーマを見つけたので、今回は予定を変更してそちらの解説を行いたいと思います。
その気になるテーマというのは、表題の通り、関係データベース (RDB) における配列の取り扱いについて。 そもそも、リレーションモデルでは、原則的に単一のカラムはスカラを表すため、これを用いてベクタである配列を直接表現することはできません。 (参考: リレーションの正規化#第一正規形 - Wikipedia) しかしながら、実際には配列というデータ構造を用いずにシステムを設計・開発することは現実的に不可能です。
結論から言えば、関係データベース上で配列を表現するためのごく簡単なテクニック (というほど大げさなものでもない) があるのですが、少なくとも私が観察した範囲では、その手法にはこれと言った名前が付けられておらず、また、それを採用せずに、奇妙あるいは姑息な手段で配列を実装しているシステムが頻繁に観察されます。 そうした手段を用いて構築されたシステムは、保守性・拡張性に乏ため、正直なことろ、あまり関わり合いになりたくないもの。 そんなわけで、正しい配列の表現が普及することを願って、今回のエントリを上げる次第です。
配列が駄目なら、カラムを増やせばいいじゃない?
先に述べた奇妙・姑息な手段の中でも、最も広く普及しているのが「要素の最大数だけカラムを定義する」というものです。 下記に、メッセージ (メールのようなものだと思ってください) の情報を表現するテーブルを示します。
カラム | 型 | 制約 | 説明 |
---|---|---|---|
id | INTEGER | PRIMARY KEY | 識別子 |
from | TEXT | NOT NULL |
差出人識別子 [ 例: ' koko ' ] |
target1_type | INTEGER | NOT NULL |
宛先 (1) 種別 [ 0: 使用しない, 1: To, 2: Cc, 3: Bcc ] |
target1_username | TEXT | - |
宛先 (1) ユーザ識別子 [ 例: ' jonah ' ] |
target2_type | INTEGER | NOT NULL | 宛先 (2) |
target2_username | TEXT | - | |
target3_type | INTEGER | NOT NULL | 宛先 (3) |
target3_username | TEXT | - | |
subject | TEXT | NOT NULL | 表題 |
content | TEXT | NOT NULL | 本文 |
例えば、ユーザ koko が、部下の lehm に To, valmet に Cc でメッセージを出したとき、その情報は次のように格納されます。 (カラム "content" は省略。)
id | from | target1_type | target1_username | target2_type | target2_username | target3_type | target3_username | subject |
---|---|---|---|---|---|---|---|---|
21 | koko | 1 | lehm | 2 | valmet | 0 | Dance with Undershaft |
この方式の何がマズいかはすぐに分かると思います。
まず、宛先の最大数が3つに固定されていること。
もしもユーザから「もっとたくさん宛先を指定したい」という要望がきたらどうなるでしょうか。
そう、現在定義されているのと同様のカラムを target4_type
, target4
, target5_type
, target5
, ... という具合に際限なく増やしていくことになります。
そうしてカラムの数が増えていくと、これを扱うSQLクエリ (問い合わせ文) も長くなっていきます。
例えば、ユーザ 'jonah
' 宛てのメッセージを取り出すクエリは次のようになるでしょう。
SELECT * FROM message WHERE target1 = 'jonah' OR target2 = 'jonah' OR target3 = 'jonah';
この反復は宛先の最大数が増えるごとに長くなっていきます。 指定できる宛先を増やすという些細な変更で、クエリ生成部のコードに変更が必要になるなんて、こんな馬鹿気た話はありません。
TEXT型なら何でも入るぞー?
前節で取り上げた配列の表現方法はあまりにも非効率的。 そこで、もう少し「進んだ」プログラマは、一工夫してより「スマート」な手法を編み出したりします。
カラム | 型 | 制約 | 説明 |
---|---|---|---|
id | INTEGER | PRIMARY KEY | 識別子 |
from | TEXT | NOT NULL |
差出人識別子 [ 例: ' koko ' ] |
targets | TEXT | NOT NULL |
宛先 <種別> : <ユーザ識別子>[ , <種別>: <ユーザ識別子> ... ] |
subject | TEXT | NOT NULL | 表題 |
content | TEXT | NOT NULL | 本文 |
見たところ、カラム数がかなり減っているのが分かるでしょう。
その理由は、「敢えて (?)」第一正規形を崩して、カラム targets
を複数のスカラ値を保持できるように、言い換えれば、ベクタ型として定義したことにあります。
このように定義されたテーブルに、前節と同じメッセージを格納すると、その内容は次のようになります。
id | from | targets | subject |
---|---|---|---|
21 | koko | 1:lehm,2:valmet | Dance with Undershaft |
なるほど。 この方法ならば、宛先の指定数はいくらでも増やすことができますね。 しかも、カラムの数が変化しないので、SQLクエリの生成コードに手を入れる必要はありません。
SELECT * FROM message WHERE targets LIKE '%:jonah%';
LIKE演算子なんて高度な機能まで使いこなしちゃってます。 なんて素晴らしい功績! 君は英雄だ!
......とでも言うと思ったかボケがァ──!!
全然素晴らしくない! LIKE検索はその動作をちょっと想像してみれば分かるように、かなり複雑な処理。 こんな基本的な部分で毎回こんなことをやっていたら、レコード数が増えるにつれてどんどん動作が遅くなっていくのは目に見えています。 そもそも、これはたまたま宛先フィールドが一つの整数と、一つの「アルファベットから成る単純な文字列」だけで構成されているからこそ通用するテクニックに過ぎません。 フィールドがもうちょっと複雑なものになったら、そのロジックは瞬時に崩壊します。
ここで、根本的な設計の見直しを避け、小手先の変更でその問題を凌ごうとすれば、アプリケーション固有のエスケープ規則などを導入することになり、システム設計の複雑さは果てしなく増大していきます。 そうした間違った「努力」は、現場のエンジニアを疲弊させるだけでなく、製品の質を落とし、最終的にはメーカの信用を失墜させるでしょう。 元を正せば、その手直しに掛かるコストは、最初の設計を蔑ろにして「浮かせた」時間と手間、そして、それを放置したことによって生じた「利息」なのです。
当たり前のデザイン
では、どうすれば配列を正しく表現することができるのか。 それは至極簡単で、配列の要素を表現・格納するテーブルをもう一つ、別に作るだけ。 これは関係データベースの性質を考えれば自然に導かれる結論で、こうしてエントリを立ててドヤ顔で説明するのが躊躇われるほど初歩的な話です。
カラム | 型 | 制約 | 説明 |
---|---|---|---|
id | INTEGER | PRIMARY KEY | 識別子 |
from | TEXT | NOT NULL | 差出人識別子 |
subject | TEXT | NOT NULL | 表題 |
content | TEXT | NOT NULL | 本文 |
カラム | 型 | 制約 | 説明 |
---|---|---|---|
message | INTEGER | PRIMARY KEY | メッセージ識別子 (message.id) |
index | INTEGER | インデクス (0から連番) | |
type | INTEGER | NOT NULL |
種別 [ 1: To, 2: Cc, 3: Bcc ] |
username | TEXT | NOT NULL |
ユーザ識別子 (例: ' jonah ') |
message_target
の主キー (primary key) が、message
の主キーと、インデクスの組み合わせになっていることに注目してください。
これによって、この二つのテーブルの間に重複のない一対多の関係があることが明示されました。
Ruby on Rails などのフレームワークの影響か、「テーブルの主キーはスカラ (さらに、多くの場合は「整数」限定) でなければならない」と思い込んでいる人が多いようですが、実際にはこのように用途に応じて柔軟に設定できます。
さて、これらのテーブルに前節までと同様にメッセージデータを格納すると、以下のようになります。
id | from | subject | content |
---|---|---|---|
21 | koko | Dance with Undershaft | ... |
message | index | type | username |
---|---|---|---|
21 | 0 | 1 | lehm |
21 | 1 | 2 | valmet |
このような設計にしておけば、後から仕様に変更が生じた場合にも、最小限の手間でこれを実装することができます。 例えば、「『宛先』のフィールドに『メモ』欄を付けて欲しい」という修正依頼があったとき、前の二つとこの最後の設計では、どれが一番楽に対応できるかと考えてみてください。 まぁ、一目瞭然ですよね。
他人が作ったプログラムを修正するときも、設計の善し悪しが重大です。 設計不在のプログラムを正しく修正するのは非常に骨が折れるか、または無理です(実際にそういう経験をしたことがある人も多いでしょう)。 逆に、きちんと設計されたプログラムは読みやすいので直すべき場所を見つけるのが簡単だし、他人が作った良いコードを読むのは勉強にもなります。 しかし、実際に良いコードに出会うことはほとんどないのではないでしょうか?
だからこそチャンスなんだ
どうやら、知恵を使いさえすればしなくても済むはずの苦労が、ソフトウェア開発業界には蔓延しているようです。 これを逆に考えてみると、設計の技術を使いこなしてよけいな苦労を回避できるようになれば、人の何倍も仕事がこなせるようになるということです。
つくる人の味方: 設計ってナンダ? - PART1
他にもいろいろ
配列の他にも、ハッシュテーブルなどのデータ構造や、クラスの継承といった、プログラム上で用いられる概念を関係データベース上に仮託する手法には、「定石」といえるようなメジャな手法が存在します。 しかしながら、そうしたデザインパターンには広く知られた名前が与えられておらず、プログラマの間で充分に共有されていないため、現場ごとに独自のノウハウが蓄積されがち。 そして、そのノウハウが先に紹介したような「正しくない」手法であるケースは決して珍しいものではありません。 私は、このような現場の状況こそが、杜撰・粗悪な設計を生み出す温床となっているのだろう、と考えています。
もし、データベースの「デザインパターン」を体系的にまとめている資料などをご存知の方がいらっしゃいましたら、是非とも教えてください。
コメント
ジョジョ? ( 投稿者: T氏 <tDKba@TVmVd> )
Re: ジョジョ? ( 投稿者: なりた )
ちなみにジョジョでやるなら「ド低能がァ──ッ!!」か「クサレ脳ミソがァ──ッ!!」ですね。個人的にはその前の部分に反応して欲しかった...。
Untitled ( 投稿者: 通りすがり <pOEJcWHp6Ud> )
勉強になりました