最近、達人に学ぶDB設計徹底指南書を読んでいる。
最終章まで読んだが、その中でも少し勇気をもらえたお話についてアウトプットしてみる。
注文レコードからその注文の商品数を出す
よくあるECサイトを作ろうとした際。
注文とその注文の明細のテーブル設計をすることになったとする。
よくあるのが下記のような感じのやつ。
注文と注文明細で1:Nで実装する。
現実ではもっと要素はあると思うが、だいたい似たような感じになると思う。
受注日ごとに商品数を出したい
で、よくあるのが、レポート機能か何かで、受注日ごとに商品数を出したい(or 受注金額の合計を出したい)みたいなやつ。
売れてる日付とかタイミングを知りたいよーみたいな。
ああ、できますよっつって、下記のようなSQLで実現しようとする。
SELECT
O.OrderDate,
SUM(OD.Quantity) AS TotalQuantity
FROM
Order O
JOIN OrderDetail OD ON O.OrderID = OD.OrderID
GROUP BY
O.OrderDate
ORDER BY
TotalQuantity DESC
これで、下記のように受注日ごとに商品数を出せるようになる。
O.OrderDate,TotalQuantity
2025/1/5,12
2025/1/4,8
2025/1/3,2
...
が、重い
しばらくはこれで良かったのだが、なにせ大手のECサイトだ。
どんどん顧客数は増加し、1日の注文数が数万とかになってくると様子がおかしくなる。
そう、JOINのコストがメタメタに高くなってきたのだ。
集計系のサマリーデータとして「商品数」カラムを用意する
そこでなんとかJOINせずに速度を出す方法として、注文テーブルの方に「商品数」というカラムをついかしてやる。
これでOrderテーブルのみで受注日ごとの商品数が集計できるようになる。
JOINのコストがなくなるのでクエリがだいぶ軽くなる。
ついでに、OrderDateとかにインデックスをつけ、レポート画面で受注日の範囲指定を必須にすれば、とんでもなく注文数が多くなってもなんとか表示できる(多分)。
これは本来OrderDetail側の情報であった商品数を、Orderに持っていったということで、正規形を崩した形になる。
が、実際の開発現場はこういうのばっかりで、よくやるよね?
みたいな。
サマリデータの弱点は更新系
当然、サマリデータを使った場合、新規注文の機能の中でそれを計算する処理が必要になるし、注文編集なんかの機能があれば、注文商品の削除や増加があった場合に、Order.ProductCountを再更新する必要が出てくる。
で、注文と言ってもいろいろな機能があるので、色々仕様追加してたらProductCountがうまく更新されないケース、なんかも出てくるわけだ。
そういうのは例えばドメインサービスクラスを使って登録・更新処理をうまくカプセル化したり、自動テストのテストケースできちんとガードするようにテスト設計したりが必要になる。
このあたりは結局泥臭くやっていくしかなくなる。
…ていうような話が書いてあって感動した
DB設計の本って、ベストプラクティスしか書いてなくて、こういう理想的には正規形にしたいけど、要件によってはそうも言ってられねーよな?? みたいなときのことが書いてなかったりするのだけど、ちゃんと書いてあって感動した。とともに、やっぱそうするしかないよね?? と安心した。
(実際の現場ではこういう集計系の要件が出てきたときに「この集計本当に必要ですか??」「ないと致命的ですか??」などと聞くのが必須。間違いなく保守性が悪くなるところなので)
その他にもビューを使って強引に要件を実現する話とか、その注意点とか、現場でよく使うお話が満載だった。
おすすめでございます。
個人的には6章のパフォーマンスの話が熱い。SQLアンチパターンとかまだ読んだことない人とかだと重宝するお話が多い。B-Treeの計算量の話とか。パーティションとかシャーディングの話もあって嬉しい。