背景
最近になってDBの知識不足に危機感を憶えて色々と読んできた。
今回は度々引用されているのを見かけるSQLアンチパターンを読んだ。
- 作者:Bill Karwin
- 発売日: 2013/01/26
- メディア: 大型本
DB関連は何かとアンチパターンから学習に入ることが多いような気がするが、この本がきっかけなんだろうか。それとも、目の前の仕様を満たす設計を行いそのようにアプリケーションに組み込めるけれど後になって実は悪手だったと気がつくことが多いからなのだろうか。どっちでも良いけど、アンチパターンから学ぶのは机上の空論ではない泥臭さがあって良いね。
本書の構成
構成はハッキリと分かれており下記のようになっている。
また各章の構成は下記のようになっている。
気になるところだけ掻い摘んで読めるので、とりあえず買って業務で目の前の課題に対応するアンチパターンを読んでそれを踏まないように注意するような使い方もできそう。
読んだ箇所のざっくりした感想
何冊か読んでいるためある程度理解した上で読むことができていた。また、経験的にわかっていることも多かった。しかし、もちろん初めて知ったものや無意識に踏んでしまいそうなアンチパターンもあったので、読んでいて非常に面白かった。
論理設計
ちゃんと正規化しようという趣旨の「ジェイウォーク(信号無視)」やちゃんと外部キーを使おうという趣旨の「キーレスエントリ(外部キー嫌い)」は経験的に理解しているものだった。これはDBの学習以前に治安の良いアプリケーションを作ろうと設計した経験がある人であれば無意図の非正規化や外部キーが存在しないことへの忌避感を感じるとは思う。
一方で、「EAV(エンティティ・アトリビュート・バリュー)」や「ポリモーフィック関連」、「マルチカラムアトリビュート(複数列属性)」、「メタデータトリブル」はかなり参考になった。
EAVとポリモーフィック関連は似ているが、問題意識は真逆だ。EAVは状態に応じてカラムが変化する場合、ポリモーフィック関連は色んな状態で共通のカラムを使用したい場合に発生する。
EAV
EAVとは状態によって属性が可変なエンティティのことを指す。つまりあるカラムの状態に応じてカラムが動的に必要になってくるということである。
EAVをプログラムで表現すると下記のようになる。
code
#[derive(Debug, Default)] struct BugData { severity: String, version_affected: String, } #[derive(Debug, Default)] struct FeatureRequstData { sponsor: String, } #[derive(Debug)] enum State { Bug(BugData), FeatureRequst(FeatureRequstData), } #[derive(Debug)] struct Issue { priority: String, version_resolved: String, state: State, } impl Issue { fn new(state: State) -> Self { Self { priority: "high".to_string(), version_resolved: "solved".to_string(), state: state, } } } fn main() { let bug = State::Bug(BugData::default()); let feature_request = State::FeatureRequst(FeatureRequstData::default()); println!("{:?}", Issue::new(bug)); println!("{:?}", Issue::new(feature_request)); }
Issue { priority: "high", version_resolved: "solved", state: Bug(BugData { severity: "", version_affected: "" }) } Issue { priority: "high", version_resolved: "solved", state: FeatureRequst(FeatureRequstData { sponsor: "" }) }
テーブルとして表現したい場合にはどうすれば良いのかアンチパターンへの対処がいくつか記載されていた。
- 1つのテーブルに各ステータスの和集合のカラムを作る
- 諦めて別テーブルにする
- BugとFeatureRequestそれぞれの差分のみテーブルに切り出して親をIssueとするリレーションを作る
- JSONのまま突っ込む
JSONのまま突っ込むのは悪手に見えるが、ビジネス要因(要件を詰めていたりユーザの反応を見ていたり)で完全に構造が確定していない場合には有効な場面もあるだろう。
直感的にきれいなものは3つ目のリレーションをキレイに整理することだが、状態が2つくらいと少なく互いに関連して作用するものでもないなら別テーブルにしてしまっても良いのかな、とも思う。スキーマ変更時にどちらも修正する必要が出てくるので脆弱だけど。
ポリモーフィック
EAVでは状態に応じて詳細が紐付いているものだったが、ポリモーフィックの場合は複数の具体テーブルに共通的なカラムが存在しているものである。
つまり、Bug用のCommentとFeatureRequest用のCommentがあり、それぞれCommentという共通の構造が存在する。
これもプログラムを見るのが早い。(簡略のためis-aではなくhas-aの関係になっているけれど趣旨の複数の具体が共通を持つという構造に意識して着目したい)
code
use chrono::prelude::*; #[derive(Debug)] struct Comment { comment_date: DateTime<Utc>, comment: String, } impl Default for Comment { fn default() -> Self { Self { comment_date: Utc::now(), comment: "".to_string(), } } } #[derive(Debug, Default)] struct BugComment { severity: String, version_affected: String, comment: Comment, } #[derive(Debug, Default)] struct FeatureRequstComment { sponsor: String, comment: Comment, } fn main() { let bug_comment = BugComment::default(); let feature_request_comment = FeatureRequstComment::default(); println!("{:?}", bug_comment); println!("{:?}", feature_request_comment); }
BugComment { severity: "", version_affected: "", comment: Comment { comment_date: 2021-05-11T01:14:45.455349Z, comment: "" } } FeatureRequstComment { sponsor: "", comment: Comment { comment_date: 2021-05-11T01:14:45.455370Z, comment: "" } }
この構造をテーブル表現に落とし込む際、CommentはBugCommentもしくはFeatureRequestCommentと紐付いているため外部キーを設定する。だが、その外部キーはBugCommentとFeatureRequestCommentのどちらを設定するのかはデータ次第になるため、どのように設定すればよいのかわからない。
このアンチパターンはBugとFeatureRequestの両方を受け入れられるようにComment側では外部キー制約をしないというものだ。しかしこれはあまりに脆弱である。
これの対処法は2つある。
- 依存関係を逆転させてBugやFeatureRequestがCommentへの外部キーを持つ(↑の例示がhas-aだったので当然のように見えてしまう)
- CommentとBugの両方の外部キーを持つBugCommentテーブルのような交差テーブルを作成する
RDBの世界に落とし込む際には一筋縄ではいかないことは外部キーを考えると納得だった。また、アンチパターンを受け入れて良いケースとしてORM側でこれらの整合性を担保されているケースを挙げていた。
物理設計
小数は「丸め誤差」が発生してしまうことや「闇雲インデックス」をしてもパフォーマンスに寄与しないということは、別の書籍でも学習済みだったので理解に苦しくなかった。
新しく知ったことは「31フレーバー」「ファントムファイル」の章だった。
31フレーバー
限定的な種別を表す際にどのような設計をすべきかという話。
例えば、Bugテーブルのstatusに未対応・調査中・修正中・確認中・解決済みというものを割り振りたいとする。その際BugテーブルのstatusカラムにCHECK制約を設けるとする。これがアンチパターンである。
このようなケースは場当たり的な改修を前に拡張性や移植を考慮する時間が取れないと発生しそうな気がするし、実感として気が回りきらない時にやってしまったかもなと思う。対処法としては、status用に別途テーブルを定義して、Bugテーブルはstatusテーブルの外部キーを持つというものだ。そうすることでBugテーブルはカラム変更でロックされることもないし、CHECK制約を加える必要もなくなる。
ファントムファイル
メディアファイルはDBに入れずにs3に入れてそのパスをRDBに格納することがあると思う。でもこれを判断なしに行うのはよくないねという話。今まで何気なしにパスを格納していたけれどこの章を読んで改めて認識が変わった。
これがアンチパターンである理由はいくつかある。ひとつはs3などのストレージからメディアファイルが削除されてしまった場合にRDBには存在しないファイルを指すパスが格納されていることになる。しかしそれに気が付かない。
また、トランザクション分離の観点でもRDBでは削除できずロールバックしたがストレージから実態を削除してしまった、ということが起こりうる。さらに、DBにはURLなどのパス用のデータ型はないため不整値を弾くことができない。
許容できるケースとしてはファイルのサイズがかなり大きかったり数がとても多くDBやネットワークを逼迫してしまう可能性がある場合。
クエリ
クエリは既存の知識の確認となるような内容が多かった。NULLの扱いとか。
「ランダムセレクション」では乱数の扱いでテーブルを丸々スキャンしないようにクエリを作ったり、「インプリシットカラム」ではselect時にカラムをちゃんと指定するとか、丁寧なSQLライフを心がけようと思いを新たにした。
だが「スパゲッティクエリ」の章は耳が痛かった。これは複雑なクエリひとつですべてを解決しないようにしようというもの。アプリケーションでは単純なクエリを心がけているが、redashとかレポートやグラフで可視化したいような時にはスパゲッティクエリを書いてしまいがちな気がした。気をつけたい。
アプリケーション開発
アプリケーション開発の章は純粋なRDBの知識ではないため別の態度で読む必要があった。
「リーダブルパスワード」「SQLインジェクション」「シー・ノー・エビル」は基本的な話だったので自分には目新しく感じるところはなかった。
非常に面白かったのは「ディプロマティック・イミュニティ」の章だった。問題は、アプリケーション開発のルールはデータベース開発には通用しないといった、データベースへの特別視・特権扱いをしてしまうことだ。これは、組織として体制や役職、業務フローに組み込まれてしまっているところはあると思う。例えば、ソフトウェアエンジニアとDBAの業務がきっぱり分かれていたり、データベースの知識や運用権限がデータベースを扱う一部の人に集中してしまったりなどだ。この章は、今までの章と異なりかなりメタ的な話だった。
アンチパターンの見つけ方には、データベースを特別視した発言を挙げていた。これはよく見かけるものだと思う。
これの対処法には文書化やバージョン管理、テストなど運用を一部の人だけに限定せず民主化することだ。より具体的には、ER図やテーブルスキーマ、セキュリティやインフラ構成などをしっかり形式知にしていくことである。思うに、それらに加えてどのような経緯で現状の形になったのか、他の選択肢はなかったのか、当時はどういう状況のため何を判断基準として意思決定したのか、という今後の意思決定のための情報が残されているとかなり健全になっていくはずだ。これはDBに限らない組織全てに関する話だと思う。
まとめ
当然に感じる知識や言語化されて整理された知識、そしてもちろん新しい知識があった。RDBに関連する本を何冊か読んできたけれど名著と言われるだけあって非常にわかりやすくて考えさせられるような話が多かった。また、明日にでも使える知識が多いので非常にためになった。
特に私はアプリケーション開発者なので、論理設計はとても参考になった。EAVとポリモーフィックは最初はなかなか分別がつかず、整理しきれていない部分があったがこうしてブログにして言語化してみると整理することができた。また、最後に書いたディプロマティック・イミュニティは非常に根が深いと思う。いち個人ではなかなか解決できないもので組織を横断して取り組む必要があると思う。社風も個人のDBへの向き合い方も関わってくる非常に難題だと感じている。少なくとも自分がこのような考えを巡らせてしまった時には本書のことを思い出して、積極的にDBの運用や構成にも関わっていければと思う。