先のエントリで書いた#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;
}
}
0 件のコメント:
コメントを投稿