データ集計に ActiveRecord を使ったらこうなったという話

CrowdWorks Advent Calendar の9日目に入るはずだったポストです。元々は、問題提起とその解決篇をセットにしてご紹介するつもりだったのですが、話が壮大になりすぎて僕の文章力ではまとめきれずに挫折した挙句に〆切に間に合わなくなってしまいました :sweat_smile: 

リベンジとして、問題提起の部分だけをピックアップしてご紹介する形にします。解決篇はバッサリ省略。どこか別のポストで紹介できればと思います。アドベントカレンダー的には数日遅れで申し訳ないですが、何かのご参考になれば。

# 結論

先に結論から書きます。

* データ集計で ActiveRecord は忘れろ
* 用途に合った適切なツールを使おう

# 前提

ここで言う「データ集計」というのは、だいたいこんな感じのモノを指しています。

* サービスのデータベース (RDB) に蓄積された情報を取り出してきて
* 日次や月次の数字を表とかグラフの形式で見えるようにして
* 経営などの意思決定に役立つようにすること

典型的には、「今月の売上高」みたいな項目が多数並んだレポートを出すようなものを想像してもらえれば。

# データ集計の特徴と ActiveRecord

データ集計において、プログラマが取扱う情報というのは次のような特徴があります。

* 時系列 x 集計項目のマトリックス
* 大半のデータは数値
* 集計表とかグラフとして表示できれば ok

「マトリックス」と書いたのは、たとえば各行が集計月で各列が集計項目になっている Excel シートのようなものを想像してもらえれば良いかと思います。そして、セルに入っている情報はほとんどが数値です。たとえば「売上: *** 円」とか「コンバージョン率: **%」みたいなデータですね。そして、データを出す目的は「並べて見ること」です。

さて、これらのデータをどうやって取ってくるかを考えます。前提で触れたように、元になる情報はデータベース (RDB) に格納されています。且つ、サービスそのものが Rails で作られているという事情も重なると、ActiveRecord を利用するのが最も手っ取り早く実現できそうです。データベースへの接続はすでに確保されていますし、既存モデルの scope などを活用すれば DRY になって良さそうに思えますね。

しかし、実際にやってみたら、これはあまり良い戦略ではありませんでした。
なぜなら、ActiveRecord はデータ集計に適したツールではなかったからです。

※以下、ActiveRecord を AR と表記します。

なぜ AR がデータ集計に向かないのかを考えるにあたって、まずは AR の特徴と得意分野を考えてみます。全部挙げればこれ以外にもあるでしょうが、主なところはこんな感じになるでしょうか。

* データベース上の多様な種類のデータを適切な型のオブジェクトにマッピングすること
* 特定のデータを軸に、その周辺情報 (= 関連先) にもアクセスしやすくすること
* 得られたデータに基づいて、モデル的な振舞いを定義できること

1つめは O/R マッピングの機能のことです。2つめは has_many などの関連を介して関係のある別のデータへ辿れることを指しています。

データ集計で扱うデータの特徴は↑にも書いたとおりですが、再掲すると (ちょっと順序を変えてあります)

* 大半のデータは数値
* 時系列 x 集計項目のマトリックス
* 集計表とかグラフとして表示できれば ok

違いがお分かりでしょうか？ 順に見ていきます。

まず、データ集計で扱いたいデータの大半は数値です。それ以外には日時情報と文字列ですが、それらが現れるのは、行や列のメタ情報としてです。すなわち、列名で文字列が出てくるのと、「この行は何月何日にデータであるのか」を表すために日時情報が出てくるぐらいです。どちらにせよ、1つの項目の中に構造化されたデータが入るようなことはありません。なので、O/R マッピングの機能はあまり必要になりません。AR よりももっと低レベルな処理を扱うライブラリ、たとえば mysql2 や pg などの gem で充分に事足ります。

次に、データ集計で取得したいデータの形はマトリックス状です。Excel のような表形式と言い換えても良いでしょう。取得してきたデータを行単位 (たとえば◯月☓日の全集計データ) や列単位 (たとえば過去から今日までの売上高の推移) どちらの切り口でも簡単にアクセスできると便利です。R とか、Python の pandas などにある DataFrame のような仕組みが重宝します。

これに対して AR が得意とするデータの形はツリー状と言えないでしょうか。主軸となるデータが1つあって、関連先データがいくつか繋がっているようなイメージですね。データ集計で欲しい、マトリックス状の形式とは趣が違っているようです。

最後に、得られたデータをどう使うかです。データ分析では、集計値をほぼそのまま画面に出すのが目的になります。あるときは数値そのものを表にまとめたり、あるいは推移や比率などを直感的に分かりやすいグラフの形式にしてみたりと、表現形式は微妙に変わりますが、データを起点として何かの振舞いやビジネスロジックが必要になることはありません。出てきた値をどう見せるかだけの話になります。なので、データの集まりにモデル的な振舞いを付加するような機能も必要ではありません。

というように、AR の得意分野とデータ集計で求められている機能との間にはギャップがあります。言い換えれば AR はデータ集計という用途には合っていない。

では、用途に合わないツールを、(そうと知りつつも？) 活用する方向で頑張るとどうなるでしょうか？ ここから先は経験に基づいた考察になりますが、弊社で起きた例をベースに考えてみたいと思います。

# データ集計で ActiveRecord を使って頑張ってみたら

まず、今からだいたい半年ぐらい前の状況をご紹介すると、こんな感じでした。

* 本体のサービスは Rails で作られている
* データ集計も Rails の仕組みに乗っかった
  * `#{RAILS_ROOT}/script` 配下にバッチ起動スクリプトがあって
  * そこから集計モデルが呼び出され、
  * Arel の DSL を使って集計クエリを作り、
  * 得られた結果をまとめてデータベースに保存する
  * 集計結果は管理画面 (これも Rails 製) で見えるようになっている

## 直面した問題

端的に言うと、集計時間が肥大していました。一日分のデータを計算するのに3時間以上かかっており、その時間はサービス自体の成長に伴って増加する傾向を示していました。その半年ぐらい前は2時間に収まっていたのですが。何も手を打たずに放置すれば、いずれ「一日分のデータを集計するために24時間以上必要になる」という事態になることは明らかです。しかも、サービスの成長が早いほど、その事態に至る時間は短くなるでしょう。

なぜそのようになったのでしょうか？ 原因を要約すると、集計したい項目1つにつき少なくとも1つのクエリが必要な構造になっていたからでした。もう少し直感的に分かりやすい言い方をすれば「マトリックスの1つのセルを埋めるために、データベースへの問い合わせが1つ発生する」といったところでしょうか。(実際にはそんなに単純ではないのですが)

このようになったのは、おそらく心理的な要因・作用が絡んでいると見ています。それは「複雑なクエリを AR および Arel の DSL を使って表現するのはとても難しい」ということです。

データ集計に必要となるクエリは、web アプリケーションで使うそれよりは遥かに複雑になる傾向があります。たとえば多段の join が必要になったり、あるいはサブクエリがいくつも必要になったり。時には、join する対象がサブクエリだったりなんてことも日常茶飯事です。

そういった複雑なクエリを Arel で記述できるのでしょうか？ 少し調べてみると、いちおう方法はあるようです。とは言え、どこまで頑張れるのかは微妙なところです。上手い書き方が見つからなかったら、生 SQL 片をベタ書きしないといけなくなるかもしれません。そうでなくても別の問題に直面します。Arel の DSL をフル活用して記述したクエリを一歩引いて眺めてみると、なにやら SQL によく似た別の言語で書かているだけ、ということも珍しくありません。

結局のところ、生成したいのは SQL なので、複雑になればなるほど構造が SQL に近くなる傾向があるようです。というような状況を見ていると、「実は生の SQL を書いた方が話が早かったんじゃね？」という本末転倒感が出てきます。実際、Arel で頑張った文と SQL の文を比べると SQL の方が読みやすい傾向がありそうです。なぜなら、SQL であれば構造が厳密に決まっているために探すものがどこにあるのか分かりやすいから。AR / Arel では、ロジックは複数のモデルに分散して記述できるが故に、探しているロジックがどのソースファイルに書かれているのか探しまわる破目になりがちです。

というようなことが分かっていると、AR で複雑なクエリを組み立てるのはハードルが高いために、主に心理的な要因が作用して「AR でも難しくない範囲で頑張る」という傾向が生まれるのではないかな、と推測しています。であれば、1項目のために1クエリを必要とする構造になったのも理解・納得できる気がします。

# 派生する諸問題

1項目に対して1クエリが必要なのだとすると、そこから「集計したい項目の数に比例して集計時間が増大する」のは必然と言えるでしょう。ここを起点として、さらに別の問題が派生してくることになります。それらは、集計時間の増大に対抗するための策によって起こりました。

まず1つ目は、並列に集計してしまえばトータルの時間は短くできるだろうという発想から生まれました。マルチスレッド化です。その副作用として、別の問題に対処しなければならなくなりました。一例を挙げると autoload 問題です。どうやら Ruby の gem モジュールの中には非同期に require されてしまうと挙動が怪しくなるものがあるようで、それを避けるために集計プロセス開始前に必要なモジュールを全て require しておかないといけなくなったりしていました。まぁ、こちらは原因と対策さえ判明してしまえば些細な問題と言えますが、やりたいことの本質とは関係のないところで調整が必要になってしまっているという点で、あまり好ましくはないと考えられるでしょう。

あるいは、クエリ発行数の増大を避けるために、クエリを介して得たデータを Ruby プログラム上で計算して結果を出すプロセスが現れました。これは計算の合理化という点では悪くないように思えるのですが、計算のためのコードを書く人間にとってはあまり良い傾向ではありませんでした。なぜなら、集計値を出しているロジックがクエリに書かれていたり、Ruby ソースコード上に書かれていたりと、バラバラになってしまうからです。典型的には「画面のここに出ている数値を出しているロジックは、いったいどこに記述されているんだ？」という問いに答えるのが難しく、保守性が徐々に低下していくという問題が発生していました。

これらの諸問題を考えるに、そもそもデータ集計に AR を使うのは良くなかったのではないか？という疑問が生じました。

# AR が問題なのではなくて、使い方次第で適切に運用できるはずじゃね？という問いに対する答え

確かに、AR / Arel の書き方をマスターして適切に利用すれば上手くいく、問題はツールじゃなくてツールの使い方に問題があったのではないかという見方にも一理あるかとは思います。

が、その考えに基づく方向性だと、職人気質的な属人化の道を進むことになりそうに思えます。頑張れば頑張るほど、その頑張りに反比例して理解できる人・ついていける人が少なくなるような。高度に専門的な知識がある人でないと手が出せなくなるような、そんな道であるように思えます。個人の生存戦略であれば、そういう方向性に特化してしまうのもアリかとは思いますが、組織が採用する戦略としては危うい気がします。社内で手伝ってくれる人を探すのも難しくなるでしょうし、人事上の都合で引継がないといけなくなった時に困りそうです。

そのように考えると、あまりに高度すぎる路線を選択するのも考えものです。頑張ればどうにかなる問題なのかもしれませんが、そこは本当に頑張るべきところなのでしょうか？

# 弊社での打開策と、見出した結論

弊社の場合は、AR を使わずに生の SQL を上手く活用する方向に舵を切りました。AR を使わない代償として、別の仕組みが必要になりましたが。具体的には [bricolage](https://github.com/aamine/bricolage) を使う方針に変えました。今のところ、この作戦は当たりを引いて、以前よりは上手く行っていると考えています。どこが良かったのか・なぜ上手く行ったと思えるのかは、いずれまたどこか別の機会にさせてください (それを説明するには、この余白は狭すぎます)。

結局のところ「解きたい問題に適したツールを使うのがいちばん」ということになると考えています。こう書くと「当たり前じゃん」という感想になりそうですが。でも、その「当たり前」を選択するのは意外と難しいし、短期的な最適解を選ぶと、それは「解きたい問題に適したツール」ではないこともあるかもしれないということは頭の片隅に置いておいても損はしないと思います。そんなわけで、どこまで一般化できるのかは分かりませんが、「やってみたら、うまく行かなかった事例」として共有できればな、と考えた次第でした。もし、何かのご参考になれば幸いです。