コンピューターサイエンス:アルゴリズム / OS / ネットワーク / DB
非正規化のトレードオフ
「先月の売上」を出すたびに 30 秒待つ
分けるほど健全になる、という話をしてきました。ここでは逆を向きます。
管理画面に「先月の売上」を出しています。注文と明細と商品と会員を結合し、300 万行を足し上げる。開いた人は毎回 30 秒待たされます。しかも遅いのは、その画面だけではありません。その 30 秒のあいだ、同じ DB で動いている注文の処理まで巻き込まれて遅くなります。ここで足りていないのは設計の正しさではなく、読む速さです。
正規化を崩して、あえて同じ値を 2 か所に置く判断を非正規化と呼びます。順番が大事で、先に分けきってから、測った結果として崩します。最初から崩した設計は、ただの雑な設計です。
崩し方は大きく 2 通りです。1 つは、結合しないと取れない値を手元の表にも書き写すこと。商品一覧を出すたびに分類名を引きに行くのをやめて、商品の表に分類名も持たせる、といった形です。もう 1 つは、毎回計算している値をあらかじめ置いておくこと。会員ごとの購入累計を、注文が入るたびに足し込んでおくのがこれにあたります。
冗長を足した瞬間、真実が 2 か所になる
書き写した以上、両方を合わせ続ける責任が生まれます。
分類名を変えたら商品側も直したか。注文が入ったら累計に足したか。注文が取り消されたら引いたか。返品されたら。この「忘れずに直す」をアプリのコードだけで守り切るのは無理があります。書き込む場所が 1 つ増えるたびに、抜け道が増えるからです。
守り方は次のいずれかです。
- DB 側の仕掛け — 変更をきっかけに自動で相手も直す。確実だが、追いにくい処理が裏で走る
- 定期的な作り直し — 集計結果を保持する仕組みを使い、決めたタイミングで作り直す。少し古い値を許せるなら扱いやすい
- 出来事を起点に更新 — 「注文が確定した」という出来事を一度だけ発火させ、それを受けて集計を直す
どれを選ぶにせよ、非正規化を入れると決めた時点で、同期の仕組みまでを 1 セットで設計します。片方だけ入れると、ずれた値を誰も直せない状態になります。
それでも、ずれは起きます。起きる前提で、定期的に本来の値を計算し直して突き合わせる仕組みを別に用意しておきます。ずれていたら直し、直した件数を記録する。この見張りがないと、崩した値が正しいかどうかを誰も確かめないまま、何年も動き続けることになります。
崩す前に、測る
もう 1 つ、性能とは関係のない非正規化があります。時間を固定するための書き写しです。
請求書の宛先住所を、会社の表から毎回引いてくる設計にすると、会社が引っ越した瞬間に過去の請求書の宛先まで書き換わります。発行時点の住所は請求書の側に焼き付けておくのが正解で、これは冗長ではなく別の事実です。
性能のための非正規化に進む前には、必ず次を確かめます。分けたままで本当に遅いのか、読みと書きの比はどれくらいか、元の値が変わる頻度は十分低いか、ずれたときに業務がどう困るか。速くしたいという気持ちだけで崩すと、後から必ず食い違いの事故が起きます。
なお、注文や決済を捌く側は分けたまま、レポートを見る側は崩した表を別に用意する、という分業もよく使われます。同じデータでも、書く場所と読む場所では最適な形が違うためです。分析の側は多少古い値でも構わないことが多く、そこが分けやすさの根拠になっています。