DynamoDB GROUP BY:GROUP BY 句なしで集計する方法
DynamoDB に GROUP BY はありません。COUNT、SUM、AVG もありません — ネイティブ API
にも、PartiQL にもです。DynamoDB は分析エンジンではなく、キーバリュー / ドキュメント
ストアなので、集計はクエリプランナーが代わりにやってくれるものではなく、あなた が
組み立てるものです。
DynamoDB で GROUP BY はできるのか?
いいえ。DynamoDB には GROUP BY、HAVING、そして COUNT、SUM、AVG のような集計関数がありません — ネイティブ API にも、SELECT が WHERE と ORDER BY しか受け付けない PartiQL にもです。データが変わるにつれて合計を事前計算する(アトミックカウンターや + Lambda のロールアップ)か、読み取り後にアプリ側でグループ化することで集計します。
- DynamoDB の PartiQL
SELECT文法はSELECT … FROM … [WHERE …] [ORDER BY …]— そして それがリストのすべてです。GROUP BYも、HAVINGも、集計関数も、JOINもありません (AWS PartiQLSELECTリファレンス)。 - DynamoDB は「アイテムをまたぐ
SUMやCOUNTのような集計操作をネイティブにサポートして いない」ため、AWS 自身のガイダンスは、データが変わるにつれて集計を 事前計算 し、その 結果を通常のアイテムとして保存することです (AWS: materialized aggregation)。 - 代替 — すべてのアイテムを読んでからアプリで集計する — は機能しますが、クエリごとに テーブル全体を読む対価を支払います。
- 単発の探索には、DynoTable の SQL Workbench が
GROUP BY/COUNT/SUM/AVGを ライブテーブルに対して直接実行します — DynamoDB の PartiQL エンドポイントが拒否する SQL です。
なぜ DynamoDB で集計が難しいのか
DynamoDB にはスキャン時の集計エンジンがありません。Query と Scan はアイテムを返します。
それらを畳み込みはしません。Scan はテーブル全体を 1 MB ずつ読み、消費するキャパシティは
残す行ではなく読んだアイテムに基づきます — FilterExpression はスキャンの 後 かつ結果が
返る 前 に適用されるので、課金を下げずに結果セットを絞る のです
(AWS Scan API リファレンス:
フィルタは「追加の読み取りキャパシティユニットを消費しない」;キャパシティは返されたのでは
なくスキャンされたアイテムサイズに基づく)。そもそも sum や count を引っ掛ける GROUP-BY の
フックがありません。
PartiQL はこれを変えません。PartiQL は同じエンジン上の SQL 互換 方言なので、同じ限界を
継承します — 新しい実行モデルではなく、構文のサーフェスです。
文書化された SELECT 文法
には GROUP BY トークンが単純にありません。
PartiQL と本物の SQL の完全な差については、PartiQL vs SQL を参照。
だから問いは「GROUP BY をどう書くか」ではありません — 「私の集計はどこに存在し、いつ計算
されるのか」です。答えは 3 つあります。
パターン 1:書き込み時に集計する(アトミックカウンター)
グループを前もって知っているなら — ステータスごとのカウント、顧客ごとの合計、月ごとの ダウンロード — カウンターアイテムを保持し、書き込みごとにそれを更新します。
インクリメントがアトミックで並行安全になるよう、ADD を使います。
ADD は数値とセットで機能し、read-modify-write の競合を避けるので、同じカウンターを
インクリメントする 2 つの書き手が互いを上書きすることは決してありません
(AWS はアトミックな ADD が「read-modify-write の競合状態を避ける」と記しています)。
UpdateItem
Key { pk: "STATS#orders", sk: "status#shipped" }
UpdateExpression "ADD orderCount :one"
ExpressionAttributeValues { ":one": 1 }
これがあなたの SELECT COUNT(*) … GROUP BY status です — ただしカウントはすでにアイテム
としてそこに座っており、1 桁ミリ秒の GetItem で読めます。トレードオフ:書き込み時に
グルーピングキーを知っている必要があり、カウンターの更新を書き込みパスに結合します。
書き込みの 後 かつカウンター更新の 前 にアプリがクラッシュすると、両者は同期からずれます
— これはまさに、次のパターンが分離する故障モードです。
パターン 2:DynamoDB Streams + Lambda のロールアップ
集計ロジックを書き込みパスに置きたくないとき — または書き込みが簡単にラップできない素の
PutItem のとき — それを下流に移します。これは AWS 自身の推奨パターン、materialized
aggregation です
(AWS: Using GSIs for materialized aggregation queries)。
- アプリが生のアイテム(注文、ダウンロード、イベント)を書きます。集計ロジックはなし。
- が書き込みをストリームレコードとして捕捉します。
- ストリームに接続された Lambda が新しいアイテムを読み、グループ(ステータス、月、
カテゴリ…)を導出し、アトミックな
UpdateItemで対応する集計アイテムにADDします — 多数の呼び出しが同じカウンターに触れるとき、これは「read-modify-write の競合状態を 避け」ます。 - あなたは事前計算された集計をクエリします — しばしば、ロールアップアイテムだけを
インデックスする を通して。そうすれば「今月のトップ 10」は
Limit 10を伴う 1 回のQueryです。
スパース GSI のトリック:集計アイテムだけがインデックス対象の属性(例:Month)を運ぶので、
生のイベント行は自動的にインデックスから除外されます — 「テーブル内の総アイテムのわずかな
一部」で、インデックスを安く保ち読み取りを速く保ちます。
これは集計を書き込みパスから分離し、書き込みをシンプルに保ちますが、その対価は 結果整合性 です — AWS は「ダウンロードが記録されてから集計が更新されるまで数秒の遅延」と記しています。 ダッシュボード、リーダーボード、トレンドカウンターにはそれで問題ありません。
同じリトライの注意が当てはまります。リトライされた Lambda 呼び出しは ADD を再実行するので、
「リトライはカウントを 2 回以上インクリメントしうる」、近似 値を残します。正確なカウントには、
べき等性を加えましょう(例:ソースアイテムの id でキー付けした条件式)。そうでなければ、小さな
マージンは分析やリーダーボードには問題ありません。
パターン 3:Scan/Query 後のアプリ側グルーピング
力任せの選択肢:アイテムを読み、自分のコードでグループ化します。
groups = {}
resp = table.scan() # or query() for one partition
while True:
for item in resp["Items"]:
key = item["status"]
groups[key] = groups.get(key, 0) + 1
if "LastEvaluatedKey" not in resp:
break
resp = table.scan(ExclusiveStartKey=resp["LastEvaluatedKey"])これは正しく、時には正しい選択です — でもコストについて正直になりましょう。Scan は
テーブル内のすべてのアイテムを読み、フィルタするかどうかにかかわらず読み取りキャパシティは
同じです。だからフルの Scan に対するアプリ側グルーピングは、集計のたびにテーブル全体を読む
対価を支払うことを意味し、レイテンシはテーブルとともに増大します。AWS は「読み取り時の
スキャンとカウント」を「レイテンシが問題にならない、ごく小さなデータセットにのみ適する」と
挙げています
(AWS: Why pre-compute aggregations)。
Query を介して単一のパーティションに絞り込めば(例:1 人の顧客の注文を数える)、アプリ側
グルーピングは完全に妥当です — あなたは 1 つのアイテムコレクションだけを読んでいます。両者の
完全なコスト差については、Query と Scan を参照。あるアグリゲーション
スキャンが実行前に何を読むかを見積もるには、代表的なアイテムのサイズを
アイテムサイズ計算ツール で測りましょう —
読み取りキャパシティは 4 KB ごとに切り上げ
なので、アイテムサイズが課金を左右します。us-east-1 のオンデマンドでは、テーブル全体の
Scan は評価したアイテムごとに結果整合性で 4 KB あたり 0.5 RCU を課金します — 1 KB の
行が入った 1 GB のテーブルなら、アプリが何かをグループ化する前におよそ 250,000 RCU です。
そのスキャンの実行単価は料金計算ツールで見積もりましょう。
DynamoDB テーブルに対する本当にアドホックな分析 SQL — 一度だけ実行する「GROUP BY status、
数える」という使い捨て — には、AWS の答えは別のエンジンをそこに向けることです。Amazon
Athena DynamoDB コネクタ が、Lambda コネクタを介して、そのテーブルを本物の SQL
(GROUP BY、集計、他のソースへの JOIN すら)でクエリさせます
(AWS: Amazon Athena DynamoDB connector)。
裏でテーブルをスキャンするので、ホットパスではなくレポーティング / BI ツールです。
どのパターンを使うか?
| 必要なもの… | 使うもの |
|---|---|
| ホットな読み取りパス上の既知のグループ合計 | パターン 1 — アトミックカウンター(ADD) |
| 書き込みパスに触れない集計 | パターン 2 — Streams + Lambda のロールアップ |
| 1 つのパーティションに絞ったカウント | パターン 3 — Query してからアプリでグループ化 |
| 正確な合計、ずれなし | パターン 1/2 を べき等性ガード付き で |
探索中の単発の GROUP BY | DynoTable Workbench(下記)または Athena |
| SQL による定期的な BI / レポーティング | Athena DynamoDB コネクタ |
DynoTable の SQL Workbench で GROUP BY を直接実行する
上記のパターンは、集計を 本番で 提供する方法です。でもテーブルを探索しているとき — 「今この瞬間、ステータスごとに注文はいくつ?」 — Lambda をプロビジョニングしたり Athena を 立ち上げたりしたくはありません。クエリをタイプしたいのです。
それが DynoTable の SQL Workbench の役目です。本物の SQL — GROUP BY、COUNT、SUM、
AVG、HAVING、JOIN すら — をライブの DynamoDB テーブルに対して直接実行し、読み取った
行に対してクライアント側で集計を行います。DynamoDB の PartiQL エンドポイントが拒否する SQL
です。
SELECT status, COUNT(*) AS orders, SUM(total) AS revenue
FROM "Orders"
GROUP BY status
HAVING SUM(total) > 1000
ORDER BY revenue DESC正直な位置づけ:内部では DynoTable は API が許す方法でアイテムを読み(できる場合は Query、
せざるを得ない場合は Scan)、それらをマテリアライズし、Workbench でグルーピングを行います
— パターン 3 と同じ「読んでから集計」の仕組みで、ただループなしで、そして DynamoDB の
アクセスパターンのルールの中で。これは 探索とアドホック分析 のために作られたもので、
ホットな読み取りパス上の本番ロールアップを置き換えるためではありません。それには事前計算
しましょう(パターン 1 / 2)。
同じくさびの JOIN 側 — DynoTable は PartiQL ができないクロステーブルの結合も実行します —
については、DynamoDB JOIN を参照。まさにこの能力で GUI クライアントを
比較しますか?DynamoDB GUI 比較 を参照してください。
よくある質問
DynamoDB PartiQL は GROUP BY をサポートしますか?
いいえ。DynamoDB の PartiQL SELECT は WHERE と ORDER BY のみをサポートします — GROUP BY、
HAVING、集計関数、JOIN はありません。文法は SELECT … FROM … [WHERE …] [ORDER BY …] と
文書化
されています。
DynamoDB テーブル全体に対して COUNT(*) はできますか?
集計関数としてはできません — PartiQL には集計関数がありません。API は Scan/Query に対する
Select=COUNT を与え、これは マッチしたアイテムの カウントを返しますが、それでもスキャンが
触れるすべてのアイテムを読み(課金し)ます
(AWS Scan API リファレンス:
キャパシティは返されたのではなく調べたアイテムに基づく)。頻繁に読まれる合計には、
カウンターアイテムを保持しましょう(パターン 1)。
パーティションキーで GROUP BY できますか?
DynamoDB でも PartiQL でもできません。「パーティションキーごと」が既知のアクセスパターンなら、
キーごとに 1 つの集計アイテムをアトミックな ADD で維持する(パターン 1)か、Streams + Lambda
でロールアップします(パターン 2)。
グループごとに SUM や AVG をするには?
SUM:グループごとに実行中の合計を保持し、書き込み時に ADD します。AVG:sum と count の
両方を保存し、読み取り時に割ります — ネイティブな average はありません。単発の探索的な AVG
には、DynoTable の SQL Workbench か Athena DynamoDB コネクタで実行しましょう。
partiql group by の回避策はありますか?
PartiQL 側のものはありません。集計を事前計算(カウンター/Streams)して ロールアップアイテムを
SELECT するか、GROUP BY を持つエンジンでそれを実行しましょう — アドホックには DynoTable の
Workbench、定期的なレポーティングには Athena です。
Lambda を書かずに自分のテーブルに対して GROUP BY を実行したいですか?
DynoTable を試して SQL Workbench をライブテーブルに向けましょう。