帳票生成を45分から10秒に短縮した話
C# / WPF で実装された帳票生成処理のボトルネックを調査し、クエリの見直しとデータ取得方法の改善によって大幅な高速化を実現した事例です。
すでにクライアント向けにリリースされていた C# / WPF のプロジェクトに参加し、いくつかの不具合調査を担当しました。その中で最初に対応した課題のひとつが、帳票生成のパフォーマンスでした。
このアプリケーションでは、ある書類用に XLS ファイルと PDF プレビューを生成する必要がありました。最初は1日分のデータしか扱えず、それでも生成には数分かかることがありました。
目的は単にパフォーマンス問題を解決することだけではなく、1日単位に限定されていた機能を改善し、期間を選択して出力できるようにすることでした。つまり、数か月分をまとめて帳票生成できる必要がありました。同時に、PDF 生成は必須ではなかったため、処理が重すぎる場合は削除してもよいという方針でした。
問題の理解
コードを調査したところ、パフォーマンス問題の原因はひとつではなく、小さな問題がいくつも重なって、非常に重い処理になっていることが分かりました。
1. ネストされたループによる過剰なクエリ発行
最初の大きな問題は、以下のようなネストされたループ構造でした。
- アイテムの一覧をループする
- 各アイテムごとに、複数の行をループする
- その2つ目のループの中で、さらに複数回データベースを問い合わせる
このため、クエリ数が非常に急増していました。
1日分のデータだけでも、1つの帳票を作成するためにおよそ 4,500 回ほどクエリが発行されることがありました。期間が長くなると、この構造は完全に現実的ではありませんでした。
2. リポジトリがデータを断片的に取得していたこと
2つ目の問題は、データアクセス層にありました。
いくつかのリポジトリメソッドが、必要な情報をまとめて取得せず、細かく分割して取得していました。たとえば、1回で全部取れるはずのデータを、以下のように何度も分けて取得していました。
- ID を取得する
- 名前を問い合わせる
- いくつかの追加項目を問い合わせる
- 関連する別データを問い合わせる
これが複数のリポジトリで繰り返されていました。
問題は、1回1回のクエリが特別に重いことではありませんでした。必要なデータ構造を、1回で効率よく取得せず、細かい断片として何度も問い合わせていたことが問題でした。
3. PDF 生成のオーバーヘッド
PDF プレビューも、処理を遅くしている要因のひとつでした。
内部の細かい実装までは完全には調査していませんが、PDF 生成を外したところ、処理速度はかなり改善しました。これは主因ではありませんでしたが、明らかに余計な負荷になっていました。
実施した対応
私の主な対応方針は、帳票生成ループの中でデータベースを何度も問い合わせないようにすることでした。
その代わりに、必要なデータをあらかじめリポジトリから一括で取得し、メモリ上に保持してから、そのデータを使って出力を組み立てるようにしました。
これにより、処理の流れは以下のように変わりました。
- 処理中に何度もデータを問い合わせる
- 必要なデータを一度取得する
- メモリ上で整理する
- そのデータを使って帳票を生成する
この変更によって、ロジックはかなり効率的になり、処理内容も分かりやすくなりました。
必要なデータをすべて集めたあとで、それぞれの値を対応付けながらループ処理を行い、XLS ファイルを正しく生成できるようにしました。
また、生成フローも修正し、1日単位ではなく、より長い期間に対応できるようにしました。
成果
改善効果は非常に大きいものでした。
もともと1か月分のデータで約45分かかっていた処理が、数か月分のデータでもおよそ10秒程度で完了するようになりました。
これは非常に大きな改善であり、この機能の使いやすさを大きく向上させました。
学んだこと
このプロジェクトは、パフォーマンス問題が小さな設計上の判断の積み重ねで発生することを改めて実感する機会になりました。
- ネストされたループ
- 繰り返し発行されるクエリ
- 断片的なリポジトリアクセス
- 不要な処理
また、1行ずつのコードを見るのではなく、処理全体の流れを見ることの重要性も学びました。
本当の解決策は、小さな最適化ではなく、データの取得方法や処理の組み立て方そのものを変えることだったりします。