一度は読んでおきたい!ーSQLアンチパターン

By | 2016/12/29

難しい本だと思っていたけどそうでもなかった。むしろあるあるネタの宝庫で共感するところが多くて面白い本だった。
アンチパターンと言っておきながらそのパターンを使ってもいい状況もあるといったグレーなことを言っているのだがそれが現実的でいい。世の中白黒はっきりしたことはそんなに多くない。圧倒的に灰色のほうが多いのだ!
ただ想定される状況を理解するのに多少苦労することがあったのでそこは頑張る必要がある。でも一度流れがわかれば苦労の先に楽しみがあるのがわかっているので耐えられる。
みんながお勧めする理由もよく分かった。以下自分なりの大雑把なまとめ。

 

・ジェイウォーク

一つの列に(カンマ区切りなどして)複数の情報をいれてしまう
→ 各値は個別の行と列に格納する

・ツリー構造の設計

隣接リストで作るとツリーを取得するときアプリで何度も実行が必要。削除も子孫をすべて取得してから消す必要がある。追加や付け替えは楽。
→経路列挙  :文字列でツリーのパスを書いておく。深さが大きい場合は文字列に入らない恐れがあるので適さない。
→入れ子集合 :ツリーを一通り舐めた値をleft,rightの値として保存しておく。ノードの挿入が多い場合は適さない。ツリーの検索に有効
→閉包テーブル:関連する子孫との関連をすべて別テーブルにとっておく。メモリを消費しやすい。用途は広い。

・常に主キーとしてid列を作ってしまう

→その必要はない。テーブルにあったキー名を使用し、場合によっては複合キーを使う。

・外部キーを使わない

→外部キーを使え!浮いた列作られない、アプリ側でチェックの必要がない、など。管理が楽になるはず。

・1つの列にその属性を表す値を格納してしまう。

問題点:重複した属性を作ってしまう恐れやおかしな属性を入れることもできてしまう、外部キーで参照整合性を作れない
→方針:クラスの継承関係をどうやってデータベースで表現するかを考える
→一つのテーブルに全属性列を入れる。属性が多くなければ使える。
→属性ごとに別々のテーブルを作る。全テーブルをまたぐような検索をする機会が少ない場合は使える。
→違う部分だけ別のテーブルを作る。共通部分を使うことが多い場合に使える。
→LOB型のデータとして保存する。サブタイプや属性の定義追加が多い場合に仕方なく使う感じ。

・ポリモーフィック関連

子テーブル(comment_id, bug_id)
親テーブル1(bug_id, name)
子と親のテーブルが1対多関連の場合。この状態で親がもひとつ増えると
子テーブル(comment_id, issue_id, parent_type)
親テーブル1(bug_id, name)
親テーブル2(feature_id, name)
子に親が何のタイプか入れておく必要が出てくる。(ポリモーフィック関連)
問題点:issue_idに参照整合性制約をつけられない。(ひとつのテーブルからしか設定できないから。)
parent_typeは特別な制約がない。それにテーブル情報というメタデータを埋め込むことになる。
→関連の仕方が逆になっているのが問題。
子テーブル(comment_id, issue_id)
親テーブル11(bug_id, comment_id)親テーブル1(bug_id, name)
親テーブル22(feature_id, comment_id)親テーブル2(feature_id, name)
parent_typeがなくなり、メタデータもなくなる。
ただし、issue_idは親同士でかぶっても何もエラーにならない。
→共通の親テーブルを作る
子テーブル(comment_id, issue_id)
共通親テーブル(issue_id)
親テーブル1(issue_id, name)
親テーブル2(issue_id, name)
issue_idに参照整合性制約をつけられる。メタデータもない。

・マルチカラムアトリビュート

例えばバグに対してタグを付ける場合
Bugs(bug_id, description, tag1, tag2)
一つの列にタグを入れるのは、ジェイウォークと同じ問題が生じる。といって複数列作ってそこに埋めると
問題点:列が足りなくなったら追加する必要があるが大変+ロックがかかるなど
同じ行に同じ値がはいるといった重複を防げない
検索、削除が大変
→別テーブルを作って対処
Bugs(bug_id, description)
Tags(bug_id, tag)
上記問題点をすべて防げる

・メタデータトリブル

年によってデータを収集したい場合、列を加えたり、テーブルを加えることでデータを分割する。
Customers(customer_id, revenue2002, revenue2003)
Bugs2002(…)
Bugs2003(…)
問題点:データの格納先を選択しないといけない。一意性を確保できない。テーブルをまたいだクエリが必要。データを変更した時移動させないといけない場合がある。などいっぱい問題がある。
→パーティショニングを設定する
→従属テーブルを作る
CustomerRevenue(customer_id, year, revenue)
Bugs(bug_id, date, …)

・ラウンディングエラー(丸め誤差)

列の値にFLOAT型を使う
問題点:計算で誤差が生まれる。IEE754標準の浮動小数点数データ型だから2進数に変換されるので、そこですでに誤差が生まれる。
→FLOAT型は使わない
NUMERIC型やDECIMAL型を使う。ただし計算が遅いので財務のような精度を要求される場合に使用する。

・31フレーバー

つまりは取りうる値がある程度決まっているけど、長くやっているとその状態が増えたり変わったりすることに対応できなくなることがあるということ。
CREATE Bugs() state CHECK(’NEW’, ‘UPDATED’, ’FIXED’)
問題点:CHECKで値をしばれる。でもこれすると変更がなかなか聞かない。
→データとして値を持っておく。よっぽど値が固定の場合のみCHECKで制限をかける。基本的にはデータとして持っておくのが良い。
BugsStates(state)
追加も楽だし、ソートも楽、変更もできる。参照整合性制約をつければ他の値を入れられなくすることもできる。

・ファントムファイル:画像などの大容量ファイルをどう扱うか

データベースに保存しないと
アプリでファイル読み込み操作が別途必要になる、行を削除してもファイルは削除されない、ロールバックしたときにファイルが残ったり消えてしまったりする、画像を更新するとすぐにクライアントに反映されてしまう、同時に画像を更新したときの挙動が不明、ファイルへのアクセス権を別途管理する必要がある
ただし、やっぱりファイルは扱いを検討する必要があるものなので、必ずデータベースに保存するとか固執した考えに囚われてはならない。

・インデックスショットガン

要はインデックスをつけまくったり、全くつけなかったり。
→MENTOR(Measure, Explain, Nominate(指名), Test, Optimize, Rebuild)
Nominate:つまり問題となっているQueryを見つけること
Optimize:インデックスキャッシュのサイズの調整とか
Rebuild:長く実行していると断片化のようなことになるらしい。定期定期にやったほうがいいようだ。

・fear of the unknown

要はよくわからずにNULLと一般の値を区別なく使おうとすると思いがけない動作になってしまうことがあるということ。
確かにNULLを使っていい場合がある。従業員の退職日とか担当者の名前とか。これを文字なしとするかNULLとするか。
問題点:空白などだと入れ忘れかどうかがわからない。
→意味を持ってNULLを設定する。
SELECT * FROM Bugs WHERE assigned=NULLのようなクエリは何も返さない。NULL=NULL -> NULLでありtrueではない。
他にも
NULL AND true -> NULL
NOT(NULL) -> NULL
など。挙動が直感的ではないので意識的に使う必要がある。
むやみにNULLを使うのも問題だからNOT NULL制約できるときは設定する。
NULLのあるデータでNULLの値で欲しくない時はcoalesce()を使うと便利。
他にNULLに対する専用のSQLを使う(IS NULL, IS NOT NULL)

・曖昧なグループ

SELECT MAX(reported_date), bug_id FROM Bugs GROUP BY reported_by;
要はGROUP BYで集めたリストでMAXなどで得たものと、普通に得たものでは全然関連性がないから気をつけろよ、ということ。
単一値の原則:GROUP BYは行グループ1つに1つの値しかとらない。
→bug_idのような値を取得しない
→相関サブクエリ:
→導出テーブル
→外部結合(OUTER JOIN)

・ランダムセレクション

SELECT * FROM Bugs ORDER BY RAND() LIMIT 0, 1;
問題点:みたいなSQLはデータ量が増えるとどんどん遅くなっていく。
→データ量が少ないことが保証されている場合はそのままで大丈夫
→主キーを全部取ってきて、アプリでランダムに取って、データを取得する。データ量がほどほどなら使える。
→データの行数を取ってきて、ランダムに行を決定し、その行を取得する。Mysqlでは LIMIT Xとやることで行を取れるらしい。
→主キーが連番であることが保証されているなら、最大の主キーと取得してその間をランダムに取得する

・プアマンズ・サーチエンジン

要は全文検索で何の対策もなくLIKEで検索しているような状態
問題点:全然スケールに対処できず、処理が重い。インデックスが機能しないし。
→ベンダーごとの機能を使う:mysqlではFULLTEXTINDEX
→外部ツールを使う:Sphinx Search、Apache Luceneなど
→自作:転置インデックスを自分で作る。1テキストごとに含まれるキーワードとIDのセットを保存しておく。キーワードも交差テーブルを作る。

・スパゲッティクエリ

要は必要以上に複雑なクエリを書くな、ということ。
問題点:わかりにくい、間違う可能性も高まる、保守しにくい
→分かりやすい複数のクエリに分割する。
→CASEやSUM関数を使う

・インプリシットカラム(暗黙の列)

要は*を使って要素を追加したり、省略してinsertするのは、問題になるということ。
問題点:取ってきた順に処理していたら、列を追加したり、削除したら不具合になる。
insertはエラーになるし、そのときなぜかわからなくなる
→*や省略はしない

・リーダブルパスワード

問題点:開発者が読めるものは、攻撃者にも読める
読み取れるところは、ネットワークを通るところと、記録に残るところ(データベース、ログ、バックアップなど)。
→以上のところで不可逆な値を渡して、見られても平気な状態にしておく。
データベースでもSHAなどの関数があってパスワードを暗号化できるけど、結局ネットワークを通ったあとでは傍受されている可能性があるので、
アプリケーション側で暗号化しておいてそれを使うことになる。
ブラウザなど
↓HTTPSで暗号化したものを送信
Webサーバー
↓暗号化されていなければ、SHAなどで暗号化して保存、またはログインなどでは比較を行う
データベース
★結局ログインのときにHTTPSで暗号化されたものをすべて傍受できたら、次からそれをそのまま使えばやりとりが可能なのでは?

・SQLインジェクション

動的SQLをそのまま使う
値のエスケープ
プリペアドステートメント
ストアドプロシージャ:SQLをデータベースに登録しておいて使い回す仕組み。
→すべての入力値は問題があるとみなして対処する
プリペアドステートメントやフィルター、バリデーター、マッピング配列などを使って、SQL文を作る。

・擬似キー潔癖症

つまり擬似キーに欠番があるのを嫌がる。
しかし実際には欠番になることがある。これ自体問題はない。
→擬似キーを行番号と混同している可能性があるので整理する
→別の列を作ってそれを行番号として、擬似キーは見えないようにする。

・シー・ノー・エビル(臭いものに蓋)

問題点:エラーチェックをやらない
→エラーチェックをやろう!
→SQL文を一時変数に作って、全文見れる状態で開発しよう!デバッグが楽になるぞ!できればこれを出力したりログに出そう。百聞は一見にしかず!

・ディプロマティック・イミュニティ(外交特権)

データベースに関しては、テストやドキュメントやバージョン管理などはやらなくていい、、、わけない!

文書化:ER図、テーブル・列・ビュー、関連、トリガー、ストアドプロシージャ、SQLセキュリティ、データベースインフラストラクチャ、ORM
バージョン管理
テスティング

・マジックビーンズ(魔法の豆)

モデルがアクティブレコードそのものだと
問題点:使用者が意図せずにデータベース操作を行えてしまう。
MVCパターンでコントローラーが重複したコードを書くことになる、そのテストをすべて行わなければならない。変更があればすべてに影響してしまう。
→モデルがアクティブレコードを持つようにする。こうすることでコントローラーがデータベースの詳細を知らなくてもよくなる。テストも減る。

・砂の城

要は滅多に起こらない問題に対応しているか?ということ
→ベンチマーク:処理の限界を知る
→例外処理を入れているか
→バックアップを取っているか
→高可用性を担保しているか
→災害などがあった場合の対処は?

 

感想

上にずらずら書いてしまいましたが読んだ後では項目がかなり絞られていたと感じました。実際はもっといろいろな問題に遭遇していた気がするので物足りなさすら感じます。でも良書。最低限気に留めておくべきことが詰まっていました。

 


コメントを残す

メールアドレスが公開されることはありません。