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