本記事について
本記事では『失敗から学ぶRDBの正しい歩き方』(著:曽根壮大さん)についてまとめます。
ただ筆者は本書については二度目、今の自分に必要な知識や忘れておきたくない箇所についてまとめておくだけのため、書籍全体についてまとめる記事ではありません。
本書の章まとめ
- データベースの迷宮
- 失われた事実
- やりすぎたJOIN
- 効かないINDEX
- フラグの闇
- ソートの保存
- 隠された状態
- JSONの甘い罠
- 強すぎる制約
- 転んだ後のバックアップ
- 見られないエラーログ
- 監視されないデータベース
- 知らないロック
- ロックの功罪
- 簡単すぎる不整合
- キャッシュ中毒
- 複雑なクエリ
- ノーチェンジ・コンフィグ
- 塩漬けのバージョン
- フレームワーク依存症
2. 失われた事実
RDBの基本として、時間軸と直行するような設計が大切であるが、それと同時に、時間軸と直行しないデータ=履歴の保存も大切になる。
筆者が携わる実業務でもいくつかの履歴テーブルを用いており、最初は「なるほど~」くらいの認識だったけど、今ではかなり重要なデータかつ助かったところも大きいなと思い返しました。
過去の事実が失われる
- 値が失われる
- 例)商品管理テーブルの商品の価格を変更してしまうと、不整合がおきるきっかけになる
- 過程が失われる
- 例)状態変化の保存がされていない
- 下記のユーザの行動例だと途中経過がわからなく調査が難しい
-
- 発注済み > キャンセル > 再発送 > 配送済み
-
- 発注済み > 配送済み
-
本書にも書いてありましたが、正常系ではなかなか問題として見えにくいところが難しいなと思いました。
実際、筆者が履歴テーブルを調査した際も、どちらかというと異常パターンによって生じた内容でしたし...。
履歴のトレードオフ
もちろん、履歴テーブルの作成によるデメリットも挙げられていました。
- レコード保存数増加によるテーブルサイズの増加
- 主キー検索では難しいため、テーブルサイズ肥大化に伴う検索速度の劣化
筆者の一個人的な感想では、これは仕方ないことなのかなと。
どちらかというと、サービス上では履歴テーブルには頻繁にアクセスしない設計をし、調査系統の際に、「履歴テーブル取っておいてよかったぁ」という感覚でいます。
4. 効かないINDEX
ここではINDEXが効かないアンチパターンについてまとめておきます。
※INDEXそのものの仕組みについてここでは深くは語りません。
検索結果が多い、全体の件数が少ない
INDEXを貼る基本的な条件としては以下の二つが紹介されていました。
- 検索結果がテーブル全体の20%以内
- 検索対象テーブルが十分に大きい(数万~数十万以上)
条件にその列を使っていない
そもそもINDEXを貼ったカラムを検索条件にいれていないと使われませんが、意外な落とし穴もあったりします。
例えば、次のSQLではage
にINDEXが貼られていても効かないパターンになります。
-- INDEXが効いていない例
select * from users where age * 10 > 100;
上記のSQLはage*10が検索対象になってしまうため、INDEXが効かずにフルスキャンになってしまうそうです。
そのため下記のように書き換えるとINDEXが有効活用できるのだとか。
-- INDEXが効いている例
select * from users where age > 100 /10;
うーん、生SQLを書くときに意外と見落としがちな気がしますね...。
カーディナリティの低い列に対する検索
※カーディナリティ:列に格納されるデータの値に含まれる種類
カーディナリティが低いとは、データの重複が多いカラムのことで、例えば性別(男性/女性)だったりする。
つまり最初に言っていた、検索結果が多い=「検索結果がテーブル全体の20%以内」に当てはまらないにつながります。
(まぁ、性別だけ(単一カラム)にINDEXを貼るってどんな状況?みたいに思いますが...)
あいまいな検索
標準では、前方一致のみINDEXが効くようになっているようです。
言われてみれば、前方一致の検索は早かった印象あります(筆者の体感です)。
5. フラグの闇
この章では、無闇に削除フラグを立てるな、という話でしたが、ここは筆者の体験談とは少し異なった印象でした。
物理削除を嫌って論理削除を使おうとした結果、UNIQUE制約が使えない、カーディナリティが低くなる、といったデメリットを上げており、削除済み用のテーブルを作成してデータを分離していく、といった代替案を上げていました。
ただ、筆者の環境ではいわゆる削除フラグは割と利用しており、そもそも複合UNIQUE制約を用いていたりするため、余計に慣れてしまっている気もします...。
9. 強すぎる制約
## 外部キー制約によるデッドロック
FOREIGN KEY
は便利だったり、カラム同士の結びつきがわかりやすくなる設計だと思っていましたが、想像以上に制約が強いようです。
例えば、外部キー制約の子テーブルを更新しようとしたときに親テーブルに共有ロックを自動でとってしまうため、デッドロックの起因となりかねないとのこと。
また、排他ロックをとったとして、今度は正しく処理順番を待つようになるとパフォーマンスのボトルネックになってしまうのだとか。
筆者も自作アプリ制作で勉強していた時は多用していたため、割と大きなギャップを感じました。
ちなみに、筆者の環境でもFOREIGN KEYは使われていません。
制約と規約
※ここでは、規約はアプリケーション側で行うバリデーション等によるデータの担保を表現しています。
本書では、ほかにもやりすぎたCHECK制約やDOMAINに似たENUMなども強すぎる制約の例として挙げられていました。
規約のほうでは、ヒューマンエラーをはじめとしたバグに弱いという欠点はありつつも、アプリケーション側はデプロイで修正ができる点が大きなメリットとして挙げられていました。
そのため、強すぎる制約はRDBMSではあまり行わず、アプリケーション側で規約として持たせる選択肢を持っておくのは大切だなと思いました。
制約の段階
本書で挙げられた制約の段階分けとしては以下の通りでした。
- 制約なし
- 指定された型ならばどんなデータでも受け付ける
- 弱い制約
- NOT NULL, UNIQUE, FOREIGN KEY
- 強い制約
- CHECK, EXCLUDE (制約内容として、一般的な事実の範囲に収まる)
- 強すぎる制約
- システムの仕様に基づく、ビジネスルールに基づくなど
筆者の環境でも弱い制約までしか見たことがない気がします。
上記の強すぎる制約は先述のように、規約のほうでバリデーションとして設けるのが適切だなと改めて感じました。
10. 転んだ後のバックアップ
本書では3つのバックアップ手法が紹介されていた。
論理バックアップ
SQLやCSVとして、DBそのものを再構成できるようにバックアップを取る手法のこと。
MySQLではmysqldump
、PostgreSQLではpg_dump
コマンドで実行できる。
サービスの初期、つまりデータサイズが大きくない場合に使いやすい。
つまりデメリットとして、バックアップのファイルサイズが大きくなりやすいこと、バックアップとリストアともに時間がかかりやすいことが挙げられる。
また、バックアップ時点にしか復旧できないため、バックアップ後に更新されたデータについては復旧対象にはならない。
物理バックアップ
DBの物理ファイル自体をそのままバックアップする手法のこと。
OSのcp
やrsync
、専用のツールを使って実行できる。
メリットとしては、最小限のサイズを取得でき、バックアップやリストアに時間がかからないこと。
しかし、専用ツールを使わない場合、基本的にはDBを停止する必要がある。
またデメリットとして、論理バックアップほど移植性が高くないこと、バックアップ時にしか復旧ができないことが挙げられていました。
ポイントインタイムリカバリ(PITR)
特定の日時の状態にデータをリストアする手法のこと。
障害発生時点のデータを復旧するために、バックアップファイルと更新ログが必要であり、MySQLではバイナリログ、PostgreSQLではアーカイブログと言ったりする。
ログとバックアップファイルの両方を保存する必要があるため、バックアップサイズは大きくなり、復旧手順も煩雑になっているデメリットももちろんあるとのこと。
13. ロックの基本
ロックについて紹介されていたのでここで今一度まとめておこうと思いました。
筆者自身も数か月前に現場でも少しロックを意識してみてねと言われたこともあるので...。
ロックの種類
-
排他ロック(X lock)
- ロック対象へのすべてのアクセスを禁止する
- select, update, insert, delete すべてできない
- ロック対象へのすべてのアクセスを禁止する
-
共有ロック(S lock)
- ロック対象への参照以外のアクセスを禁止する
- ほかトランザクションからのselectでのアクセスは可能
- ロック対象への参照以外のアクセスを禁止する
-
テーブルロック(表ロック)
- テーブル対象へのロックのため、該当テーブル内の行すべてがロック対象
-
レコードロック(行ロック)
- 行単位で対象のロックを行う
- 一行でも複数行でもどちらでもでき、全行になるとテーブルロックと同義
- 行単位で対象のロックを行う
13. ロックの功罪
ACID特性
トランザクションが満たしている要件のこと。
ここもそれぞれが何を示しているのかはうろ覚えなので、ここで一つまとめておこうかと。
- Atomicity(原子性)
- トランザクション内の操作が、すべて実行されるorすべて棄却されることを保証する
- すべてCOMMITされるかすべてROLLBACKで消される
- トランザクション内の操作が、すべて実行されるorすべて棄却されることを保証する
- Consistancy(一貫性)
- 指定された状態に対して、一貫性、整合性があることを保証する
- Isolation(分離性)
- 実行中のトランザクションがほかのトランザクションに影響を与えないことを保証する
- Durability(永続性)
- 一度コミットされたトランザクションは何があっても残されることを保証する
トランザクションの分離レベルの問題
- Dirty Read
- コミットされていないトランザクションの変更を、他トランザクションからREADできてしまう現象
-
- トランザクションAでREADする
-
- トランザクションBで変更するが、コミットしない
-
- トランザクションAでREADすると変更内容が読み取れる
-
- トランザクションBをROLLBACKすると異なる内容をREADしたことになる
-
- コミットされていないトランザクションの変更を、他トランザクションからREADできてしまう現象
- Fuzzy Read
- 他トランザクションが対象レコードを更新した場合、トランザクション中でREADする内容が異なってしまう現象
-
- トランザクションAで全レコードREADする
-
- トランザクションBで変更し、コミットする
-
- トランザクションAで再度READすると1とは異なる内容がREADされる
-
- 他トランザクションが対象レコードを更新した場合、トランザクション中でREADする内容が異なってしまう現象
- Phantom Read
- 他トランザクションがコミットした新規レコード(追加・削除)した場合、トランザクション中での全件READの内容が異なってしまう現象
-
- トランザクションAで全レコードREADする
-
- トランザクションBで新規レコード追加し、コミットする
-
- トランザクションAで再度、全レコードREADすると1とは異なる内容がREADされる
-
- 他トランザクションがコミットした新規レコード(追加・削除)した場合、トランザクション中での全件READの内容が異なってしまう現象
上記問題が起きうる分離レベルとの関係性は把握しておいたほうがいいとのこと。
下に行けば行くほど問題は発生しないが、並列処理ができなくなる。
Dirty Read | Fuzzy Read | Phantom Read | |
---|---|---|---|
read uncommitted | 発生 | 発生 | 発生 |
read committed | 起きない | 発生 | 発生 |
rpeatable read | 起きない | 起きない | 発生 |
serializable | 起きない | 起きない | 起きない |
最後に
現場でのDBの運用を見てきてから本書を改めて読むことで、より理解しやすい部分も多くあったと思います。
ただ運用上何となく動いているから、といった理由で根本の理解が及んでいない部分もあり、そういった原理をもう一度押さえなおして実運用されている状況を見返してみるいい機会になりました。
たまたまではありますが、本記事を執筆中にDBの設計に携わる機会があり、いくつか役立った場面もあったのはちょうどよかったです。