2009年8月15日土曜日

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

先のエントリで書いた#appengine でクエリの結果件数を取得する方法(1000件超とか)の続きです。

どーしても動的な条件に対する結果件数を…ていうなら、最悪Low-level APIでsetKeysOnly()しつつページング(常に条件を変えつつoffset0~limit1000で)、で数えるという手もあるかもしれない。これもまた試してみよう。この方法なら1000件につき約110msなんで、7000件でも770msくらいで数えきれるはずだ。

気になって眠れないので引き続きこちらの方法も試してみた。

手法

AppEngineでのページング手法としてBest practices for writing scalable applicationsでも書かれているページング方法で、setKeysOnly()を使う場合、使わない場合のふたつのパターンを試してみた。どちらもasList()を使い、FetchOptionにはwithOffset(0).limit(1000)だけを指定している。setKeysOnly()を使った方は当然条件に使用するpropertyの値がfetchできないので、ページングに必要なEntityのみDatastoreService.get(key)でfetchしている。

結果、setKeysOnly()しない方は平均3642ms/7473件で、setKeysOnly()した方は平均1441ms/7473件となった。JDOを使ってアレコレして1000件以上取得する場合は7218ms/7473件だったから、随分と速くはなった。どーーーーしても1000件以上の件数をカウントしたい場合はlow-level APIで件数の為にページングする、といった手法が一番現実的ですね。それでも遅いけど!FetchOptionをもぅちっとカスタマイズすれば、まだ速くなるかなぁ?

ページング部分のコード

最近はgistが重いっぽいので久々にSyntaxHighlighterで。PersonというEntityのheightという、値が重複する可能性があるpropertyを条件に検索する、という想定。

static int countEntities(DatastoreService service, Double param) {
  int pageNo = 1;
  int totalCount = 0;
  PagingResultVO result = null;
  while (true) {
    Query query = new Query(Person.class.getSimpleName())
        .addSort("height", SortDirection.ASCENDING)
        .addSort("__key__", SortDirection.ASCENDING);
    if (pageNo == 1) {
      query.addFilter("height", FilterOperator.GREATER_THAN_OR_EQUAL, param);
      result = getListInPage(service, query);
      totalCount += result.count;
    } else {
      // まずは重複する可能性のあるpropertyをequality filterし、
      // 次ページの先頭となるpropertyをgreater thanでfilterしてクエリする。
      Key nextKey = result.nextEntity.getKey();
      Object nextParam = result.nextEntity.getProperty("height");
      query.addFilter("height", FilterOperator.EQUAL, nextParam)
          .addFilter("__key__", FilterOperator.GREATER_THAN_OR_EQUAL, nextKey);
      PagingResultVO dupResult = getListInPage(service, query);
      totalCount += dupResult.count;
      if (dupResult.hasNext) {
        // 1page分丸ごと取得できた。
        result = dupResult;
      } else {
        // 先の条件だと1page分に足りないので続きを取得する。
        // GT_OR_EQではなくGTを使う。
        query = new Query(Person.class.getSimpleName())//
            .addSort("height", SortDirection.ASCENDING)//
            .addSort("__key__", SortDirection.ASCENDING)//
            .addFilter("height", FilterOperator.GREATER_THAN, nextParam);
        result = getListInPage(service, query);
        totalCount += result.count;
      }
    }
    if (!result.hasNext) {
      break;
    } else {
      // 1000件取得している場合は、最後の1件が次pageにも登場するので1件分引いておく。
      totalCount -= 1;
    }
    pageNo += 1;
  }
  return totalCount;
}

static PagingResultVO getListInPage(DatastoreService service, Query query) {
  FetchOptions fetchOptions = FetchOptions.Builder.withOffset(0).limit(1000);
  List<Entity> list = service.prepare(query.setKeysOnly()).asList(fetchOptions);
  int size = list.size();
  if (size > 999) {
    return new PagingResultVO(service, size, list.get(999));
  } else {
    return new PagingResultVO(service, size, null);
  }
}

public static class PagingResultVO {
  public final int count;
  public final Entity nextEntity; // 次のページの最初のEntity
  public final boolean hasNext; // 次のページが存在するか。

  public PagingResultVO(DatastoreService service, int size, Entity nextEntity) {
    this.count = size;
    if (nextEntity != null) {
      // keyしか入っていないので、filterに必要なpropertyを取得する必要がある。
      try {
        this.nextEntity = service.get(nextEntity.getKey());
      } catch (EntityNotFoundException e) {
        LOGGER.warning("あるはずのKeyが見つからない!" + nextEntity.getKey());
        throw new RuntimeException(e);
      }
    } else {
      this.nextEntity = nextEntity; // null
    }
    this.hasNext = nextEntity != null;
  }
}

コメントを投稿