GAE/Go＋Datastoreで楽にお安く総件数を取得する

Google Cloud Datastoreは一般的なRDBMSに比べると使えるクエリの柔軟性がありません。特にGAE初心者が困惑する点の一つが、SQLのCOUNT関数相当の機能が用意されていないことです。

「正確な件数なんて大抵の場合不要な情報だろう」というのがGoogleの中の人の考えなのかもしれません。実際、GoogleのプロダクトはGoogle検索にせよGmailにせよ件数表示が適当なものが多い気がします。その方針を貫き通せるならそれが一番いいような気もしますよね。

とはいえ、Datastoreを使っていてCOUNT関数が欲しくなるときもあるでしょう。できるだけ労力をかけず、かつできるだけGoogleさんにお金を払わずにDatastoreクエリの件数を取る方法について調べてみました。

# 統計用のkindを利用する

特定のkindのおおよその総件数を知りたい場合、Datastoreの統計用kindである「`__Stat_Kind__`」や「`__Stat_Ns_Kind__`」が利用できるかもしれません。これは下記の公式ドキュメントで紹介されています。

- [Datastore Statistics in Go  |  App Engine standard environment for Go  |  Google Cloud Platform](https://cloud.google.com/appengine/docs/standard/go/datastore/stats)

具体的には次のようなコードで特定kindの総エンティティ数を取得できます。

```go
type statKind struct {
	Count     int64     `datastore:"count"`
	Bytes     int64     `datastore:"bytes"`
	Timestamp time.Time `datastore:"timestamp"`

	KindName            string `datastore:"kind_name"`
	EntityBytes         int64  `datastore:"entity_bytes"`
	BuiltinIndexBytes   int64  `datastore:"builtin_index_bytes"`
	BuiltinIndexCount   int64  `datastore:"builtin_index_count"`
	CompositeIndexbytes int64  `datastore:"composite_index_bytes"`
	CompositeIndexCount int64  `datastore:"composite_index_count"`
}

func statKindHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)
	q := datastore.NewQuery("__Stat_Kind__").Filter("kind_name =", "testkind")
	var stats []statKind
	if _, err := q.GetAll(ctx, &stats); err != nil {
		return
	}
	if len(stats) == 0 {
		return
	}
	fmt.Fprintf(w, "%v\n", stats[0].Count)
}
```

これはDatastore読み取り操作1件で実現できるので、なかなかオトクですね。

ただし、このデータは日次バッチで生成されているようで、最大で24-48時間ほど古いデータになってしまいます。また、kindの総エンティティ数しかわからないので、特定の条件を満たす件数を取りたいときには使えません。

# `datastore.Query`の`Count()`メソッドのお値段

DatastoreにはSQLで言うところの集約関数が無いので、総件数を取ろうと思うと全件取得する必要があります。Datastoreはエンティティの読み書き1件あたりの課金体系になっているため、2万エンティティを持つkindの総件数を取るには20000件分の読み取り費用が必要になりそうです。そんなことだとお金がいくらあっても足りませんね。

ところが、実際に2万エンティティにマッチするクエリに対して標準Goライブラリの`Count()`メソッドを試したときの操作の内訳は下記のようになります。

- Datastore 読み取り操作 1件 （$0.78/100万操作）
- Datastore 小規模操作 20000件 （$0.00/100万操作）

なんと読み取り操作が1件分で済んでいます。なぜでしょうか？

実は`Count()`の内部ではキーオンリークエリを利用しているため、いくらエンティティ数が多くても小規模操作で済むようになっています[^1]。この小規模操作の単価は0ドルなので、いくら使っても無料というわけです。これはオトクですね。

[^1]: 「キーのみのクエリは小規模な操作であり、クエリ自体については 1 回のエンティティ読み取りとしてカウントされます。」（ https://cloud.google.com/datastore/docs/concepts/queries#keys-only_queries より）

# Datastore小規模操作を使い放題にする方法

しかし、ここで注意すべき点があります。小規模操作の単価が0ドルというのはGAE有料ユーザーの話で、無料ユーザーには5万件/日のクォータがかかっているのです。この状態では小規模操作のありがたみはほとんどありません。

クォータを外すにはGAEプロジェクトの1日の課金上限を0以外の数字にする必要があります。課金上限に設定できる最小値は1セントなので、下記のように設定すれば小規模操作が使い放題になります。

<img width="661" alt="Datastore小規模操作を無料にする方法" src="https://qiita-image-store.s3.amazonaws.com/0/46856/ca80a029-eec9-0e7e-30cc-7c2df5cbb68e.png">

というわけで、1ヶ月最大30セント払えばDatastoreの件数が取り放題というわけです！やりましたね！（また料金改定があるかもしれませんが…）

この`Count`メソッドは任意のクエリについて適用できるので、使い勝手も非常に良いと思います。唯一の問題点としては、件数が多いと非常に時間がかかるということです。2万件の場合で約1秒かかるので、数千〜数万件を限度としておくのが無難ではないでしょうか。また、可能なら積極的にMemcacheにキャッシュしていくべきでしょう。

# まとめ

- Datastoreの各kindの総件数が統計用kindの`__Stat_Kind__ `からお安く取り出せる
    - 日次バッチで更新されるため最新の値ではない点に注意
- 標準のDatastoreライブラリの`Count`メソッドでDatastoreの任意クエリの件数がお安く取り出せる
    - 少なくともGoライブラリには存在、他の言語は未調査
    - 小規模操作の上限を外すためにプロジェクトの課金上限を1セント以上にしておく必要がある

`__Stat_Kind__`には更新日時も格納されているので、両方を組み合わせて最新の総件数を取り出すような使い方も可能だと思います。