失敗から学ぶRDBの正しい歩き方を読んだ

以前の達人に学ぶDB設計に引き続き下記の本を読んだ。 prelude.hatenablog.jp

失敗から学ぶRDBの正しい歩き方 (Software Design plus)

失敗から学ぶRDBの正しい歩き方 (Software Design plus)

  • 作者:曽根 壮大
  • 発売日: 2019/03/06
  • メディア: 単行本(ソフトカバー)

読んだ背景

それっぽく何が良いのかのわかったので、アンチパターンから学んでいこうという理由から。本書の内容をおおまかに分類すると「設計」「SQL」「管理」に分けられる(主観)。SQLについてはアンチパターンというよりはJOINやロックの仕組みをちゃんと押さえましょうね、という話が主だった。達人に学ぶDB設計に引き続いて設計をもう少し知りたくなったので、今回は「設計」の章についての感想を残しておく。

感想

達人に学ぶDB設計では正規化など正攻法?について学べることが多かったが、この本ではアンチパターンから学んでいくため納得させられる話が多かった。この本を読む前は実際の業務で扱うような複雑さに対処できるほどの知識はなかったので、改めてテーブル設計の難しさを感じた。

失われた状態、フラグの闇、隠された事実

これらの章に共通していたのは、データが一体何なのかわからない、ということである。

失われた状態では、過去の状態がわからないため過去の状態を元にした処理を行うことができない。例でも触れられていたが、状態が失われる設計をしていると返品による払い戻し処理などができなくなってしまう。例えば、消費税や割引額が現在と過去で変わってしまった場合、払い戻しの金額がわからなくなる。このようにその時の状態とその時のデータというものは履歴として控えておく必要があるものがあるのだ。しかし、何でもかんでも履歴として残しておくとデータサイズが増えてしまったり、状態を意識したSQLを発行する必要が出てきて複雑度が増すので、ちゃんと過去の状態・データを参照する可能性があるものだけを見定める必要はある。あと本書では触れていなかったけれど、外部キーを持つようなテーブルであったら、親が履歴を必要とする場合、子も履歴を控えた方が良いケースもあると思うので、テーブルのデータだけで完結せず、より俯瞰して設計する必要がありそう。色んなことを考えさせてくれる章で、自分も気づかずアンチパターンを踏んでいそうだな、とすごく思った。

また、似たようなものとしてフラグの闇という章がある。これは名前の通りだがフラグによる状態管理の話だ。やりがちなアンチパターンとして削除フラグがある。削除フラグが存在すると削除されたデータと削除されていないデータで似たようなデータが増えるため、カーディナリティが低くなる。カーディナリティとは列に格納されるデータにどのくらいの種類があるかを意味しており、カラムのデータの濃度のようなものである。似たようなデータが多い場合は重複があるため濃度が下がる。これによりINDEXが効きづらくなったり一意に選び取れずにメモリ効率が悪くなったり色々起きる。このようなフラグへの対処としては、事実のみを保存できるようなテーブルを設計するということだ。削除フラグでは削除済みのテーブルを作る。

さらに、隠された事実という章では、フラグに似ているが、論理ID・スマートカラムなる話が出てくる。これはIDに意味を持たせたもので、学籍番号がイメージしやすい。学籍番号は入学年度と学部学科のIDを繋げたものであるが、そのように複数の意味を持たせたIDのことを論理IDなどと呼ぶ。単一のカラムだけでなく、あるデータの属性によって入る値が変わるカラムなども同様である。これはカラムではなくテーブル単位で複数の意味を持ってしまっている。属性によってカラムの値が変わるのであれば、属性に応じてテーブルを分けた方が良い。
似たようなアンチパターンとしてEAVが上げられていた。これはtypevalueのように属性名と属性値をカラムとして持つテーブルのことで、取り出すまで属性名や属性値が一切わからない。これも本来であれば属性に応じてカラムやテーブルを用意すべきである。
しかるに、正規化をしっかり行って、カラムやテーブルに複数の意味を持たせないようにしようということである。

強すぎる制約、簡単すぎる不整合

これらの章では最適を目指した結果、それが悪手になってしまっているようなものを扱っていたように思う。DBMS側の制約とアプリケーション側の制約を適切に使うことでより良い設計ができる。

強すぎる制約では、いま現在の仕様に従って制約を作っていくと仕様変更に弱いテーブルになってしまうという話だった。制約はデータの整合性を保つために必要なものであるが、こだわって作りすぎるとイレギュラーな場合にデータを投入することすらできなくなってしまう。例えば、日時のカラムで現在時刻以降しか受け付けない制約があったとする。その場合、過去日のテストデータはどうやって用意すればよいだろうか。また、障害時にデータをどのようにリストアすればよいだろうか。
制約はテーブルだけではなくアプリケーションでバリデーションをかけてあげることで担保する方法もある。これはもちろんDB側での制約ではないため、バグが混入していると不正なデータが格納されてしまう恐れがあるが、それはそれでアプリケーション側の設計で担保すべきことでもある。要は制約をかける場所も段階的に分けることで設計を考えるべき、ということだ。

逆に、正規化を行い完璧な制約を組む話とは裏腹にパフォーマンスのために非正規化したくなる時もある。この場合はアプリケーションで整合性を担保するしかない。あるデータを変更した場合は、それに関連するデータの変更はDBMSではなくアプリケーションの責務となる。そのため、ビジネスロジックが複雑になり時間が経っても意図を残しておける運用が求められる。
これには制約を上手く使うことで回避できることもある。例えば、あるカラムのデータが特定の値だった場合は、このカラムのデータの値はこのようになるはず、という制約を組むことで非正規化されていたとしても不正なデータは混入しづらくなる。カラムに応じて制約を作るような設計は先ほどの複数の意味を持つテーブルなのでは?という疑問が湧いてくるが、パフォーマンスを上げる必要があるテーブルの場合はこの矛盾と付き合いながらベストではなくベターな解決策が必要になる。その際はこの話は非常に参考になるだろうと思った。

キャッシュ中毒

パフォーマンスの鍵だが、アーキテクチャの複雑度を大きく上げる要因でもある。

キャッシュはメモリ上のキャッシュしか考えていなかったが、それ以外にも色々あることを知った。
まず、クエリキャッシュ。これは実行されたクエリの結果がDBMS側で保持されているもの。これにより同じクエリが叩かれた時にすぐにデータを返すことができる。しかし、実行されたクエリの結果が、キャッシュなのか最新情報なのかわからなかったり、テーブルが更新されるとクエリキャッシュはクリアされてしまう。
続いて、マテリアライズド・ビューである。これは先ほどのクエリキャッシュと似ているが、実際にテーブルを作成することが違い。そのため、このマテリアライズドビューに対してINDEXを貼ったり、クエリを発行したりできる。しかし、テーブルを作成しているため、作成の元となるクエリに関係するテーブルに変更があったり、データに大きな変更があると再構築の必要がある。この再構築コストはもちろんテーブルが大きいほどかかってしまうのだが、コストがかかるようなテーブルだからこそキャッシュをしているわけなので、今回の文脈においてもやはり再構築は非常にコストが高い。
最後に、アプリケーションキャッシュである。これはredisやmemcachedなどである。RDBMSを介さないので非常に高速に動作する。しかし、他のキャッシュ同様に状態管理はやはり難しい。

キャッシュの注意点としてはデータの状態を意識することが難しいことに尽きる。参照時にどの状態なのかわかりづらいため、障害時は特に問題の切り分けが難しくなる。また、どのデータがキャッシュされているのか把握しづらいため、デバッグの難易度が高くなる。

キャッシュと付き合っていくためには、キャッシュヒット率や更新頻度を推測・計測をしっかり行っていく必要がある。また、複雑性を極力下げるためにもキャッシュの対象と範囲を見極め、生存期間と更新方法を明確にする。キャッシュがキャッシュであることを自明にしていくことで扱いやすくする。

まとめ

横着して設計を怠るとその時の生産性は一見上がったように見えるがその負債は障害時に支払うことになる。アンチパターンを見ていて思ったが、書籍の情報以上に考えることが多い。きっと筆者は文章以上の経験をやはり積んだ上で、厳選して載せているのだろう。SQLアンチパターンを引き合いにしていることが多くあったので、次回はそれを読んでみようと思う。