2009年8月15日土曜日

#appengine でクエリの結果件数を取得する方法(1000件超とか)

今日ひがさんと「クエリの結果件数の取得方法」についてTwitterで少しやりとりした。

  • Low-level APIのPreparedQueryだと1000件の上限があるが、JDOのQueryだと上限が無い
  • 確かに上限は無いけど、JDOでQuery#execute().size()はめっちゃ遅くて現実的ではない
  • JDOでもQuery#setResult("count(this)")は速い。でも、こいつは実はLow-level APIに丸投げなのでやっぱ1000件の上限に引っかかる。
  • JDOでもQuery#setResult("key")するとLow-level APIでいうsetKeysOnly()と同じ、キーのみクエリになる。でも1000件の上限に引っかからない!

大体こんなカンジで、最後の件はひがさんが発見してGoogle App Engine for Java GroupMLにPostもしていた。で、具体的にどれくらいのパフォーマンスなのかが気になったので試してみる。

以下の5つのパターンで試してみた。

  1. パターン1:普通に((List)Query#execute()).size()して件数を取得する。
    1000件超の件数も取得できるし、Entityの全フィールドを取得できる。
  2. パターン2:Query#setResult("count(this)")して、(Integer)Query#execute()で件数を取得する。
    これは当然のことながら件数が返るだけ。しかも1000件以上の時は取得できない(1000という件数が返される)。
  3. パターン3:Query#setResult("key")して、((List)Query#execute()).size()で件数を取得する。
    1000件超の件数も取得できるし、各Entityの主キーフィールドだけは取得できる。
  4. パターン4:Low-level APIを使う。DatastoreService#preparedQuery()#countEntities()で件数を取得する。
    件数のみ取得できる。
  5. パターン5:Low-level APIでQuery#setKeysOnly()してから、DatastoreService#preparedQuery()#countEntities()で件数を取得する。
    件数のみ取得できる。

当然ながら、どのパターンも「filterあり」で実行する。ソート対象は「filter対象のProperty、主キー」を使用する。

1000件超の件数を取得する場合

クエリの結果件数は7473件、全Entityの数は10万件くらい(cronでランダムなEntityを自動生成していたので、全部で何件か?が把握できていないw)。主キーはKeyを使っており、gae.encoded-pkなStringではない。22回ずつ試して最大最小を取り除いて平均してみた。試行が少ないけど、ぶれも少なかく安定した数値が出ていたのでまぁいいか、と。

  • パターン1:Query#execute()の実行時間の平均は62msで、取得したListのsize()メソッドの実行時間の平均が9140ms
  • パターン5:Query#execute()の実行時間の平均は19msで、取得したListのsize()メソッドの実行時間の平均が7218ms

素のクエリと比較すると、setResult("key")する方が20%以上速いよぅだ。

1000件以内の件数を取得する場合

クエリの結果件数は720件。極力1000件ギリギリにしたかったんだけど、いいクエリ条件を作れなかった、ゴメンナサイ。クエリ対象のKindは上記の1000件超と同じもの。

  • パターン1:((List)Query#execute()).size()の実行時間の平均は858.2ms
  • パターン2:(Integer)Query#execute()の実行時間の平均は99.2ms
  • パターン3:DatastoreService#preparedQuery()#countEntities()の実行時間の平均は97.8ms
  • パターン4:DatastoreService#preparedQuery()#countEntities()の実行時間の平均は104.4mssetKeysOnly()しない時よりも7msほど遅いけど、たぶん誤差ですな。
  • パターン5:((List)Query#execute()).size()の実行時間の平均は704.2ms

やっぱ件数の取得専用の機能を使う方が断トツでいい結果ですよ、と。

ちなみに、この1000件以内の場合に件数を取得したQueryと同じQueryでKeyを100件Fetchする場合(パターン2は無理だけど)、JDOを使ったパターン1とパターン3はJava的な速度で済むから平均1msで走査できる。パターン4とパターン5は、Keyを100件Fetchするのにそれぞれ平均148ms112msかかる。setKeysOnly()が効いています。パターン4でKey以外を100件走査するのに147msで、Keyの走査と同じ速度ですね。

まとめ

1000件以内とほぼ断定できているなら素直にJDOでQuery#setResult("count(this)")するか、Low-level APIでcountEntities()しよぅ、て事ですね。

1000件超の件数を知りたい場合は、今のところJDOでQuery#setResult("key")で件数を取得するのが一番マシな方法、…とはいえ7000ms/7000件だし、まぁ実用的ではない。1000件以上数えるような要件はとっとと捨ててLow-level APIを使いましょう、て事かな。実際1000件以上の件数を数える要件にそこまで価値がある事は少ないと思うし。1000件以上をカウントしたい場合でも、別途カウンタを用意して済むならそれがいいし(Shardingとかの手法の考慮が必要ですけどもね。あとTaskQueueもとっとと導入してくれないとツライ)。動的な条件による結果の件数、となるとそこまで価値は無いような気がするしなぁ。どーしても動的な条件に対する結果件数を…ていうなら、最悪Low-level APIでsetKeysOnly()しつつページング(常に条件を変えつつoffset0~limit1000で)、で数えるという手もあるかもしれない。これもまた試してみよう。この方法なら1000件につき約110msなんで、7000件でも770msくらいで数えきれるはずだ。

追記

最悪Low-level APIでsetKeysOnly()しつつページング(常に条件を変えつつoffset0~limit1000で)、で数える

無理です、setKeysOnly()したらページングできません。…と追記するつもりだったけど、最後の1件だけkey以外もFetchするって事ならsetKeysOnly()でもいけるか?…とか思ったりもする。

コメントを投稿