中級読了 4 分

DynamoDB GROUP BY:GROUP BY 句なしで集計する方法

DynamoDB に GROUP BY はありません。COUNTSUMAVG もありません — ネイティブ API にも、PartiQL にもです。DynamoDB は分析エンジンではなく、キーバリュー / ドキュメント ストアなので、集計はクエリプランナーが代わりにやってくれるものではなく、あなた が 組み立てるものです。

DynamoDB で GROUP BY はできるのか?

いいえ。DynamoDB には GROUP BYHAVING、そして COUNTSUMAVG のような集計関数がありません — ネイティブ API にも、SELECTWHEREORDER BY しか受け付けない PartiQL にもです。データが変わるにつれて合計を事前計算する(アトミックカウンターや + Lambda のロールアップ)か、読み取り後にアプリ側でグループ化することで集計します。

  • DynamoDB の PartiQL SELECT 文法は SELECT … FROM … [WHERE …] [ORDER BY …] — そして それがリストのすべてです。GROUP BY も、HAVING も、集計関数も、JOIN もありません (AWS PartiQL SELECT リファレンス)。
  • DynamoDB は「アイテムをまたぐ SUMCOUNT のような集計操作をネイティブにサポートして いない」ため、AWS 自身のガイダンスは、データが変わるにつれて集計を 事前計算 し、その 結果を通常のアイテムとして保存することです (AWS: materialized aggregation)。
  • 代替 — すべてのアイテムを読んでからアプリで集計する — は機能しますが、クエリごとに テーブル全体を読む対価を支払います。
  • 単発の探索には、DynoTable の SQL WorkbenchGROUP BY / COUNT / SUM / AVG を ライブテーブルに対して直接実行します — DynamoDB の PartiQL エンドポイントが拒否する SQL です。

なぜ DynamoDB で集計が難しいのか

DynamoDB にはスキャン時の集計エンジンがありません。QueryScan はアイテムを返します。 それらを畳み込みはしません。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)。

  1. アプリが生のアイテム(注文、ダウンロード、イベント)を書きます。集計ロジックはなし。
  2. が書き込みをストリームレコードとして捕捉します。
  3. ストリームに接続された Lambda が新しいアイテムを読み、グループ(ステータス、月、 カテゴリ…)を導出し、アトミックな UpdateItem で対応する集計アイテムに ADD します — 多数の呼び出しが同じカウンターに触れるとき、これは「read-modify-write の競合状態を 避け」ます。
  4. あなたは事前計算された集計をクエリします — しばしば、ロールアップアイテムだけを インデックスする を通して。そうすれば「今月のトップ 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 BYDynoTable Workbench(下記)または Athena
SQL による定期的な BI / レポーティングAthena DynamoDB コネクタ

DynoTable の SQL Workbench で GROUP BY を直接実行する

上記のパターンは、集計を 本番で 提供する方法です。でもテーブルを探索しているとき — 「今この瞬間、ステータスごとに注文はいくつ?」 — Lambda をプロビジョニングしたり Athena を 立ち上げたりしたくはありません。クエリをタイプしたいのです。

それが DynoTable の SQL Workbench の役目です。本物の SQL — GROUP BYCOUNTSUMAVGHAVINGJOIN すら — をライブの 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 SELECTWHEREORDER BY のみをサポートします — GROUP BYHAVING、集計関数、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)。

グループごとに SUMAVG をするには? 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 をライブテーブルに向けましょう。

更新日