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 沒有,PartiQL 也沒有,它的 SELECT 只接受 WHERE 與 ORDER BY。你要彙總,就得隨資料變動時預先計算總數(原子計數器,或 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 參考:過濾
「不會消耗任何額外的讀取容量單位」;容量是依掃描的項目
大小計算,而非回傳的)。一開始就沒有 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 進行具現化彙總查詢):
- 應用程式寫入原始項目(一筆訂單、一次下載、一個事件)。沒有彙總 邏輯。
- DynamoDB Streams 把該寫入捕捉成一筆串流記錄。
- 接到串流的 Lambda 讀取新項目、推導出分組
(狀態、月份、類別…),並用原子的
UpdateItem對相符的彙總項目做ADD— 當許多次調用碰到同一個計數器時,這會「避免讀-改-寫競爭狀況」。 - 你查詢預先計算好的彙總 — 通常透過一個稀疏 GSI,
只為彙整項目建立索引,因此「本月前 10 名」是一次帶有
Limit 10的Query。
稀疏 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 BY | DynoTable Workbench(下方)或 Athena |
| 用 SQL 進行週期性 BI/報表 | Athena DynamoDB 連接器 |
在 DynoTable 的 SQL Workbench 中直接執行 GROUP BY
上面的模式是你在生產環境中提供彙總的方式。但當你在 探索一張資料表時 — 「現在每個狀態有幾筆訂單?」 — 你不會想 佈建一個 Lambda 或架起 Athena。你想要的是把查詢打出來。
那正是 DynoTable 的 SQL Workbench 的用途。它對你的
即時 DynamoDB 資料表直接執行真正的 SQL — GROUP BY、COUNT、SUM、AVG、
HAVING,甚至 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 就用,必須 Scan 才 Scan),把它們具現化,再
在 Workbench 中做分組 — 與模式 3 相同的「先讀再彙總」機制,
只是少了那個迴圈,並且在 DynamoDB 存取模式規則之內。它是
為探索與臨時分析而打造,不是用來在熱讀取路徑上取代生產
彙整。要做那個,請預先計算(模式 1 / 2)。
關於同一切入點的 JOIN 那一面 — DynoTable 也能執行 PartiQL
做不到的跨表 join — 請見 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 中不行。如果「按分割鍵」是已知的存取模式,
就用原子的 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 指向一張即時資料表。