N+1クエリを安全に減らす、opt-in prefetchの考え方
しろまえ殿は、一覧処理の遅さを直すとき、共通関数を一気に置き換えなかった。 既存の呼び出し元を壊さず、対応できた箇所だけが高速化を受ける形にした。
遅さの正体を数えた
問題は、ループの内側で同じ種類のデータを何度も取得していたことだった。 これはN+1クエリと呼ばれる。1件の一覧を読むために、一覧取得の1回に加えて、件数分の問い合わせが発生する状態だ。
たとえば、買い物かごの商品を一つ読むたびに倉庫へ確認に行くようなものだ。 商品が10個なら10回、100個なら100回の確認になる。
しろまえ殿はまず、データ取得をループの外へ出した。 必要な一覧を1回で読み、識別子をキーにした検索用のマップへ変換した。 ループ内では、そのマップを参照するだけにした。
既存動作を守る入口
共通関数には、省略可能な引数を追加した。 引数が渡されたときだけ、呼び出し元が用意したデータを使う。 渡されなければ、従来の取得処理へ戻る。
この方式はopt-inと呼べる。 新しい高速化を、明示的に選んだ呼び出し元だけへ適用する仕組みだ。
全呼び出し元を同時に変更しないため、影響範囲を小さくできる。 もし問題が起きても、未対応の呼び出し元は従来動作を続ける。
数えたうえで広げる
高速化の判定は、体感だけで済ませなかった。 問い合わせ回数を数える計測用の仕組みを用意し、変更前と変更後を比較した。 結果の内容も比較し、件数だけ減って処理結果が変わっていないかを確認した。
書き込みが発生していないことも確認対象にした。 性能改善の試験でデータを壊さないためだ。
最初は少数の呼び出し元だけで試す。 等価性とクエリ数の両方が確認できたら、同じ型を段階的に広げる。
しろまえ殿が残した判断は単純だ。 共通化は便利だが、失敗時の影響も共通化される。 だから、互換性を保つ入口と実測を組み合わせる。
速くすることより、壊さずに速くすることを優先する。 レガシーコードの改善では、この順番が効く。