入門閱讀時間 3 分鐘

如何在 DynamoDB 中進行 COUNT、SUM 與聚合

DynamoDB 只有一個內建的聚合:用 Select=COUNT 計算符合條件的 item 數量。它沒有原生的 SUMAVGMINMAX。而且就連你 取得的那個計數,也會讀取(並計費)它所計數的每一個 item。本文涵蓋實際支援的功能、人們會採用的近似做法,以及當你需要時如何對一張表執行真正的 COUNT/SUM/AVG

DynamoDB 能做 SUM、COUNT 與聚合函式嗎?

大致上不行。DynamoDB 唯一內建的聚合是 Select=COUNT,它會回傳符合條件的 item 數量,但仍會讀取(並計費)每一個 item。它沒有原生的 SUMAVGMINMAX,PartiQL 也沒有補上任何一個。若要搭配 GROUP BY 做真正的聚合,請在你的應用程式中折疊計算、維護一個計數器,或在 DynoTable 的 Workbench 中執行 SQL

  • Select=COUNT 回傳符合條件的 item 數量,但 DynamoDB 仍會讀取每一個 item 才能算出它——你付的是完整的 Scan/Query 讀取成本,而不是便宜的「計數」成本。
  • 沒有原生的 SUMAVGMINMAX DynamoDB 的讀取操作回傳 item;它們不會把這些 item 折疊成一個數字。PartiQL 也沒有加上聚合函式。
  • DescribeTable.ItemCount 是免費的,但是近似的,而且只「大約每六小時」更新一次——拿來當儀表板小卡可以,要精確就不行。
  • 若要精確的 COUNT/SUM/AVG/MIN/MAX(搭配 GROUP BY),請在你的應用程式中聚合、維護一個計數器,或在 DynoTable 的 SQL Workbench 中執行(見下文)。

計數 item:Select=COUNT

QueryScan 都接受一個 Select 參數。把它設為 COUNT,回應就會帶有計數而非 item:

aws dynamodb scan \
  --table-name Orders \
  --select COUNT \
  --filter-expression "#s = :open" \
  --expression-attribute-names '{"#s":"status"}' \
  --expression-attribute-values '{":open":{"S":"OPEN"}}'

回應會給你兩個數字(AWS:計算結果中的 item 數量):

  • Count——「在套用 filter expression(若有)之後 剩下的 item 數量。」
  • ScannedCount——「在套用任何 ScanFilter 之前 被評估的 item 數量。」沒有 filter 時,ScannedCountCount 相同。

如果你只有 partition key,又需要計算其中的重複項,你傳入的條件 + filter 正是 DynamoDB Expression Builder 所產生的——上面那些 KeyConditionExpressionFilterExpressionExpressionAttributeNames/Values 對映——而且不必手動轉義 JSON。

還有兩個會咬到計數大表的人的陷阱:

  • 1 MB 分頁上限仍然適用。「如果 Scan 結果集的大小超過 1 MB,ScannedCountCount 只代表全部 item 的部分計數」(AWS Scan 文件)。你必須用 LastEvaluatedKeyExclusiveStartKey 分頁並保留累計總數,才能得到真正的數字——也就是 DynamoDB 分頁 中涵蓋的同一個迴圈。
  • 窄範圍的 Query 勝過 ScanQuery 上用 Select=COUNT 只計量目標 partition 內的 item,而非整張表。如果你能鎖定一個 partition key(基底表或 GSI),就在那裡計數——這就是把 Query vs Scan 的成本落差套用到計數上。

Select=COUNT vs ItemCount(以及為什麼它會過時)

DescribeTable 免費回傳一個 ItemCount(以及 TableSizeBytes),沒有讀取成本。問題在於 API 參考本身:「DynamoDB 大約每六小時更新一次這個值。最近的變更可能不會反映在這個值中。」所以它可能遠遠落後於你表格的實際狀態。

Select=COUNTDescribeTable.ItemCount
精確度精確(針對符合的集合)近似
即時性即時約每 6 小時更新一次
成本讀取並計費每一個被計數的 item免費(中繼資料)
能否過濾/計數子集可(filter expression)否——只能整張表

ItemCount 用於粗略的「這張表多大」的直覺檢查或儀表板小卡。當你需要精確、過濾過或當下的數字時,就用 Select=COUNT——並接受讀取成本。若要真正即時又免費的東西,請自己追蹤一個計數器(見下方 聚合模式)。

為什麼沒有原生的 SUM/AVG/MIN/MAX

DynamoDB 的讀取操作回傳 item。沒有查詢規劃器能把結果集折疊成一個純量,所以沒有東西能拿來計算 SUMAVG。計數是這個 API 唯一提供的折疊,透過 Select=COUNT

PartiQL 並未改變這一點。PartiQL SELECT 文法SELECT {{expression}} [, …] FROM {{table}}[.{{index}}] [WHERE …] [ORDER BY {{key}} …],其中 expression 是「由 * 萬用字元或一份含一個以上屬性名稱或文件路徑的投影清單所構成的投影」。那份文法裡沒有聚合函式、也沒有 GROUP BY 子句——而 ORDER BY 接受一個 {{key}},文件記載為「用來排序回傳結果的 hash key 或 sort key」。每一條 PartiQL SELECT 仍會編譯成 GetItemQueryScan,所以 SELECT SUM(total) FROM "Orders" 根本無法表達。(更多關於 PartiQL 上限的內容見 PartiQL vs SQL。)

聚合模式(計數器、串流、應用程式端)

既然 DynamoDB 不會替你聚合,既有的模式就把工作推到別處:

  • 維護一個計數器 item。 保留一個專用 item(例如 PK = "STATS#orders"),並在每次寫入時用 UpdateItem 對一個數值屬性 ADD。讀取聚合就成了單一的 GetItem——精確又便宜,但增量邏輯、其一致性,以及單一計數器被猛敲時的爭用,都由你自己承擔。
  • DynamoDB Streams → 聚合器。 啟用串流並接到一個 Lambda,在 item 變更時更新累計總數(計數、總和)。根據 AWS Streams 文件,你可以設定串流的 StreamViewType,讓每一筆記錄帶有 NEW_AND_OLD_IMAGES——「item 的新映像與舊映像兩者」——足以在不重新 scan 的情況下保持 SUM 式聚合的即時性。串流記錄受 24 小時生命週期約束(「shard 內的串流記錄會在 24 小時後自動移除」),所以消費端必須跟上。
  • 應用程式端折疊。 分頁讀取符合的 item,並在你自己的程式碼中累加 SUM/AVG/MIN/MAX。正確,但它每次都會讀取(並計費)每一個 item——成本輪廓與 Select=COUNT 相同,外加資料傳輸。
  • 卸載到分析服務。 對於繁重或臨時的分析型聚合,把表格匯出到 S3 並用 Athena 查詢,或把它串流進資料倉儲。根據 AWS 匯出至 S3 文件,匯出「不消耗讀取容量單位」,並讓你「使用 Athena 等 AWS 服務執行分析與複雜查詢」——這是當你成長到超出每次請求聚合的範圍後,AWS 建議的路徑。

每一種都用簡潔性換取寫入時的記帳(計數器、串流)或讀取時的成本(應用程式端 scan)。沒有任何模式能讓 DynamoDB 自己免費算出 SUM。這個取捨的 grouping 版本——按鍵 聚合而非對整張表聚合——自成一篇指南:DynamoDB GROUP BY

在 DynoTable 的 SQL Workbench 中執行 COUNT/SUM/AVG

當你只是需要答案——「有幾筆 OPEN 訂單,總額是多少」——而不想寫一個分頁的 scan 迴圈或一個 Lambda,DynoTable 的 SQL Workbench 能執行真正的聚合。它透過 DynamoDB 實際的 Query/Scan 執行環境物化你的表格,再於其上執行你完整的 SQL:在 DynamoDB 的存取模式規則之內的 SQL。

-- Runs in the DynoTable Workbench (NOT in PartiQL):
SELECT status,
       COUNT(*)        AS orders,
       SUM(total)      AS revenue,
       AVG(total)      AS avg_order,
       MIN(total)      AS smallest,
       MAX(total)      AS largest
FROM orders
GROUP BY status
ORDER BY revenue DESC

這就是 COUNTSUMAVGMINMAXGROUP BYORDER BY——沒有任何一個是 DynamoDB 或 PartiQL 能表達的——在一條語句裡完成。這跟 SQL for DynamoDB 是同一個分析切入點;完整的 grouping 故事請見 DynamoDB GROUP BY

Workbench 對底層的存取模型很誠實,不是假扮的 Postgres:

  • 各列仍透過 DynamoDB 真實的 Query/Scan 而來。對整張表做 GROUP BY 底層仍是一個 Scan——Workbench 會把那個成本攤開來呈現而非隱藏,這與 Query vs Scan 是同一個取捨。
  • 聚合在各列落地之後,作用於物化的純量屬性。

常見問答

我能不 scan 就計數 DynamoDB 中的 item 嗎? 不完全能。要取得精確、當下的計數,你必須讀取那些 item——Select=COUNT 仍會計量每一個被計數的 item。唯一不必 scan 的選項是近似的 DescribeTable.ItemCount(約每 6 小時更新一次),或你自己在每次寫入時維護的計數器 item。

我該如何按 GSI 計數 item? 對該索引執行 Query(或 Scan)並搭配 Select=COUNT。透過一個窄範圍的 GSI partition 計數,遠比 scan 基底表便宜,因為你只讀取那個索引 partition 內的 item——請圍繞你需要的計數來設計索引。

DescribeTable.ItemCount 準確嗎? 它是近似的。API 參考 指出 DynamoDB 「大約每六小時」更新 ItemCountTableSizeBytes,且「最近的變更可能不會反映在這個值中」。在需要精確或即時數字的場合不要用它。

DynamoDB 能做 SUMAVG 嗎? 原生不能,PartiQL 中也不能——PartiQL SELECT 文法 沒有聚合函式。請在你的應用程式中聚合、維護一個計數器(可選擇透過 DynamoDB Streams),或在 DynoTable 的 SQL Workbench 中執行 SUM/AVG

CountScannedCount 有什麼差別? ScannedCount 是 DynamoDB 在你的 filter 之前評估了多少 item;Count 是套用 filter 之後剩下多少。沒有 filter expression 時兩者相等。兩者落差很大代表這是一個沒有效率的計數。


需要對你的 DynamoDB 資料做 sum、average 或 grouping,卻又不想寫 scan 迴圈嗎?下載 DynoTable,在一個 Workbench 分頁中執行它。想先比較各家用戶端?看看它相對於 純 DynamoDB GUI 的定位。

已更新