適切なデータ構造を選択する

WEBアプリで遅い機能があったが適切なデータ構造を選択することでパフォーマンスがよくなった、という経験をしたのでそれについて書く。

本記事で取り上げる機能は、商品毎に設定する部門機能だ。部門機能とは商品に設定するメタ情報で集計時の集計単位として使われる。

「部門機能」について具体的に説明する。
下記の箇条書きが商品で、そのカッコ書きが部門名とする。部門名は、スラッシュで区切られていて階層構造になっている。
任意の部門毎の売上金額が集計されようになっていて、 食品 での階層では5つの商品の売上が計算され、 食品/アイスクリーム では、3つの商品の売上が計算される。

  • チョコのアイス(食品/アイスクリーム/チョコ)
  • バニラのアイス(食品/アイスクリーム/バニラ)
  • バニラのアイス2(食品/アイスクリーム/バニラ)
  • うまい棒(食品/駄菓子/棒)
  • たけのこの里(食品/駄菓子/チョコ)

以前の実装では、部門の階層構造はRDBを使って内包テーブルで表現されていたが、不具合の原因になっていた。

不具合というのは、数十万オーダーで部門データ(索引)を作成しようとすると、内包テーブルの行をRDBに挿入する前にメモリ上に内包テーブル相当の木構造を作っていて、これが実行マシンのメモリから溢れており長時間マシンリソースを専有してしまう問題。もう一つは、トランザクションを貼りながら実行中しているため、大量に発生したギャップロックで他のトランザクションをブロックしてしまう問題。

部門機能を正しく使えている場合は「部門名のカーディナリティは商品数に対して圧倒的に小さい」という特性があり、部門名は1つだけなのに商品の数だけパス行(内包テーブル)が生成されてしまうので部門機能で内包テーブルは相性が悪く、小手先の変更では解決しないと思って、テーブルの構造をまるっと変更して経路列挙モデルに実装し直した。

これによって、メモリ上に木構造を作る必要がなくなり、20時間かかっていた処理が10分で終わるようになった。

段階的にリリースするなどしたのでこのタスク全体は結構たいへんだったんだけど、作業中は「大量データを想定していないクソコード」とは思っていなくて、当時のビジネスを支えていた偉大な実装だと思っている。

おわり。