SQLアンチパターンまとめ
はじめに
DBについての学習のなかで、オライリーが出版している「SQLアンチパターン」を読みました。
読んだ内容のアウトプットとして自分用に各章の要点をまとめてみました。
1章 ジェイウォーク(信号無視)
一つのカラムに「,」区切りで複数の情報を入れないようにする
⚪︎デメリット
-
ソートなどの操作がしづらくなる
-
値の長さによって入ってるデータ数が変わる
例)VARCHR(140)では1つのデータが14文字なら10個、10文字なら14個入ることになる
-
「,」が入力されない保証がない
-
想定外のデータを入れられるかもしれない
例)id: 1,2,banana
-
解決策:交差テーブルを作成する
2章 ナイーブツリー(素朴な木)
-
目的
階層構造を格納し、クエリを実行する
→例)組織図、スレッド形式のコメント欄
-
アンチパターン
常に親のみに依存する(parent_id列を追加する)
→同テーブルの別の行を参照する(隣接リスト)
-
メリット
- コメントとその直近の子の取得は簡単
- 葉ノードの追加が簡単
- ノードやサブツリーの移動も簡単
-
デメリット
- すべての子孫の取得が難しい
- 隣接リストはツリーの1つの階層が1つのJOINに対応している(SQLを書く際はJOINの数を固定しなくてはならない)
- 集約関数の扱いが難しい
- ノードの削除は難しい
- すべての子孫の取得が難しい
-
用いても良い場合
階層構造に対しての操作がノードの直近の親子の取得や列の挿入に限られる場合は隣接リストは効果的に機能する。
また、隣接リストに格納された階層構造をサポートするSQL拡張機能がある場合も選択肢に入る。
-
-
解決策
代替ツリーモデルを使用する
-
経路列挙
path列を大きめのVARCHARとして定義し、現在の行までのパスを格納する。
例)1/4/5/6/7
ジェイウォークと同じく、パス値の保証が正確な形成や保証ができなかったり、文字列の長さの上限があるため無制限の深さのサポートは厳しいという脆弱性がある。
-
入れ子集合
子孫の集合に関する情報を書くノードに格納
nsleft:そのノードより下の階層にある全てのノードが持つ値より小さな値を格納
nsright:そのノードより下の階層にある全てのノードが持つ値より大きな値を格納
子孫はあるコメントのnsleftとnsrightの間にnsleftが含まれるノードを検索することで取得可能
先祖はあるコメントのnsleftを、nsleftとnsrigthの間に含むノード全てを検索することで取得可能
直近の親子の取得、ノードの挿入や移動は複雑になる。
サブツリーに対する迅速かつ容易なクエリ実行が重要な場合は適している。
-
閉包テーブル
ツリー全体のパスを格納
もうひとつ新たにテーブルを定義する。
このテーブルの各行に先祖/子孫関係を共有するノードの組み合わせを格納する。ツリー状の離れた位置にあるノードも含めた、全てのノードが対象になる。(自分自身を参照する行も追加)
例)
先祖 子孫 1 1 1 2 1 3 1 4 コメント4の子孫を取得する場合、先祖が4の行を検索。
先祖を探す場合は、子孫が4の行を検索。
-
デメリット
テーブルを増やすため、メモリを多く使う
-
-
-
それぞれの設計に長所・短所があるため、最も重要な操作に沿って最適な設計を選ぶことが大切
設計 | テーブル数 | 子へのクエリ実行 | ツリーへのクエリ実行 | 挿入 | 削除 | 参照整合性維持 |
---|---|---|---|---|---|---|
隣接リスト | 1 | 簡単 | 難しい | 簡単 | 簡単 | 可能 |
再起クエリ | 1 | 簡単 | 簡単 | 簡単 | 簡単 | 可能 |
経路列挙 | 1 | 簡単 | 簡単 | 簡単 | 簡単 | 不可 |
入れ子集合 | 1 | 難しい | 難しい | 難しい | 難しい | 不可 |
閉包テーブル | 2 | 簡単 | 簡単 | 簡単 | 簡単 | 可能 |
3章 IDリクワイアド(とりあえずID)
-
目的:主キーを持つこと
ある対象領域をモデル化したテーブルに、その領域で意味を持たない人工的な値を格納するには新たな列を追加しなければいけない。この列を主キーとすることで行を一意のものとして扱えるようになる。
→このタイプの主キー列は、擬似キー・代理キーと呼ぶ。
-
アンチパターン:全てのテーブルに「id」列を用いる
- 列名はid
- データ型は32ビット、64ビットの整数
- 一意の値が自動的に生成される
➡︎id=主キーと考えることが一般的だが実は誤解
-
デメリット
-
冗長なキーが作成されてしまう
同テーブルにある別の列が「自然な」主キーとして使えそうでもid列を作成してしまうことがある。→必要ない
-
重複業を許可してしまう
複合キーで一意であることを保証しなければいけない時に、id列を主キーとすると複合キーとなる列の組み合わせが常に一意であること保証しなくなる
-
キーの意味が分かりにくくなる
idという列名は一般的であるため、明確な意味を持たない。
-
USINGを使用する
両方のテーブルに同じ名前の列がある場合、USINGを使用してテーブルを結合できるが、全てのテーブルの主キーがidだと、従属テーブル側の外部キー列には、参照する主キーと同じ名前が使えない
-
-
アンチパターンを用いても良い場合
-
一部のオブジェクトリレーショナルマッピング(ORM)フレームワークを使用する場合
全テーブルで同じように主キーを定義する。規約に従った方が便利な機能を使いやすい
-
自然な主キーがあまりにも長すぎる場合
-
-
解決策:状況に応じて適切に調整する
主キーは制約であり、データ型ではない
単一の列や複数の列を主キーとして宣言できるし、自動インクリメントする整数方の列を主キーではない列としても定義できる。
-
分かりやすい名前をつける
主キーが識別する対象のエンティティを表すものにすべき。
-
規約に縛られない
-
自然キーと複合キーの活用
一意であることを保証できて、NULLを許容しない、行の識別に使える属性がテーブルに含まれているのであれば、自然キーや複合キーも使える。
プロジェクトの途中から自然な主キーが重複を許可するようになってしまう場合もある。→これらのケースの唯一の解決策は擬似キーの使用。
-
4章 キーレスエントリ(外部キー嫌い)
-
目的:データベースのアーキテクチャを単純化する
-
アンチパターン:外部キー制約を使用しない
外部キー制約を省略すれば、データベース設計がシンプルになり、柔軟性が高まり、実行速度が速くなる→しかし、参照整合性を保証するためのコードを書く責任が開発者に生じる
-
アンチパターンを用いても良い場合
- 外部キー制約をサポートしていないデータベース製品を使用する場合
-
解決策:外部キー制約を宣言する
-
カスケード更新で複数テーブルの変更をサポートする
-
オーバーヘッドにならない
外部キーの方が効率的
- 挿入、更新、削除の前にselectクエリを実行する必要がない
- 複数テーブルの変更を防ぐために、テーブルをロックする必要がない
- 他の方法のように孤児が生じてしまうことがないので、データ品質管理用スクリプトを定期的に実行する必要がない
-
5章 EAV(エンティティ・アトリビュート・バリュー)
-
目的:可変属性をサポートする
-
アンチパターン:汎用的な属性テーブルを使用する
属性を行に格納する
-
エンティティ
通常、親テーブルに対応する外部キー。親テーブルではエンティティごとに1行が割り当てられる。
-
属性
従来型のテーブルでは属性は列名に相当するが、EAV設計では属性名が各行に入っており、行ごとに識別が必要
-
値
エンティティの属性の値
例)EAV設計
エンティティ(外部キー) 属性名 値 0001 product 1 0001 date_reported 2009-06-01 0001 status new 0001 desctiption 保存処理失敗 0001 reported_by John
デメリット
- 属性の取得が冗長になり、明確さに欠ける
- データ整合性が保てない
- 必須属性を設定できない
- SQLのデータ型を使えない→全て文字列型
- 参照整合性を強制できない
- 属性名の統一が難しい→同じものでも人によって入れる値が変わる
- 行を再構築しなければいけない
- 全ての属性を行の一部分として取得(1行として取得)するには、各属性の行のJOINが必要。
-
-
アンチパターンを用いてもよい場合
EAV設計の使用を正当化する理由は簡単に見つからない。
非リレーショナルなデータ管理が必要なら、非リレーショナルな技術を使用するべき。
-
解決策:サブタイプのモデリングを行う
-
シングルテーブル継承
全ての属性を個別の列に格納し、関連する全てのサブタイプを1つのテーブルに格納。
対応する属性を持たないオブジェクトを格納する行にはNULLが入る。
新しい属性が出てくるたびに手いーぶる定義を変更して列を追加しないといけない。
サブタイプ数とサブタイプ固有の属性の数が少なく、アクティブレコードを使用する場合は適切。
-
具象テーブル継承
サブタイプごとにテーブルを作成。
-
クラステーブル継承
テーブルをオブジェクト指向のクラスであるかのようにみなして、継承を模倣する。
全てのサブタイプに共通する属性を含む基底型のテーブルを1つ作り、次にサブタイプごとに1つずつ追加のテーブルを作成し、基底型テーブルに対する外部キーの役割を持つ主キーを設定。
-
半構造化データ
サブタイプの数が多い場合や、頻繁に新しい属性を追加しなければならない場合は、LOB列(TEXT型など)を追加し、XMLやJSONなどの形式で属性名と値を共に格納することもできる。
-
6章 ポリモーフィック関連
-
目的:複数の親テーブルを参照する
-
アンチパターン:二重目的の外部キーを使用する
-
ポリモーフィック関連を定義する
issue_idのような外部キー列と現在の行が参照する親テーブルの名前を格納する文字列型の列を作成する。
→外部キーを指定できない(外部キーはテーブルを1つのみ指定するため)
→参照整合性制約を定義できない
-
-
アンチパターンを用いても良い場合
なるべくポリモーフィック関連の使用は避け、外部キー制約などを用いて参照整合性を保証する。
ORMフレームワークを用いる場合、ポリモーフィック関連を使用しないといけない状況もある。
-
解決策:関連(リレーションシップ)を単純化する
-
参照を逆にする
ポリモーフィック関連では本来あるべき関連が、逆さまになっている
-
交差テーブルの作成
複数の親テーブルそれぞれに対応した交差テーブルを作成し、各交差テーブルでは 子テーブルへの外部キーに加え、各親テーブルへも外部キーを定義する。
-
交差点に交通信号を設置する
この解決策の潜在的な弱点として許可したくない関連付けが許可されてしまう可能性がある。
→各交差テーブルの子テーブルの外部キーとなる列にUNIQUE制約を宣言することで複数の行に関連付いてしまうことを防げる。
-
共通の親テーブルの作成
- すべての親テーブルが継承する規定テーブルを作成し、基底テーブルを参照するために子テーブルに外部キーをくわえる
-
[テーブル間の関連(リレーションシップ)には、参照元テーブルと参照先テーブルがつねにそれぞれ1つしかないことを忘れないようにする]
7章 マルチカラムアトリビュート
-
目的:複数の値を持つ属性を格納する
≒タグ付け
-
アンチパターン:複数の列を定義する
例)
id desc tag1 tag2 tag3 111 保存処理でクラッシュ crash NULL NULL 222 パフォーマンス向上 printing performance NULL 333 XMLのサポート NULL NULL NULL デメリット
- 特定のタグが付けられたものを検索するには、3列全て取得しなければいけない。(tag1,tag2,tag3のどこに格納されているかわからないため)
- 値を追加するためにはその列がNULLか確認しなければいけない
- 一意性の保証が難しい
- 列数が足りなくなるかもしれない。
-
アンチパターンを用いれも良い場合
属性値の選択肢を限定できる場合
-
解決策:従属テーブルを作成する
8章 メタデータトリブル(メタデータ大増殖)
-
目的:スケーラビリティを高める
クエリの実行速度を劣化させずに、データが増加し続けるテーブルに対応できるようデータベースの構造を設計する
-
アンチパターン:テーブルや列をコピーする
- 行数の多いテーブルを、複数のテーブルに分割する(あるテーブルの属性の区別しやすいデータ値に基づいてテーブルを命名する)
- 列を複数列に分割する(別の属性の区別しやすい値に基づいて列を命名する)
デメリット
-
テーブルの増殖
-
データの整合性を管理しないといけない
CHECK制約などもあるが、新テーブルを作成するときにCHECK制約の値を修正しないと、受け入れるべき行を拒絶するテーブルを作成することになる。
-
データの同期
修正のために、対象テーブルから行を消し、別のテーブルに挿入しないといけない問う可能性がある
-
一意性の保証
行をあるテーブルから別のテーブルに移動させる時に主キーの値が他のテーブルの主キーの値と重複していないことを確認しないといけない。
-
テーブルを跨いだクエリ実行
複数のテーブルを参照する場合、UNIONで全ての分割テーブルの集合を構築
-
メタデータの同期
分割テーブルに新たな列を追加する場合、全ての分割テーブルに追加しなければUNIONを使用できなくなる(列が異なるため)
-
参照整合性の管理
親テーブルが複数に分割されていると、外部キーを定義できない
-
アンチパターンを用いても良い場合
- 過去データを最新のデータから分離するようなアーカイブが目的の場合
-
解決策:パーティショニングと正規化を行う
-
水平パーティショニング(シャーディング)の使用
行でテーブルを分割
行を分割する幾つかのルールを定めて論理テーブルを定義すれば、データベースが必要な作業を行なってくれる。
テーブルは物理的には分割されているが、1つのテーブルを扱うようにSQLステートメントを実行できる。
-
垂直パーティショニングの使用
列でテーブルを分割
列の一部のサイズが大きい場合や、滅多に使用されない場合にメリットがある(BLOB型やTEXT型など)
例)バグデータベースの製品テーブルにある各製品のインストーラーのコピーファイルなど
-
従属テーブルの導入
-
9章 ラウンディングエラー(丸め誤差)
-
目的:整数の代わりに小数値を使用する
整数以外の数値を格納し、正しく計算を行う
-
アンチパターン:FLOATデータ型を使用する
デメリット
-
丸目が避けられない
浮動小数点における一部の数値は、止むを得ず近似値に丸めなくてはならない
循環小数など
-
SQLでのFLOATの使用
誤差が生じると、等価性比較などを行うときに求めている結果と異なる結果となる
浮動小数点の誤差累積の影響は積算の場合に大きくなる
-
-
アンチパターンを用いても良い場合
FLOATはINTEGERやNUMERICなどのデータ型がサポートするよりも広い範囲の実数を扱う場合に適している。科学技術計算を行うアプリなど
-
解決策:NUMERIC型を使用
NUMERICまたはDECIMALを用いて固定精度の少数点数を表す
→指定した精度で数値を格納する
精度:値として使用可能な10進数の桁の総数
第二引数にはスケールを指定
スケール:小数点以下に格納できる桁数
[できる限りFLOAT型は使わないようにする]
10章 サーティワンフレーバー(31のフレーバー)
-
目的:列を特定の値に限定する
列に格納できる値を限定された値に制限する
-
アンチパターン:限定する値を列定義で指定する
例)CHECK制約を使用する
デメリット
-
列に許可されている値の取得が複雑
特定の値のみ使用されている場合、その値しか取得できない
ENUMデータ型を含む文字列
例)ENUM(’NEW’, ‘IN PROGRESS’, ‘FIXED’)
-
値の追加、削除をする場合、新たな値セットで列を再定義するしかない
-
値の廃止をする場合、過去のデータに影響が出る可能性がある
-
移植が困難
-
-
アンチパターンを用いても良い場合
有効値の変更が不要だと断言できる場合
-
解決策:限定する値をデータで指定する
新しい参照テーブルを作成し、許可する値を1行に1つずつ格納する。
元のテーブルに外部キー制約を宣言し、新しく作成したテーブルを参照させる。
メリット
-
値セットがSELECTで取得できる
-
参照テーブルの値の更新で許可する値の追加や削除が行える
-
新たに列を追加することで、値を廃止できる
例)active列を追加し、INACTIVE,ACTIVEの2択で値を許可する
-
移植が容易
-
[列に入力する値を限定するときは、値セットが固定されている場合はメタデータを、流動的な場合はデータを用いる]
11章 ファントムファイル(幻のファイル)
-
目的:画像をはじめとする大容量メディアファイルを格納する
-
アンチパターン:物理ファイルの使用を必須と思い込む
画像をファイルシステムに格納し、ファイルパスをVARCHARとしてデータベースに格納する人もいる
デメリット
-
データベースで画像へのパスの行を削除しても、パスの指定先のファイルは自動的に削除されない
-
トランザクション分離の問題
通常、COMMITするまで変更は他のクライアントにはわからないが、ファイルが外部にあることで、ファイルを削除した瞬間他のクライアントはすぐにそのファイルにアクセスできなくなる
-
ロールバック時の問題
データベースの行と対応するファイルを削除
→ロールバックした場合、データベースの行は元に戻るが、ファイルは削除されたまま
-
バックアップツール使用時に2段階のプロセスを行う必要がある。
-
SQLのアクセス権限が適用されない
-
ファイルはSQLデータ型ではないため、データベースで処理すべきチェックを行うアプリケーションコードを書かないといけない
-
-
アンチパターンを用いても良い場合
- DBの容量を減らす
- DBのバックアップが短期間で終了し、バックアップファイルの容量も抑えられる
- 画像がいるが外部にあれば、プレビューや編集が容易になる
上記の利点が特に重要であり、デメリットが深刻な問題でないと判断できる場合
画像処理の方法を計画するときには、十分な情報に基づき、よく検討した上で判断する
-
解決策:必要に応じてBLOB型を採用する
アンチパターンの問題を全て解決できる
- データベースに画像が格納されるため、読み込みのための追加のステップが不要
- 行を削除すると自動的に画像も削除される
- 画像の変更はコミットするまで他のクライアントに見えない
- トランフザクションをロールバックすると、以前の状態に戻せる
- 行の更新時にロックされる
- バックアップが取れる
- アクセス権限を設定できる
[データベース外部のリソースは、データベースでは管理できないことに注意]
12章 インデックスショットガン
-
目的:パフォーマンスを最適化する
インデックスを効果的に使用する
-
アンチパターン:闇雲にインデックスを使用する
インデックスを理解しないまま、インデックスの使用を判断すると以下の3つのミスどれかが起こる
- インデックスを全く定義しないか、少ししかインデックスを定義しなくなる
- インデックスを多く定義しすぎるか、役立たないインデックスを定義してしまう
- インデックスを活用しないクエリを実行してしまう
デメリット
-
インデックスを全く定義しない
インデックスの更新によってデータベースにオーバーヘッドが生じる
→全てのオーバーヘッドが無駄なわけではない
インデックスを定義していない列で値を検索するステートメントでは、一致する行を探すためにテーブル全体を検索しないといけなく、テーブルに対するクエリ発行回数の方が、テーブル更新回数よりも何倍も多いのでオーバーヘッド以上にメリットがある。
-
インデックスを多く定義しすぎる
インデックスのメリットを得られるのは、インデックスを使うクエリを実行するときのみ。
使用しないインデックスを定義してもメリットは受けられず、オーバーヘッドを増やしてしまう
-
インデックスが役立たないとき
インデックスを使えないクエリを実行する。
例)電話帳
名字が「チャールズ」の人を探すのは簡単→苗字順に名前が記載されているから
名前が「チャールズ」の人を探す→どんな名字の人でも名前が「チャールズ」の可能性があるため、電話帳全体を検索しないといけない。
-
アンチパターンを用いても良い場合
汎用的なデータベースを設計する場合、どのようなクエリを最適化しなければならないかを理解していなければ、最適なインデックスはわからない。
できる限り、十分な情報に基づいた検討をすべき。
-
解決策:「MENTOR」の原則に基づいて効果的なインデックス管理を行う
Measure(測定), Explain(解析), Nominate(指名), Test(テスト), Optimize(最適化), Rebuild(再構築)の頭文字
-
Measure(測定)
ほとんどのデータベースには、SQLのクエリ実行時間を記録する方法がある。
アプリケーションで最も多くの時間を消費するクエリを特定していれば、最適化でどこに注目すべきかわかる。
パフォーマンス測定中はクエリのキャッシュを全て無効化する必要がある。
-
Explain(解析)
クエリの処理が遅くなっている原因を解析
QEPの分析結果のレポートを取得し、原因を探す。
-
Nominate(指名)
QEPを読んで、クエリがインデックスを使わないでテーブルにアクセスしている箇所を探す。
-
Test(テスト)
インデックス作成後、再びクエリのプロファイリングを行う。
-
Optimize(最適化)
インデックスはコンパクトで、使用頻度の高いデータ構造であるため、キャッシュメモリに格納されやすくなる。
キャッシュに割り当てるシステムメモリの量を設定する
-
Rebuild(再構築)
長期にわたって行の更新や削除を行うことで、インデックスは次第に不均衡になっていく。
定期的にメンテナンスを行うことでインデックスの効率を高められる。
推測のみに基づいて、闇雲にインデックスをつけてはならない
[データとクエリについての理解を深め、MENTORの原則に基づいてインデックスを管理する]
-
13章 フィアオブジアンノウン(恐怖のunkown)
-
目的:欠けている値を区別する
NULLを含む列に対してクエリを書く
-
アンチパターン:NULLを一般値として使う、または一般値をNULLとして使う
-
式でNULLを使う
例)SELECT hours + 10 FROM Bugs;
hoursがNULLの場合結果はNULLを返す
NULL ≠ 0
NULLは不明(unkown)を表す
-
NULLを許容する列の検索
例)SELECT * FROM Bugs WHERE NOT (assigned_to = 123);
→NULLを割り当てられた行は返さない
NULLを用いた比較はすべて不明(unknown)を返すため
-
プリペアドステートメントでNULLを扱う
プリペアドステートメントでパラメータ化したSQLで、NULLを一般地のように扱うことも難しい。
NULLリテラルはパラメータに使えない
※プリペアドステートメントとは
→SQL文で値がいつでも変更できるように、変更する箇所だけ変数のようにした命令文を作る仕組みのこと。
-
NULLの使用を避ける
NULLの代わりに、不明または適用不能を意味する値を新たに定義する
例)NULLの代わりに-1を使用する
→列で計算をするときに混乱する。正しく計算するためにはWHERE句やCASE式でこの値を持つ行を除外しないといけない
-
-
アンチパターンを用いても良い場合
NULLを用いることがアンチパターンではない。NULLを一般値として扱ったり、一般値をNULLに相当するものとして扱うことがアンチパターン。
-
解決策:NULLを一意な値として使う
プログラミング言語は2値論理でSQLは3値論理の振る舞いをするため誤解が生まれやすい。
- スカラー式でのNULL
式 予想結果 実際の結果 理由 NULL = 0 TRUE NULL NULLはゼロではない NULL = 12345 FALSE NULL 不明な値が、ある値と等しいかわからない NULL<>12345 TRUE NULL 不明な値が、ある値と等しくないかわからない NULL + 12345 12345 NULL NULLはゼロではない NULL ‘string’ ‘string’ - 論理式でのNULL
式 予想結果 実際の結果 理由 NULL AND TRUE FALSE NULL NULLはFALSEではない NULL AND FALSE FALSE FALSE AND FALSEの真理値は全てFALSE NULL OR FALSE FALSE NULL NULLはFALSEではない NULL OR TRUE TRUE TRUE OR TRUEの真理値は全てTRUE NOT(NULL) TRUE NULL NULLはFALSEではない -
NULLの検索
IS NULL:NULLの場合TRUEを返す
IS NOT NULL:NULLではない場合にTRUEを返す
IS DISTINCT FROM:非等価演算子<>のような機能。対象データがNULLの場合も、常にTRUE・FALSEを返す(データベースによってサポート状況が異なる)
-
列にNOT NULL制約を宣言する
NULLがアプリケーションのポリシーに反する場合や、その列においてNULLが意味をなさない場合にはNOT NULL制約を宣言する。
-
動的なデフォルト
COALESCE関数を使用して、NULLの場合に代わりの値を入れる。
[データ型を問わず、欠けている値にはNULLを用いる]
14章 アンビギュアスグループ(曖昧なグループ)
-
目的:グループ内で最大値を持つ業を取得する
GROUP内の最大値が見つかった行の他の属性も取得する
-
アンチパターン:非グループ化列を参照する
-
単一値の原則
GROUP BYや集約関数を用いたクエリのSELECT句の選択リストに列挙される全ての列は、行グループごとに単一の値の行でないといけない。
-
SQLがクエリの意図を汲んでくれるとは限らない
-
よくある誤解
「別の列に対してMAX関数が使用されているから、どのbug_idを出力したいのかをSQLが適切に判断してくれるだろう」
→下記例のような場合はどの行から値を取得すべきかSQLで判断できない
- date_reported列で2つのバグが同じ報告日付を持ち、かつその日付がグループの最大値である場合、クエリはどちらのbug_id値を返せば良いか
- クエリで2つの異なる集約関数を実行した場合(MAXとMINなど)、集約関数の値はおそらくグループ内の2つの異なる行から取得される。この場合クエリはどちらのbug_idを返せば良いのか
- 集約関数で返された値と一致する行がテーブルにない場合、bug_idの値はどうすれば良いか。(このような状況は、AVG・COUNT・SUMを使う場合によく見られる)
-
-
-
アンチパターンを用いても良い場合
MySQLとSQLite以外ではエラーが起こる。
逆に上記2つでは、単一値の原則にそこまで厳密に準拠してないという事実を利用できる場合がある。
→関数従属性を持つ列のみにクエリを実行する場合
※関数従属性とは
2つの属性の間で片方の値を決定するともう一方の値が一意に決まるという性質のこと
-
解決策:曖昧でない列を使用する
-
関数従属性のある列のみにクエリを実行する
クエリから曖昧な列を排除する
-
相関サブクエリを使用する
例)同じ製品でより新しい日付を持つバグを検索する相関サブクエリを実行する
※最善のパフォーマンスは得られない
-
導出テーブルを使用する
サブクエリを導出テーブルとして使用する。
サブクエリで返された列の値が複数行と一致する場合、1つのグループに複数行が取得される可能性がある。
※導出テーブルとは
リレーショナルデータベースで、表(テーブル/実表)に対してSQLによる問い合わせを行うことで生成される仮想的な表。
-
JOINを使用する
外部結合を使い、クエリでNULLを検出した場合、対象の行が存在しなかったものだとわかる。
-
他の列に対しても集約関数を使用する
-
グループごとに全ての値を連結する
GROUP_CONCAT:カンマ区切りの文字列を返す
各グループのある列にある全ての値が含まれるため、特定はできない
SQL標準に準拠していないため、サポートされてない場合がある
-
[曖昧なクエリ結果を避けるために、単一値の原則に従う]
15章 ランダムセレクション
-
目的:サンプル行をフェッチする
ランダムな結果を返す。
直接データベースからクエリでサンプルを抽出する
-
アンチパターン:データをランダムにソートする
ランダムにソートを行い、最初の行を取得する
デメリット
-
インデックスからメリットを得られない
→比較基準がなく、実行するたびに結果が変わるため
-
すべての結果を一時的なテーブルとして保存し、物理的に行を入れ替える処理を行う(テーブルスキャン)ため、データセットのサイズが大きくなるにつれてパフォーマンスも落ちる
-
-
アンチパターンを用いても良い場合
- データセットが小さく、スケールしない場合
-
解決策:特定の順番に依存しない
-
1と最大値の間のランダムなキー値を選択する
- 主キーが1から始まり、連続している必要がある
-
欠番の穴の後にあるキー値を選択する
-
すべてのキー値のリストを受け取り、ランダムに1つを選択する
テーブル全体のソートを回避し、各キー値もほぼ平等に選択されるが、下記のような弱点もある
- リストのサイズが大きくなる
- クエリを2回実行する必要がある(主キーリストの取得、ランダムに選択された主キーの取得)
-
オフセットを用いてランダムに行を選択する
行数をカウントし、0と行数までの間の乱数を返す
キー値が連続していることが前提とできず、各行が平等に選択される必要がある場合に使用できる
-
ベンダー依存の解決策
データベース製品によっては独自の解決策が実装されている
例)OracleのTABLESAMPLE句
[クエリは最適化できないものもあるため、別のアプローリも考える]
-
16章 プアマンズ・サーチエンジン(貧者のサーチエンジン)
-
目的:全文検索を行う
-
アンチパターン:パターンマッチ述語を使用する
LIKE述語など
デメリット
-
パフォーマンスの低下
インデックスのメリットを得られないため、テーブルの全てをスキャンするため
-
意図しないマッチが生じてしまう
例)oneを含むテキストを検索→money, prone, lonelyなどもマッチする
-
-
アンチパターンを用いても良い場合
アンチパターンのクエリでも、シンプルな用途に対して正しく使用すれば、大きな価値を得ることができる。
使用頻度が低いクエリなどは、インデックスを更新するコストより安い
-
解決策:適切なツールを使用する
-
ベンダー拡張
主要なデータベース製品は、全文検索というありふれた要件に対する独自の解決策を用意している。
-
例)MySQLのフルテキストインデックス
- CHAR, VARCHAR, TEXT型にフルテキストインデックスを定義できる。
- キーワードの検索にはMATCH関数を使用する
その他の製品にもそれぞれ独自の解決策がある
-
[問題解決のために必ずしもSQLを使用する必要はない]
17章 スパゲッティクエリ
-
目的:SQLクエリの数を減らす
クエリを優雅に効率的に書く
-
アンチパターン:複雑な問題をワンステップで解決しようとする
1つのクエリですべてのタスクを処理しようとする
デメリット
-
意図に反した結果
デカルト積が生じる
→クエリで指定する2つのテーブルが関連を制限する条件を持たないときに生じる
制限がないと1つのテーブルの各行がもう一方のすべての行とペアになってしまう
-
クエリの記述や修正、デバッグが難しくなる
-
シンプルなクエリと比較して、最適化処理や高速な実行が難しくなる
-
-
アンチパターンを用いても良い場合
- 単一のクエリをデータソースに結びつけてアプリケーションに表示するような、プログラミングフレームワークやビジュアルコンポーネントライブラリを使用している場合
-
解決策:分割統治を行う
-
求めるもの一つずつクエリを分割する
- 分割したクエリからはデカルト積が生じない
- 新たな要件が追加されたとき、複雑なクエリをさらに複雑にするよりも、単純なクエリを新たに書く方が簡単
- 単純なクエリの方がスムーズかつ確実に実行できる
- コードレビューなどではシンプルな複数のクエリを説明する方が簡単
-
UNIONで1つの結果にまとめる
- サブクエリの結果を他のサブクエリを区別するための行を忘れないように
- 両方のサブクエリの列に互換性がある時のみ使用可能
-
CASE式とSUM関数を組み合わせる
例)SUM(CASE b.status WHEN ‘FIXED’ THEN 1 ELSE 0 END) AS count_fixed
-
似たようなクエリを複数作成する場合はSQLを用いたSQLの自動的な記述をする
コード生成:コンパイルや実行が可能なコードをコードから出力する
-
[SQLでは、1行のコードで複雑な問題を解決できると思える場合があるが、状況に応じてクエリを分割することも検討するようにする]
18章 インプリシットカラム(暗黙の列)
-
目的:タイプ数を減らす
列名の指定
-
アンチパターン:ショートカットの罠に陥る
ワイルドカードや暗黙的な列の指定によってタイプ数を減らす
-
リファクタリングにおける問題
暗黙的な列を使ったINSERTステートメント(INSERT時に列を指定しない)では、列が定義されている順番で全ての列に値を与えなければいけないため、列定義に変更があった場合に、エラーがでる。さらに悪い場合、誤った列に値が格納される。
また、ワイルドカード使用時には列は定義順に基づいて参照され、$row[10]といった形でデータを使用している場合に、別の人が列を削除などしてしまうと、使用する値が変わってしまう。
-
ワイルドカードを使用していると、スケールに合わせて取得列数も増えるためパフォーマンスが落ちる
ワイルドカードを使いながら、一部のみ除くといったことはできない
-
-
アンチパターンを用いても良い場合
- 1回しか使用しないクエリ
-
解決策:列名を明示的に指定する
ワイルドカードなどを使用せず、必要な列名は明示的に指定する
明示的に指定するメリット
- 列の順番が変更されても、クエリ結果の列位置は変わらない
- 列が追加されても、クエリ結果に影響はない
- 使用している列が削除された場合、エラーが出てすぐに原因を突き止められる
必要最低限の列のみ取得することでタイプ量が減り、効率的かつ安全になる。
[必要な列のみ指定する]
19章 リーダブルパスワード(読み取り可能パスワード)
-
目的:パスワードのリカバリーとリセットを行う
-
アンチパターン:パスワードを平文で格納する
-
パスワードの格納
平文で格納するのは安全ではない
- アプリケーションクライアントからデータベースサーバーに送信されたSQL文のネットワークパケットを傍受することは、見かけほど難しくない。
- データベースサーバー上のSQLクエリログを探す方法もある
- データベースのバックアップファイルからデータを読み取れる。
上記のように攻撃者には、パスワードを盗むチャンスがいくつもある。
-
パスワードの認証
-
ユーザーの入力内容とデータベースのパスワードが一致しているかによって認証をする際、パスワードが平文として格納されていると比較も平文として行われる。
ユーザーが入力したパスワードを平文としてSQLクエリに挿入されるので攻撃者にパスワードを晒すことになる。
-
-
パスワードを電子メールで送信する
平文のパスワードを電子メールで送るのは非常に深刻なセキュリティリスク。
攻撃者はさまざまな方法で、電子メールの傍受、記録、保存を行える。
-
-
アンチパターンを用いても良い場合
-
外部のサービスにアクセスするためのパスワードを扱う場合
自分のアプリケーションがクライアント側になるため、解読可能な形式でパスワードを格納しなければならない。(平文ではなく、アプリケーションで解読できる形が良い)
-
本人識別と認証を区別する考えも大切。
本人識別:自分が誰であるかを申告すること。ユーザーは誰の名前でも自由に申告できる。
認証:自分が名乗った人物であることを証明すること。パスワードは認証を行う最も一般的な方法である。
-
-
解決策:ソルトをつけてパスワードハッシュを格納する
-
ハッシュ関数を理解する
一方向性の暗号学的ハッシュ関数を用いて暗号化する
例)SHA-256アルゴリズム
→パスワードを256ビットの文字列に変換する。16進数の文字列64文字
ハッシュ値から入力文字列を復元することはできない。
-
SQLでのハッシュの使用
- 列を固定調のCHAR列として定義する CHAR(64)
- ユーザー入力に同じハッシュ関数を適用し、その結果をデータベースに格納されたハッシュ値と比較する
-
ハッシュにソルトをつける
-
辞書攻撃などでありふれた言葉が使われたパスワードであれば同じハッシュ値を見つけ出すことも難しくはない。
-
暗号化前のパスワードにソルトをつけることで防げる
ソルト:ハッシュ関数に渡す前にパスワードに連結する無意味な文字列のこと
-
それぞれのパスワードに異なるソルト値を付加することで、攻撃者のハッシュデータベースのハッシュとは一致しにくくなる。
-
-
SQLからパスワードを隠す
上記の方法だけでは、攻撃者によってネットワークパケットが傍受された場合や、SQLクエリが記録されたログファイルが攻撃者の手に渡ってしまった場合に、パスワードを読み取られてしまう。
→SQLクエリでパスワードを平文として使わない。アプリケーションコードでハッシュを計算し、SQLクエリではハッシュのみを用いるようにする。
まだユーザーのブラウザとウェブアプリケーションサーバー間のネットワークではデータが傍受される可能性があるため、セキュアHTTP(HTTPS)を使用する
-
パスワードをリカバリーするのではなく、リセットする
- パスワードを忘れたユーザーを助ける
ハッシュを格納しているため、パスワードのリカバリーはできない。
- 一時パスワードを電子メールで送る。
- 短時間で一時パスワードを無効化することでセキュリティを強化することができる。ユーザーの初回ログイン時にパスワード変更を強制する。
- リクエストをデータベーステーブルに記録し、一意のトークンを識別子として割り当てる。電子メールにトークンを記載して送信することで、第3者がパスワードリセットをリクエストしてきた場合でも、アカウントの実際の所有者のみにトークンを記載した電子メールが送信される。
-
[自分が読み取れるパスワードは攻撃者も読み取れる]
20章 SQLインジェクション
-
目的:動的SQLを記述する
SQLを文字列として作成し、文字列内にアプリケーション変数を挿入する
-
アンチパターン:未検証の入力をコードとして実行する
SQLインジェクションの典型例
挿入された値によって、元のSQL文の実行に続けて別のSQL文も実行される
-
アクシデントは起こる
- O’HARE(オヘア)のような名前を使用すると「’」が引用符文字とされ、’O’という部分で終了してしまう
-
ウェブ最大のセキュリティ脅威
例)攻撃者は、リクエストパラメータがSQLステートメントでどのように使われるかを推測し、悪用するために巧妙な文字列を送信する
- SQLインジェクションは、SQLステートメントが解析される前に構文を解析する
-
対処法の追求
-
値のエスケープ
引用符を全てエスケープ
-
プリペアドステートメント
- SQL文字列内に動的に値を挿入する代わりに、クエリの前処理時にパラメータのプレースホルダーを文字列内に残し、準備したクエリを実行する前にクエリパラメータとして値を渡す。
- SQLインジェクションの最も強力な防御手段
デメリット
-
値のリストを1つのパラメータにすることができない
カンマ区切りの最初の数値のみが文字列パラメータとして渡されたかのように処理される
-
テーブル識別子もパラメータとして扱えない
-
列名もパラメータとして扱えない
-
SQL予約後もパラメータにならない
-
ストアドプロシージャ
プロシージャ定義時に解析された性的なSQLステートメントが記述されている。
しかし、ストアドプロシージャでも安全性の低い動的SQLを使用できる。
-
データアクセスフレームワーク
利用者がSQLステートメントを文字列として直接記述できるフレームワークは、SQLインジェクションのリスクからコードを保護できない。
安全なSQLコードを保証するフレームワークはない。
-
-
-
アンチパターンの見つけ方
ほぼ全てのデータベースアプリケーションは、動的にSQLステートメントを構築する。
SQLステートメントの一部分を文字列連結や変数の挿入を用いて構築する場合、そのステートメントによって、攻撃を受けるリスクにさらされる。
-
アンチパターンを用いても良い場合
セキュリティ上の脆弱性のため正当化する理由がない
-
解決策:誰も信用してはならない
-
入力のフィルタリング
その入力にとって無効な文字をはじめから全て取り除くようにする
-
動的値のパラメータ化
動的な部分がシンプルな値から構成されている場合、プリペアドステートメントを使える。
SQLステートメントを解析した後にパラメータの値を渡すので、SQLインジェクション攻撃は、パラメータ化されたクエリの構文を改変できなくなる。
アプリケーション変数をリテラル値としてSQL文字列と連結する必要がある場合は、プリペアドステートメントを使うべき
-
動的値を引用符で囲む
SQLインジェクションの対策にはほぼ全ての場合において、プリペアドステートメントが最善策だが、ごく稀に、クエリオプティマイザーがおかしな判断をしてしまうことがある。
SQLステートメントの中に直接値を挿入する方が良い場合もある。その際には、十分に注意して文字列を引用符で囲む必要がある。
例)account_status = ? というような指揮を使う場合
-
ユーザーの入力をコードから隔離する
テーブル識別子や列識別子、SQL予約後を動的にする場合、リクエストパラメータの値を用いて定義済みの値を参照し、SQLでは定義済みの値を使うという技法がある
- ユーザーの入力値をキー、SQL列名を値にマッピングする
directions配列を宣言するsortorders配列を宣言する。また、ユーザーの入力値をキー、SQL予約後ASC、DESCを値にマッピングする - ユーザーの入力値が配列内に存在しない場合のデフォルト値を
direction変数として設定するsortorder変数と - ユーザーの入力値が、
directionsで宣言した配列キーと一致する場合、対応する値を使用するsortordersと
この処理を行うと、コード内で宣言した、安全性の保証された値のみが格納されているため、
direction変数を安全に使用できるsortorder変数と メリット
- ユーザーの入力内容とSQLクエリの連結が行われないため、SQLインジェクションのリスクが減る
- SQLステートメントのどの部分でも動的にできる
- ユーザー入力値の妥当性確認を、簡単かつ効率的に行える
- ユーザーインタフェースから、データベースクエリ内部の詳細を分離できる
- ユーザーの入力値をキー、SQL列名を値にマッピングする
-
他者にコードをレビューしてもらう
ガイドライン
- アプリケーション変数や文字列連結、文字列置換によって構築されているSQLステートメントを特定する
- SQLステートメントで使われている、全ての動的コンテンツの起点を辿り、外部ソースからくる全てのデータを特定する。(ユーザー入力、外部ファイル、環境周り、外部のウェブサービス、サードパーティーのコード、データベースから取得した文字列など)
- これらの外部コンテンツには全て、潜在的なリスクがあると想定する。フィルター、バリデーター、マッピング配列などを用いてこれら信頼性の低いコンテンツを変換する。
- プリペアドステートメントまたは検証済みのエスケープ関数を用いて、SQLステートメントと外部データを組み合わせる
- 他にもストアドプロシージャなどの、動的SQLステートメントが隠れている可能性がある場所をチェックする
-
[ユーザーには、値の入力は許可しても、コードの入力は許可してはいけない]
21章 シュードキー・ニートフリーク(擬似キー潔癖症)
-
目的:欠番を詰める
-
アンチパターン:隙間を埋める
かけている行を埋める方法
-
欠番を割り当てる
新しい行に、検出した最も値の小さい欠番番号を割り当てる。
最も小さな欠番の値を特定するために不要な自己結合クエリを実行しないといけない。
-
既存行に番号を振り直す
値が連続するように、既存行のキーの値を更新する。
上記2種類の方法はどちらも競合状態を引き起こす可能性がある。
振り直す前にその行を参照していた全ての子レコードに反映する必要もある。
一時的に欠番がなくなったとしても、次に挿入される行は最後に生成した値より1つ大きな値のため、現在のテーブルの最大値ではない。
擬似キーは再利用してはいけない
-
-
アンチパターンを用いても良い場合
擬似キーの変更を正当化する理由はない。
-
解決策:擬似キーの欠番は埋めない
-
行のナンバリング
-
行番号と主キーを混同してはいけない
主キー:テーブルにおける1行を識別する
行番号:結果セットにおける行の順序番号
主キーの値と行番号は必ずしも一致しない
-
行番号の使用には正当な理由がある
例)結果から行のサブセットを返したい場合
=ページネーション
ウィンドウ関数のROW _NUMBER関数を用いる
-
-
GUIDの使用
グローバル一意識別子(Globally Unique IDentifier)
128ビットの疑似乱数で、同じ識別子が生成される可能性は極めて低いため、一意の値とみなされる。
メリット
- 複数のデータベースサーバー間で、重複した値を生成することなく、並行して疑似キーを生成できる
- 欠番に関する不満を誰も口にしなくなる
デメリット
- 値が長いためタイプしづらい
- 値がランダムなので、値からパターンを推測したり、値の代償から生成された順番を推測できない
- GUIDの格納には16バイト必要。一般的な4バイトの整数と比べて多くスペースが必要になり、実行時間も長くなる
-
自然キーの使用
- 識別しやすい意味を表す文字列や数値を使用する
[疑似キーは、行を一意に識別するためにある。行番号と混同しないようにする]
-
22章 シー・ノー・エビル(臭い物に蓋)
-
目的:簡潔なコードを書く
-
アンチパターン:肝心な部分を見逃す
このアンチパターンに陥る2つのパターン
1.データベースAPIの戻り値を無視する
2.アプリケーションコード内に点在するSQLしか読まない
-
診断せずに判断する
戻り値や例外チェックのために自分のコードに手を加える必要はないと考える傾向がある。
-
見逃しがちなコード
-
SQL文字列を組み立てるアプリケーションコード側からデバッグを開始することは簡単ではない。
SQLを構築するコードを調べることによって、デバッグの際に驚くほど多くの時間とエネルギーを浪費してしまう。
-
-
-
アンチパターンを用いても良い場合
エラーに対して全く何もする必要がない時は、エラーチェックを省略できる。
-
解決策:エラーから優雅に回復する
-
リズムを維持する
- データベースAPI呼び出しの戻り値と例外チェックは、ステップにミスがないことを保証するための最善策。
-
ステップをたどり直す
デバッグには実際に構築されたSQLクエリを使用することも重要。
綴り間違いや括弧の不一致など、コードでわかりにくく不可解になりがちな、単純ミスを簡単に見つけられる。
- データベースAPIの引数に渡すその場で直接SQLを組み立てるのではなく、まず一時変数を使ってSQLクエリを構築してからAPIに渡す。そうすることで、SQLに入った変数を使用前に調べられる。
- アプリケーションとは別の出力先にSQLを出力するようにする。(ログファイルやデバッグコンソールなど)
- ウェブアプリケーション出力のHTMLコメント内にSQLを表示しないようにする。ユーザーは誰でもページのソースを見ることができるため、SQLクエリを読んだ攻撃者に、データベース構造について多くの情報を与えてしまう。
-
[コードのトラブルシューティングは、それだけで十分に大変な作業。闇雲に進めても、作業を遅らせるだけ。]
23章 ディプロマティック・イミュニティ(外交特権)
-
目的:ベストプラクティスを採用する
- バージョン管理
- ユニットテストや機能テスト
- ドキュメント、仕様書など
-
アンチパターン:SQLを特別扱いする
アプリケーションコードの開発ではベストプラクティスを受け入れる開発者でも、データベースではこれらの慣習が免除されると考える傾向がある。
-
アンチパターンを用いても良い場合
1回限りのテストや、ユーザーの質問に答えるためのSQLクエリの場合。
使い終わったコードをすぐに削除する。気が進まないのであれば、そのコードは保存しておく価値がある。
-
解決策:包括的に品質問題に取り組む
-
文書化
データベースの要件と実装は文書化すべき
必要なもの
-
ER図
テーブルとその関連を表す
-
テーブル、列、ビュー
テーブルが表現するエンティティの分類についての説明
参照テーブルや交差テーブル、従属テーブルにも説明が必要
列には、その列にとって妥当な値は何か、単位、NULLを許容するか、一意性制約があるかなどを記述
ビューには、目的、想定される用途、更新可能かなどを記述
-
関連(リレーションシップ)
-
トリガー
- データのバリデーションや変換、データベース変更のロギングなどトリガーに実装しているビジネスルールなどを記載
-
ストアドプロシージャ
- どのような問題を解決するためのものか、データに対する変更を行うかどうか、入出力パラメータのデータ型と意味など
-
SQLセキュリティ
- データベースユーザーの定義、各ユーザーのアクセス権、提供するロールと対象ユーザーなど
-
データベースインフラストラクチャ
- データベース製品の種類やバージョン、ホスト名など
-
オブジェクトリレーショナルマッピング(ORM)
- ORMライブラリを使ったクラス群を通して実装されるビジネスルールを記載
-
-
バージョン管理
-
データ定義スクリプト
- CREAT TABLEなどのデータベースオブジェクトを定義するSQLスクリプト
-
トリガーとプロシージャ
-
ブートストラップデータ
-
データベースの初期状態として参照テーブルに格納するデータ
=シードデータ
-
-
ER図とドキュメント
-
データベース管理スクリプト
アプリケーションとデータベースコードは、同じバージョン管理リポジトリを用いるようにする
-
-
テスティング
- テーブル、列、ビュー
- 存在すべきテーブルやビューが存在しているかのテスト
- 制約
- 制約に違反したデータで実際にエラーが生じるかをテスト
- トリガー
- トリガーが発動するステートメントを実行し、トリガーが意図した処理を行なったかどうかを確認するためのクエリを実行
- ストアドプロシージャ
- 入力範囲やデータベースを更新する副作用などプロシージャの持つ全ての側面をテスト
- ブートストラップデータ
- 初期データの存在確認
- クエリ
- 結果セットに適切な列名とデータ型が含まれていることを確認
- オブジェクトリレーショナルマッピングを使用したクラス
- 入力に対して良きされた振る舞いをすることや、無効な入力を拒絶することを確認
テストのいずれかが失敗する場合、アプリケーションが間違ったデータベースインスタンスを使用している可能性もある。
[アプリケーションと同じく、データベースに対しても、ソフトウェア開発のベストプラクティスを適用し、文書化、テスト、バージョン管理を行う]
- テーブル、列、ビュー
-
24章 マジックビーンズ(魔法の豆)
-
目的:MVCのM(モデル)を単純化する
-
アンチパターン:モデルがアクティブレコードそのもの
MVCアプリケーションのすべてのモデルクラスがアクティブレコードの基底クラスを継承するという規約がアンチパターン
- アクティブレコードはモデルをデータベーススキーマに強く依存させてしまう
- データベースをリファクタリングして新たなデータ構造を表す場合には、モデルクラスだけでなく、そのモデルクラスを使うアプリケーションのコードも変更する必要がある
- 新たなコントローラを追加する場合、モデルとのやり取りを行うために、既存のコードに似たようなコードをコントローラに書く必要がある
- アクティブレコードはCRUD機能を公開してしまう
- モデルクラスを使用する他のプログラマーが、意図された用法を無視して、CRUD操作メソッドを通じて直接データを更新してしまう
- アクティブレコードはドメインモデル貧血症をもたらす
- モデルがすべき仕事を表現する新たなメソッドを追加せずに、アクティブレコードの基底クラスを単に継承してしまう
- 複数のコントローラにロジックが描かれ、モデルの振る舞いの凝集度が低下する
- マジックビーンズのユニットテストは困難
-
モデルのテスト
モデルをアクティブレコードと同じクラスにしているため、モデル自身の振る舞いのテストを、データベースアクセスから分離して行うことができない。
-
ビューのテスト
特定のHTML要素のレンダリングと解析のために、複雑で時間のかかるコードを実行しなければいけない
-
コントローラのテスト
複数のコントローラにおけるコードの重複を招くため、コントローラのテストも複雑になる。重複したコードも全てテストが必要。
-
- アクティブレコードはモデルをデータベーススキーマに強く依存させてしまう
-
アンチパターンを用いても良い場合
アクティブレコードはシンプルなCRUD操作にとって便利なパターン。
-
解決策:モデルがアクティブレコードを「持つ」ようにする
[モデルはテーブルから分離させる]
25章 砂の城
-
目的:サービスの安定稼働
-
アンチパターン:想定不足
- トラブルは当然起きるものと想定しておく
- 実際に運用時に事象が起きた時どのような対応をするべきかといった検討も必要
-
アンチパターンを用いても良い場合
対策を講じれば講じるほどコストが嵩むため、システムが休止する損失や事業計画への影響などを考慮し、オーバースペックにならないように威をつける
-
解決策
-
ベンチマーク
どの程度まで処理可能かベンチマークしておく。
- 現実に即したシナリオを用いる
→アプリケーションと同じ負荷をかける
- 実際のアプリケーションで用いるデータと同程度のデータサイズで実施
- 同じハードウェアやOSを使用
-
テスト環境の構築
- 本番環境と同じものを1セット用意する
-
例外処理
適切な例外処理=どのような例外が発生するか想定できている
-
バックアップをとっておく
-
高可用性
マシンが冗長化する仕組みを考えておく
-
ディザスタリカバリ
データセンターの一部又は全体が使用不能になった際に、他のデータセンターで処理を引き継ぐ
-
運用ポリシーの策定
-
[どれだけ盤石な基盤を築けるかは、自分がどれだけのインシデントを想定しているかが鍵]
付録 正規化のルール
-
リレーショナルとは
- テーブルそれ自体のことを意味するか、もしくは同じテーブル内の列間の関係性を意味する。
リレーションの成立基準
- 行に上下の順番がない
- 列に左右の順番がない
- 重複行を許可しない
- 全ての列は1つの型を持ち、各行に1つの値を持つ
- 行に隠されたコンポーネントがない
-
正規化とは
- 人間が理解できる形で、現実世界の事実を表現する
- 事実の格納方法から冗長性を排除し、データの異常や不整合を防ぐ
- 整合性制約をサポートする
-
第1正規系
基本要件
- テーブルがリレーションであること
- 繰り返しグループがないこと
-
第2正規系
- テーブルが複合主キーを持つこと
-
第3正規系,ボイスコッド正規系
- 第3正規系→全ての非キー属性がテーブルのキーに従属しなければならない
- ボイスコッド正規系→キー列にも上記ルールが適用される
ボイスコッド正規系はキーになる可能性がある列の組み合わせがテーブルに複数ある場合にのみ該当する
-
第4正規系
- 1つの交差テーブルで多対多関連を表そうとすると、第4正規系に違反する
- 各タイプの多対多関連ごとに1つの交差テーブルをもつように変更する
-
第5正規系
- ボイスコッド正規系を満たし、複合主キーを持たないテーブルは、すでに第5正規系
正規化のルールは冗長性を減らし、データの生合成を保つために用いられる、ごく常識的な技法である
Discussion