DynamoDB で COUNT・SUM・集計を行う方法
DynamoDB に組み込まれた集計はちょうど 1 つだけ: Select=COUNT でマッチしたアイテムを数えることだ。ネイティブの SUM・AVG・MIN・MAX はない。そして 得られる カウントですら、数えたすべてのアイテムを読む(そして課金される)。このガイドでは、実際にサポートされているもの、人々が手を伸ばす近似手段、そして必要なときにテーブル全体に対して本物の COUNT/SUM/AVG を実行する方法を取り上げる。
DynamoDB は SUM・COUNT・集計関数を実行できますか?
ほとんどの場合はできません。DynamoDB に組み込まれた集計は Select=COUNT だけで、マッチしたアイテム数を返しますが、それでもすべてのアイテムを読み(そして課金され)ます。ネイティブの SUM・AVG・MIN・MAX はなく、PartiQL もそれらを追加しません。GROUP BY を伴う本物の集計には、アプリ側で畳み込むか、カウンタを保守するか、DynoTable の Workbench で SQL を実行してください。
Select=COUNTはマッチしたアイテム数を返すが、DynamoDB はそれを生成するために依然としてすべてのアイテムを読む — 安価な「カウント」コストではなく、Scan/Queryの読み取りコストをまるごと支払う。- ネイティブの
SUM・AVG・MIN・MAXはない。 DynamoDB の読み取りオペレーションはアイテムを返すだけで、それらを 1 つの数値に畳み込まない。PartiQL も集計を追加しない。 DescribeTable.ItemCountは無料だが 近似値で、更新は「おおよそ 6 時間ごと」 にしか行われない — ダッシュボードのタイルには十分だが、正確さが必要なものには間違っている。- 正確な
COUNT/SUM/AVG/MIN/MAX(GROUP BY付き)には、アプリ側で集計するか、カウンタを保守するか、DynoTable の SQL Workbench で実行する(後述)。
アイテムを数える: Select=COUNT
Query と Scan はどちらも Select パラメータを受け取る。これを COUNT に設定すると、レスポンスはアイテムの代わりにカウントを運ぶ:
aws dynamodb scan \
--table-name Orders \
--select COUNT \
--filter-expression "#s = :open" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":open":{"S":"OPEN"}}'レスポンスは 2 つの数値を返す (AWS: Counting the items in the results):
Count— 「フィルタ式(あれば)が適用された 後 に残るアイテムの数」。ScannedCount— 「ScanFilterが適用される 前 に評価されたアイテムの数」。フィルタがなければ、ScannedCountはCountと同じになる。
パーティションキーしかなく、その中の重複を数える必要がある場合、渡す条件 + フィルタはまさに DynamoDB Expression Builder が生成するもの — 上記の KeyConditionExpression・FilterExpression・ExpressionAttributeNames/Values マップ — を JSON の手作業エスケープなしで得られる。
大きなテーブルを数える人を悩ませる、もう 2 つの落とし穴:
- 1 MB のページ制限は依然として適用される。 「
Scanの結果セットのサイズが 1 MB を超える場合、ScannedCountとCountは総アイテム数のうちの部分的なカウントしか表しません」 (AWS Scan ドキュメント)。本当の数値を得るには、LastEvaluatedKey→ExclusiveStartKeyでページングし、合計を積み上げ続けなければならない — DynamoDB のページネーション で扱っているのと同じループだ。 - 狭い
QueryはScanに勝る。Queryに対するSelect=COUNTは、テーブル全体ではなく、対象パーティション内のアイテムだけをメーター計上する。パーティションキー(ベーステーブルまたは GSI)を確定できるなら、そこで数えるべきだ — これはカウントに適用された Query vs Scan のコスト差だ。
Select=COUNT vs ItemCount(そしてなぜ古くなるのか)
DescribeTable は読み取りコストなしで無料で ItemCount(と TableSizeBytes)を返す。落とし穴は API リファレンスそのもの にある: 「DynamoDB はこの値をおおよそ 6 時間ごとに更新します。最近の変更はこの値に反映されていない可能性があります」。つまり、テーブルの実際の状態よりかなり遅れることがある。
Select=COUNT | DescribeTable.ItemCount | |
|---|---|---|
| 正確さ | 正確(マッチした集合に対して) | 近似 |
| 鮮度 | ライブ | おおよそ 6 時間ごとに更新 |
| コスト | 数えたすべてのアイテムを読み課金 | 無料(メタデータ) |
| サブセットのフィルタ / カウント | はい(フィルタ式) | いいえ — テーブル全体のみ |
「このテーブルはどれくらい大きいか」のおおまかな感覚チェックやダッシュボードのタイルには ItemCount を使う。正確・フィルタ済み・最新の数値が必要なときは Select=COUNT を使い — 読み取りコストを受け入れる。本当にライブで無料のものが必要なら、自分でカウンタを追跡する(下記の 集計パターン を参照)。
なぜネイティブの SUM/AVG/MIN/MAX がないのか
DynamoDB の読み取りオペレーションはアイテムを返す。結果セットをスカラーに畳み込むクエリプランナが存在しないため、SUM や AVG を計算する対象がない。カウントが API の提供する唯一の畳み込みであり、Select=COUNT を介して行われる。
PartiQL もこれを変えない。PartiQL の SELECT 文法 は SELECT {{expression}} [, …] FROM {{table}}[.{{index}}] [WHERE …] [ORDER BY {{key}} …] であり、ここで式は「* ワイルドカード、または 1 つ以上の属性名やドキュメントパスからなる射影リストで形成される射影」だ。この文法には集計関数も GROUP BY 句もない — そして ORDER BY は {{key}} を取り、それは「返される結果の順序付けに使うハッシュキーまたはソートキー」とドキュメント化されている。すべての PartiQL の SELECT は依然として GetItem・Query・Scan にコンパイルされるので、SELECT SUM(total) FROM "Orders" は単に表現できない。(PartiQL の上限については PartiQL vs SQL で詳しく。)
集計パターン(カウンタ、ストリーム、アプリ側)
DynamoDB が集計してくれない以上、確立されたパターンは作業を他所へ押しやる:
- 保守されたカウンタアイテム。 専用のアイテム(例
PK = "STATS#orders")を保持し、書き込みのたびにUpdateItemで数値属性にADDする。集計の読み取りはその場合、単一のGetItemになる — 正確で安価だが、増分ロジック・その一貫性・1 つのカウンタが叩かれ続けた場合の競合は自分の責任になる。 - DynamoDB Streams → 集計器。 ストリームを有効にして、アイテムが変化するたびに走行中の合計(カウント、サム)を更新する Lambda に配線する。AWS Streams ドキュメント によれば、ストリームの
StreamViewTypeを構成して各レコードにNEW_AND_OLD_IMAGES—「アイテムの新しいイメージと古いイメージの両方」— を運ばせることができ、再スキャンなしでSUM系の集計を最新に保つのに十分だ。ストリームレコードには 24 時間の寿命がある(「シャード内のストリームレコードは 24 時間後に自動的に削除されます」)ので、コンシューマは追いつかなければならない。 - アプリ側の畳み込み。 マッチしたアイテムをページングし、自分のコードで
SUM/AVG/MIN/MAXを積み上げる。正しいが、毎回すべてのアイテムを読む(そして課金される) —Select=COUNTと同じコストプロファイルに、加えてデータ転送がかかる。 - 分析へのオフロード。 重いまたはアドホックな分析的集計には、テーブルを S3 にエクスポートして Athena でクエリするか、ウェアハウスにストリームする。AWS の S3 エクスポートドキュメント によれば、エクスポートは「読み取りキャパシティユニットを消費せず」、「Athena のような AWS サービスを使って分析や複雑なクエリを実行する」ことを可能にする — リクエストごとの集計を卒業したら AWS が推奨する経路だ。
それぞれが、書き込み時の帳簿付け(カウンタ、ストリーム)か読み取り時のコスト(アプリ側のスキャン)かのどちらかと引き換えに簡潔さを得る。DynamoDB 自身に SUM を無料で計算させるパターンはない。このトレードオフのグループ化版 — テーブル全体ではなく キーごと に集計する — は別のガイドだ: DynamoDB GROUP BY。
DynoTable の SQL Workbench で COUNT/SUM/AVG を実行する
ページングするスキャンループや Lambda を書かずに、ただ答えがほしいとき —「OPEN な注文はいくつあり、その合計はいくらか」— DynoTable の SQL Workbench は本物の集計を実行する。あなたのテーブルを DynamoDB の実際の Query/Scan ランタイムを通してマテリアライズし、その上であなたの完全な SQL を実行する: DynamoDB のアクセスパターンの規則の中での SQL。
-- Runs in the DynoTable Workbench (NOT in PartiQL):
SELECT status,
COUNT(*) AS orders,
SUM(total) AS revenue,
AVG(total) AS avg_order,
MIN(total) AS smallest,
MAX(total) AS largest
FROM orders
GROUP BY status
ORDER BY revenue DESCこれが COUNT・SUM・AVG・MIN・MAX・GROUP BY・ORDER BY — どれも DynamoDB も PartiQL も表現できない — を 1 ステートメントで実現する。これは SQL for DynamoDB と同じ分析的な楔だ。完全なグループ化の話は DynamoDB GROUP BY を参照。
Workbench は下にあるアクセスモデルについて正直で、Postgres のふりをしない:
- 行は依然として DynamoDB の本物の Query/Scan を通ってくる。テーブル全体に対する
GROUP BYは下では依然としてScanだ — Workbench はそのコストを隠すのではなく表に出す。同じ Query vs Scan のトレードオフだ。 - 集計は、行が着地した後、マテリアライズされたスカラー属性に対して実行される。
FAQ
スキャンせずに DynamoDB でアイテムを数えられますか?
正確には無理です。正確で最新のカウントには、アイテムを読まなければなりません — Select=COUNT は依然として数えたすべてのアイテムをメーター計上します。スキャンなしの選択肢は、近似の DescribeTable.ItemCount(おおよそ 6 時間ごとに更新)か、書き込みのたびに自分で保守するカウンタアイテムだけです。
GSI でアイテムを数えるにはどうしますか?
Select=COUNT を付けてインデックスに対して Query(または Scan)を実行します。狭い GSI パーティションを介して数えるほうが、そのインデックスパーティション内のアイテムしか読まないため、ベーステーブルをスキャンするよりはるかに安価です — 必要なカウントに合わせてインデックスを設計してください。
DescribeTable.ItemCount は正確ですか?
近似です。API リファレンス は、DynamoDB が ItemCount と TableSizeBytes を「おおよそ 6 時間ごと」に更新し、「最近の変更はこの値に反映されていない可能性があります」と述べています。正確またはライブな数値が重要な場面では使わないでください。
DynamoDB は SUM や AVG を実行できますか?
ネイティブには無理で、PartiQL でも無理です — PartiQL の SELECT 文法 に集計関数はありません。アプリケーションで集計するか、カウンタを保守する(任意で DynamoDB Streams 経由)か、DynoTable の SQL Workbench で SUM/AVG を実行してください。
Count と ScannedCount の違いは何ですか?
ScannedCount はフィルタの前に DynamoDB が評価したアイテム数で、Count はその後に残った数です。フィルタ式がなければ両者は等しくなります。両者の大きな差は、非効率なカウントを意味します。
スキャンループを書かずに DynamoDB のデータを合計・平均・グループ化する必要がありますか? DynoTable をダウンロードして Workbench タブで実行してください。クライアントの比較が先? 素の DynamoDB GUI に対してどう位置づけられるかを参照してください。