中階閱讀時間 4 分鐘

DynamoDB GROUP BY:如何在沒有 GROUP BY 子句的情況下彙總

DynamoDB 中沒有 GROUP BY。也沒有 COUNTSUMAVG — 原生 API 沒有,PartiQL 也沒有。DynamoDB 是鍵值/ 文件儲存,不是分析引擎,所以彙總是 要建構的東西,而不是查詢規劃器替你做的事。

你能在 DynamoDB 中使用 GROUP BY 嗎?

不能。DynamoDB 沒有 GROUP BYHAVING,也沒有 COUNTSUMAVG 之類的彙總函式 — 原生 API 沒有,PartiQL 也沒有,它的 SELECT 只接受 WHEREORDER BY。你要彙總,就得隨資料變動時預先計算總數(原子計數器,或 Streams + Lambda 彙整),或在讀取後於應用程式端分組。

  • DynamoDB 的 PartiQL SELECT 文法是 SELECT … FROM … [WHERE …] [ORDER BY …] — 而那就是全部了。沒有 GROUP BY、沒有 HAVING、沒有彙總 函式,也沒有 JOINAWS PartiQL SELECT 參考)。
  • 因為 DynamoDB「並不原生支援跨項目的 SUMCOUNT 等彙總操作」,AWS 自家的指引是預先計算 彙總值(隨資料變動時),並把結果存成普通的項目 (AWS:具現化彙總)。
  • 另一個替代做法 — 讀取每一個項目,再在你的應用程式中彙總 — 可行,但你 在每次查詢時都要付出讀整張資料表的代價。
  • 對於一次性的探索,DynoTable 的 SQL Workbench 能對一張 即時資料表直接執行 GROUP BY / COUNT / SUM / AVG — 這正是 DynamoDB 的 PartiQL 端點所拒絕的 SQL。

為什麼在 DynamoDB 中彙總很難

DynamoDB 沒有掃描時的彙總引擎。QueryScan 回傳的是項目; 它們不會折疊它們。Scan 一次以 1 MB 讀整張資料表,而 它所消耗的容量是依它讀取的項目計算,而非你保留的資料列 — FilterExpression 是在掃描之後、但在回傳結果之前套用,因此它 在不降低帳單的情況下縮小結果集AWS Scan API 參考:過濾 「不會消耗任何額外的讀取容量單位」;容量是依掃描的項目 大小計算,而非回傳的)。一開始就沒有 GROUP-BY 掛鉤 能掛上加總或計數。

PartiQL 並不改變這點。PartiQL 是在同一個引擎上的 SQL相容 方言,因此它繼承同樣的限制 — 它是一個語法介面,不是新的 執行模型。記載的 SELECT 文法 就是沒有 GROUP BY 這個詞彙。 關於 PartiQL 與真正 SQL 之間的完整缺口,請見 PartiQL vs SQL

所以問題不是「我要怎麼寫一個 GROUP BY」 — 而是「我的 彙總住在哪裡,又在什麼時候被計算?」有三個答案。

模式 1:寫入時彙總(原子計數器)

如果你事先就知道分組 — 按狀態計數、按客戶加總、 按月份計算下載數 — 就保留一個計數器項目,並在每次寫入時更新它。

ADD 更新運算式,讓增量是原子且並行安全的。 ADD 對數字與集合有效,並避免了讀-改-寫的競爭,因此 兩個寫入者對同一個計數器做增量永遠不會互相覆蓋 (AWS 指出原子的 ADD「避免了讀-改-寫競爭狀況」):

UpdateItem
Key                         { pk: "STATS#orders", sk: "status#shipped" }
UpdateExpression            "ADD orderCount :one"
ExpressionAttributeValues   { ":one": 1 }

這就是你的 SELECT COUNT(*) … GROUP BY status — 只不過計數已經 以一個項目坐在那裡,能在個位數毫秒的 GetItem 中讀取。 取捨是:你必須在寫入時就知道分組鍵,而且你把 計數器更新耦合到了寫入路徑上。如果應用程式在寫入之後、但 計數器更新之前當機,兩者就會漂移而不同步 — 這正是 下一個模式所解耦的失敗模式。

模式 2:DynamoDB Streams + Lambda 彙整

當你不想在寫入路徑上放彙總邏輯 — 或寫入是一個 你無法輕易包裝的單純 PutItem 時 — 就把它移到下游。這是 AWS 自家 推薦的模式,具現化彙總AWS:使用 GSI 進行具現化彙總查詢):

  1. 應用程式寫入原始項目(一筆訂單、一次下載、一個事件)。沒有彙總 邏輯。
  2. DynamoDB Streams 把該寫入捕捉成一筆串流記錄。
  3. 接到串流的 Lambda 讀取新項目、推導出分組 (狀態、月份、類別…),並用原子的 UpdateItem 對相符的彙總項目做 ADD — 當許多次調用碰到同一個計數器時,這會「避免讀-改-寫競爭狀況」。
  4. 你查詢預先計算好的彙總 — 通常透過一個稀疏 GSI, 只為彙整項目建立索引,因此「本月前 10 名」是一次帶有 Limit 10Query

稀疏 GSI 的技巧:只有彙總項目帶有被索引的屬性 (例如 Month),因此原始事件資料列會自動被排除在索引外 — 「在資料表的總項目中只佔很小一部分」,這讓索引保持 便宜、讀取保持快速。

這把彙總與寫入路徑解耦,並讓寫入保持簡單,代價是 最終一致 — AWS 指出「在記錄一次下載與 彙總被更新之間,會有幾秒的延遲」。對於儀表板、 排行榜與趨勢計數器而言這沒問題。

同樣的重試注意事項適用:被重試的 Lambda 調用會重跑 ADD,因此 「重試會把計數增加不只一次」,留下一個近似的 值。為了得到精確計數,請加上冪等性(例如一個以 來源項目 id 為鍵的條件運算式);否則對分析與 排行榜而言,那點小誤差沒問題。

模式 3:Scan/Query 之後的應用程式端分組

蠻力選項:讀取項目,再在你的程式碼中分組它們。

groups = {}
for item in paginate(table.scan()):       # 或對單一分割區用 query()
    key = item["status"]
    groups[key] = groups.get(key, 0) + 1

這是正確的,有時也是對的選擇 — 但對成本要誠實。一個 Scan 會讀資料表中的每一個項目,而讀取容量無論 你是否過濾都一樣。所以對整張 Scan 做應用程式端分組,意味著你要付 讀取整張資料表的代價於每次彙總,而且延遲會隨資料表成長。 AWS 把「在讀取時掃描並計數」列為「只適合延遲不是 顧慮的非常小型資料集」 (AWS:為什麼要預先計算彙總)。

透過 Query 縮限到單一分割區(例如計算單一 客戶的訂單),應用程式端分組就完全合理 — 你只讀一個 項目集合。關於兩者間的完整成本差距,請見 Query vs Scan。要在執行前估計某次彙總 掃描會讀多少,請用 項目大小計算器估一個有代表性的項目 — 讀取容量 每 4 KB 向上取整, 因此項目大小決定帳單。

對於真正臨時、對一張 DynamoDB 資料表所做的分析型 SQL — 那種你只跑一次的 「GROUP BY status、數一數」 — AWS 的答案是把 另一個引擎指向它:Amazon Athena DynamoDB 連接器讓你能 用真正的 SQL(GROUP BY、彙總,甚至 JOIN 到其他來源) 透過 Lambda 連接器查詢資料表 (AWS:Amazon Athena DynamoDB 連接器)。 它在幕後掃描資料表,所以它是報表/BI 工具,不是熱路徑。

我該用哪個模式?

你需要…
熱讀取路徑上的已知分組總數模式 1 — 原子計數器(ADD
不碰寫入路徑的彙總模式 2 — Streams + Lambda 彙整
縮限到單一分割區的計數模式 3 — Query 後在應用程式中分組
精確總數、無漂移模式 1/2 加上冪等守衛
探索時的一次性 GROUP BYDynoTable Workbench(下方)或 Athena
用 SQL 進行週期性 BI/報表Athena DynamoDB 連接器

在 DynoTable 的 SQL Workbench 中直接執行 GROUP BY

上面的模式是你在生產環境中提供彙總的方式。但當你在 探索一張資料表時 — 「現在每個狀態有幾筆訂單?」 — 你不會想 佈建一個 Lambda 或架起 Athena。你想要的是把查詢打出來。

那正是 DynoTable 的 SQL Workbench 的用途。它對你的 即時 DynamoDB 資料表直接執行真正的 SQL — GROUP BYCOUNTSUMAVGHAVING,甚至 JOIN — 在它所讀取的資料列上於用戶端 執行彙總。這正是 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 就用,必須 ScanScan),把它們具現化,再 在 Workbench 中做分組 — 與模式 3 相同的「先讀再彙總」機制, 只是少了那個迴圈,並且在 DynamoDB 存取模式規則之內。它是 為探索與臨時分析而打造,不是用來在熱讀取路徑上取代生產 彙整。要做那個,請預先計算(模式 1 / 2)。

關於同一切入點的 JOIN 那一面 — DynoTable 也能執行 PartiQL 做不到的跨表 join — 請見 DynamoDB JOIN。想針對這項 能力比較 GUI 用戶端嗎?請見 DynamoDB GUI 比較

常見問題

DynamoDB PartiQL 支援 GROUP BY 嗎? 不支援。DynamoDB 的 PartiQL SELECT 只支援 WHEREORDER 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 中不行。如果「按分割鍵」是已知的存取模式, 就用原子的 ADD 為每個鍵維護一個彙總項目(模式 1),或用 Streams + Lambda 把它彙整起來(模式 2)。

我要如何按分組做 SUMAVG SUM:為每個分組保留一個累計總數,並在寫入時對它 ADDAVG:同時存 加總與計數,並在讀取時相除 — 沒有原生的平均。 對於一次性的探索型 AVG,請在 DynoTable 的 SQL Workbench 或透過 Athena DynamoDB 連接器執行。

有沒有 partiql group by 的變通做法? 沒有 PartiQL 端的。要嘛預先計算彙總(計數器/Streams)並 SELECT 那個彙整項目,要嘛在一個有 GROUP BY 的引擎中執行 — 臨時用 DynoTable 的 Workbench,週期性報表用 Athena。


想在不寫 Lambda 的情況下對你自己的資料表執行 GROUP BY 嗎? 試用 DynoTable,把 SQL Workbench 指向一張即時資料表。

已更新