如何在 DynamoDB 中做 COUNT、SUM 和聚合
DynamoDB 恰好只有一個內建聚合:用 Select=COUNT 計數匹配的項。沒有原生的 SUM、AVG、MIN 或 MAX。而且即便是你 能 拿到的那個計數,也要讀取(並計費)它計入的每一個項。本指南講清楚哪些是真正受支援的、人們會去夠的那些近似做法,以及當你需要時如何對著一張表執行真正的 COUNT/SUM/AVG。
DynamoDB 能做 SUM、COUNT 和聚合函式嗎?
大體上不能。DynamoDB 唯一的內建聚合是 Select=COUNT,它返回匹配項的計數,但仍然會讀取(並計費)每一個項。沒有原生的 SUM、AVG、MIN 或 MAX,PartiQL 也一個都沒加。要做帶 GROUP BY 的真正聚合,就在你的應用裡摺疊它們、維護一個計數器,或者在 DynoTable 的 Workbench 裡執行 SQL。
Select=COUNT返回匹配項的數量,但 DynamoDB 仍然要讀取每一個項來算出它——你付的是完整的Scan/Query讀取成本,而不是一個廉價的“計數”成本。- 沒有原生的
SUM、AVG、MIN或MAX。 DynamoDB 的讀取操作返回項;它們不會把項摺疊成一個數字。PartiQL 也沒加聚合。 DescribeTable.ItemCount是免費的,但 是近似的,且只“大約每六小時”更新一次——對一個儀表盤的小方塊來說沒問題,對任何需要精確的場景來說都是錯的。- 要精確的
COUNT/SUM/AVG/MIN/MAX(帶GROUP BY),就在你的應用裡聚合、維護一個計數器,或者在 DynoTable 的 SQL Workbench 裡執行它(見下文)。
計數項:Select=COUNT
Query 和 Scan 都接受一個 Select 引數。把它設成 COUNT,響應裡帶的就是計數而不是項:
aws dynamodb scan \
--table-name Orders \
--select COUNT \
--filter-expression "#s = :open" \
--expression-attribute-names '{"#s":"status"}' \
--expression-attribute-values '{":open":{"S":"OPEN"}}'響應給你兩個數字(AWS:計數結果中的項):
Count——“在應用了篩選運算式(如果有的話)之後 剩下的項的數量。”ScannedCount——“在任何ScanFilter被應用 之前 被求值的項的數量。”沒有篩選時,ScannedCount與Count相同。
如果你只有 ,需要計數其中的重複項,你所傳入的那個條件 + 篩選,正是 DynamoDB 運算式構建器 生成的東西——上面那些 FilterExpression 和 ExpressionAttributeNames/Values 對映,加上當你透過 Query 在一個分割區內計數時的 KeyConditionExpression——而無需手工轉義 JSON。
還有兩個坑,會咬到計數大表的人:
- 1 MB 分頁上限仍然適用。 “如果
Scan結果集的大小大於 1 MB,ScannedCount和Count只代表全部項的部分計數”(AWS Scan 文件)。你必須分頁——把每次響應的LastEvaluatedKey作為下一次請求的ExclusiveStartKey回喂進去,一路保留一個累加值來得到真實的數字——這就是 DynamoDB 分頁 裡講的同一個迴圈。 - 一次窄的
Query勝過一次Scan。 在一次Query上用Select=COUNT只計量目標分割區裡的項,而不是整張表。如果你能鎖定一個分割區索引鍵(基表或一個 GSI),就在那裡計數——這是把 Query 對比 Scan 的成本差應用到了計數上。
Select=COUNT 對比 ItemCount(以及為什麼它是陳舊的)
DescribeTable 免費返回一個 ItemCount(和 TableSizeBytes),沒有讀取成本。問題在於 API 參考本身:“DynamoDB 大約每六小時更新這個值。近期的改動可能不會反映在這個值裡。”所以它可能遠遠落後於你表的實際狀態。
Select=COUNT | DescribeTable.ItemCount | |
|---|---|---|
| 精確性 | 精確(對匹配集而言) | 近似 |
| 新鮮度 | 實時 | 約每 6 小時更新 |
| 成本 | 讀取並計費每一個被計數的項 | 免費(後設資料) |
| 能否篩選 / 計數子集 | 能(篩選運算式) | 不能——只能整張表 |
用 ItemCount 來做一次粗略的“這張表有多大”的直覺判斷,或者一個儀表盤小方塊。當你需要一個精確的、篩選過的或當前的數字時用 Select=COUNT——並接受讀取成本。至於任何真正實時且免費的東西,就自己跟蹤一個計數器(見下面的 聚合模式)。
為什麼沒有原生的 SUM/AVG/MIN/MAX
DynamoDB 的讀取操作返回項。沒有查詢規劃器把一個結果集摺疊成一個標量,所以沒有東西可以用來算出一個 SUM 或 AVG。計數是這個 API 提供的唯一折疊,透過 Select=COUNT。
PartiQL 並不改變這一點。PartiQL SELECT 語法 是 SELECT @@P0@@ [, …] FROM @@P1@@[.@@P2@@] [WHERE …] [ORDER BY @@P3@@ …],其中 expression 是“由 * 萬用字元或一個由一個或多個屬性名或文件路徑構成的投影列表形成的一個投影。”那個語法裡沒有聚合函式、沒有 GROUP BY 子句——而 ORDER BY 取的是一個 @@P4@@,文件裡說是“用來給返回結果排序的一個雜湊鍵或一個排序索引鍵。”每一個 PartiQL SELECT 仍然編譯成一個 GetItem、Query 或 Scan,所以 SELECT SUM(total) FROM "Orders" 根本就無法表達。(關於 PartiQL 天花板的更多內容見 PartiQL 對比 SQL。)
聚合模式(計數器、Streams、應用端)
既然 DynamoDB 不會替你聚合,那些成熟的模式就把這份活兒推到別處:
- 維護一個計數器項。 保留一個專門的項(例如
PK = "STATS#orders"),並在每次寫入時用一個UpdateItem對一個數值屬性ADD。讀取這個聚合就變成一次GetItem——精確且廉價,但遞增邏輯、它的一致性,以及某個計數器被猛擊時的爭用,都歸你負責。 - 餵給一個聚合器。 啟用一個流,把它接到一個 Lambda,在項變化時更新累計合計(計數、求和)。按照 AWS Streams 文件,你可以配置流的
StreamViewType,讓每條記錄攜帶NEW_AND_OLD_IMAGES——“項的新舊兩個映像”——足以在不重新掃描的情況下讓SUM式的聚合保持最新。流記錄有 24 小時的生存期(“分片內的流記錄在 24 小時後自動移除”),所以消費者必須跟得上。 - 應用端摺疊。 分頁遍歷匹配的項,在你自己的程式碼裡累積
SUM/AVG/MIN/MAX。正確,但它每次都讀取(並計費)每一個項——與Select=COUNT相同的成本輪廓,外加資料傳輸。 - 解除安裝到分析。 對於繁重或臨時的分析型聚合,把表匯出到 S3,用 Athena 查詢它,或者把它流入一個資料倉儲。按照 AWS 匯出到 S3 文件,匯出“不消耗讀容量單位”,並讓你“使用像 Athena 這樣的 AWS 服務執行分析和複雜查詢”——一旦你超出了按請求聚合的規模,這就是 AWS 推薦的路徑。
每一種都用簡單性去換取要麼寫入時的記賬(計數器、流),要麼讀取時的成本(應用端掃描)。沒有哪種模式能讓 DynamoDB 本身免費算出一個 SUM。這個權衡的分組版本——按鍵 聚合而非對整張表聚合——是它自己的一篇指南:DynamoDB GROUP BY。
在 DynoTable 的 SQL Workbench 裡執行 COUNT/SUM/AVG
當你只需要那個答案——“有多少個 OPEN 訂單,它們的合計是多少”——而不想寫一個分頁掃描迴圈或一個 Lambda 時,DynoTable 的 SQL Workbench 會執行真正的聚合。它透過 DynamoDB 實際的 Query/Scan 執行時把你的表物化出來,然後在其之上執行單條 SELECT——聚合、GROUP BY、HAVING、DISTINCT:SQL,在 DynamoDB 的訪問模式規則之內。
-- 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這就是 COUNT、SUM、AVG、MIN、MAX、GROUP BY 和對一個計算聚合的 ORDER BY——其中沒有一樣是 DynamoDB 或 PartiQL 能表達的(PartiQL 的 ORDER BY 僅限於鍵屬性)——就在一條語句裡。這與 DynamoDB 的 SQL 是同一個分析型楔子;關於完整的分組故事,參見 DynamoDB GROUP BY。
Workbench 對底下的訪問模型是誠實的,不是一個假裝的 Postgres:
- 那些行仍然透過 DynamoDB 真正的 Query/Scan 而來。對一整張表的
GROUP BY底下仍然是一次Scan——Workbench 把那份成本擺到檯面上,而不是把它藏起來,這與 Query 對比 Scan 是同一個權衡。 - 聚合是在行落地之後,對物化出來的標量屬性執行的。
常見問題
我能不掃描就在 DynamoDB 裡計數項嗎?
不完全能。要一個精確的、當前的計數,你必須讀取那些項——Select=COUNT 仍然計量每一個被計數的項。唯一的不掃描選項是近似的 DescribeTable.ItemCount(約每 6 小時更新一次),或者一個你自己在每次寫入時維護的計數器項。
我怎麼按一個 GSI 計數項?
對著索引用 Select=COUNT 執行 Query(或 Scan)。透過一個窄的 GSI 分割區來計數,比掃描基表便宜得多,因為你唯讀取那個索引分割區裡的項——圍繞你需要的計數來建模這個索引。
DescribeTable.ItemCount 準確嗎?
它是近似的。API 參考 說明 DynamoDB“大約每六小時”更新 ItemCount 和 TableSizeBytes,而且“近期的改動可能不會反映在這個值裡。”別在需要精確或實時數字的地方用它。
DynamoDB 能做 SUM 或 AVG 嗎?
原生不能,PartiQL 裡也不能——PartiQL SELECT 語法 沒有聚合函式。在你的應用裡聚合、維護一個計數器(可選地透過 DynamoDB Streams),或者在 DynoTable 的 SQL Workbench 裡執行 SUM/AVG。
Count 和 ScannedCount 有什麼區別?
ScannedCount 是 DynamoDB 在你的篩選之前求值了多少個項;Count 是篩選之後還剩多少個。當沒有篩選運算式時它們相等。兩者之間的大差距意味著一次低效的計數。
需要對你的 DynamoDB 資料求和、求平均或分組,又不想寫一個掃描迴圈?下載 DynoTable,在一個 Workbench 標籤頁裡執行它。想先比較各家用戶端?看看它在 普通的 DynamoDB GUI 之中的位置。