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 にもです。集計は、データが変わるときに合計を事前計算する(アトミックカウンター、または Streams + Lambda のロールアップ)か、読み取った後にアプリ側でグループ化することで行います。
- DynamoDB の PartiQL
SELECT文法はSELECT … FROM … [WHERE …] [ORDER BY …]— そしてそれがリストのすべてです。GROUP BYも、HAVINGも、集計関数も、JOINも ありません(AWS PartiQLSELECTリファレンス)。 - DynamoDB は「項目をまたいだ
SUMやCOUNTのような集計操作をネイティブにサポートしない」ため、 AWS 自身の指針は、データが変わるときに集計を 事前計算 し、結果を通常の項目として保存することです (AWS: マテリアライズド集計)。 - 代替案 — すべての項目を読んでからアプリで集計する — は動きますが、クエリのたびに テーブル全体を読む対価を払います。
- 一度きりの探索には、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 は数値とセットに対して動き、読み取り-変更-書き込みの競合を避けるので、同じカウンターを
インクリメントする2つのライターが互いを潰すことはありません
(AWS はアトミックな ADD が「読み取り-変更-書き込みの競合状態を避ける」と記しています)。
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 自身の推奨パターン、マテリアライズド集計です
(AWS: マテリアライズド集計クエリのための GSI の使用)。
- アプリが生の項目(注文、ダウンロード、イベント)を書きます。集計ロジックなし。
- DynamoDB Streams がその書き込みをストリームレコードとして捕捉します。
- ストリームにアタッチされた Lambda が新しい項目を読み、グループ(ステータス、月、
カテゴリ…)を導出し、アトミックな
UpdateItemで一致する集計項目にADDします — 多くの呼び出しが同じカウンターに触れるとき「読み取り-変更-書き込みの競合状態を避け」ます。 - 事前計算された集計をクエリします — しばしば、ロールアップ項目だけをインデックスする
スパース GSI を通して、「今月のトップ10」は
Limit 10付きの1つのQueryになります。
スパース GSI のトリック: 集計項目だけがインデックス属性(例 Month)を持つので、生のイベント行は
自動的にインデックスから除外されます — 「テーブルの全項目のごく一部」で、インデックスを安価に、
読み取りを速く保ちます。
これは集計を書き込みパスから分離し、書き込みを単純に保ちます。代償は 結果整合性 です — AWS は「ダウンロードが記録されてから集計が更新されるまで数秒の遅延」と記しています。ダッシュボード、 リーダーボード、トレンドカウンターにはこれで問題ありません。
同じ再試行の注意が適用されます: 再試行された Lambda 呼び出しは ADD を再実行するので、
「再試行はカウントを2回以上インクリメントする」ことになり、概算の値が残ります。正確なカウントには
冪等性を加えてください(例 ソース項目の id をキーにした条件式)。そうでなければ、分析や
リーダーボードには小さなマージンで問題ありません。
パターン3: Scan/Query 後のアプリ側グループ化
力技の選択肢: 項目を読み、自分のコードでグループ化します。
groups = {}
for item in paginate(table.scan()): # 1パーティションには query()
key = item["status"]
groups[key] = groups.get(key, 0) + 1これは正しく、ときに正しい判断です — が、コストについて正直になりましょう。Scan は
テーブル内のすべての項目 を読み、読み取りキャパシティはフィルターするかどうかに関わらず同じです。
だからフル Scan に対するアプリ側グループ化は、集計のたびにテーブル全体を読む対価を払うことを
意味し、レイテンシーはテーブルとともに増えます。AWS は「読み取り時のスキャンとカウント」を
「レイテンシーが問題にならない非常に小さなデータセットにのみ適する」と挙げています
(AWS: なぜ集計を事前計算するか)。
Query 経由で単一パーティションに絞れば(例 1顧客の注文を数える)、アプリ側グループ化は
まったく合理的です — 1つの項目コレクションを読むだけです。両者のフルコスト差は
Query vs Scanを参照してください。所定の集計スキャンが実行前に何を
読むかを見積もるには、項目サイズ計算機で代表的な項目を
測ってください — 読み取りキャパシティは 4 KB ごとに切り上げなので、
項目サイズが請求を左右します。
DynamoDB テーブルに対する真にアドホックな分析 SQL — 一度だけ実行する使い捨ての
「GROUP BY status、それらを数える」 — には、AWS の答えは別のエンジンを向けることです。
Amazon Athena DynamoDB コネクタ が、Lambda コネクタ経由で本物の SQL(GROUP BY、集計、
他のソースへの JOIN まで)でテーブルをクエリできます
(AWS: Amazon Athena DynamoDB コネクタ)。
裏でテーブルをスキャンするので、ホットパスではなくレポーティング/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 比較を参照してください。
FAQ
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: 合計とカウントの両方を
保存し、読み取り時に割ります — ネイティブの平均はありません。一度きりの探索的な AVG には、
DynoTable の SQL Workbench または Athena DynamoDB コネクタ経由で実行してください。
partiql group by の回避策はありますか?
PartiQL 側のものはありません。集計を事前計算(カウンター/Streams)してロールアップ項目を
SELECT するか、GROUP BY を持つエンジンで実行してください — アドホックには DynoTable の
Workbench、反復的なレポーティングには Athena です。
Lambda を書かずに自分のテーブルに対して GROUP BY を実行したいですか?
DynoTable を試して、SQL Workbench をライブテーブルに向けてください。