入門閱讀時間 3 分鐘

為什麼 DynamoDB Scan 又慢又貴

一次 Scan 會讀取表中的每一個項,之後才做過濾。它是 你出於 SQL 肌肉記憶而伸手去用的操作,也是那個悄悄 推高你賬單、同時把延遲拖得比你逃離的 RDS 機器還差的操作。

為什麼我的 DynamoDB Scan 又慢又貴?

一次 Scan 會在 FilterExpression 執行之前讀取表中的每一個項,所以 無論最終返回多少行,你都要為讀取整張表付費,而且它會 隨著表的增長而變慢。修復之道幾乎總是一次按鍵的 Query —— 圍繞某個鍵 對訪問模式建模,讓 DynamoDB 只觸及一個分割區,而不是 全部。

  • Scan 每次都讀取整張表。 決定你付多少費、耗多長時間的是表的 大小,而不是你的結果條數。
  • FilterExpression 是關於成本的一個謊言。 它在讀取被計量_之後_ 才執行,所以返回 12 個項也可能按讀取 1200 萬個來計費。
  • Scan 會隨著你的增長而變慢。 而一次按鍵的 Query 保持平穩 —— 無論表變得 多大,它都只觸及一個分割區。
  • 修復之道幾乎總是建模,而不是調優。 如果你靠 Scan 來回答一個 日常問題,那說明你少了一個鍵。

Scan 實際上做了什麼

從 SQL 過來,SELECT * FROM events WHERE type = 'checkout' 感覺是免費的 —— 引擎要麼有索引,要麼沒有,但無論哪種情況你都能拿回行。在 DynamoDB 裡,沒有一個查詢規劃器替你做那個決定。

一次 Scan 會順序走遍整張表,每次 1 MB,把每 一頁交給你的 FilterExpression。凡是被過濾器拒絕的都仍然被讀取、 仍然被計量、仍然算在你的賬單上。(AWS:掃描表

這就是陷阱所在。過濾器看起來像一個 WHERE 子句,但它改變的是 結果集,絕不改變成本。無論 是否存在過濾器,Scan 都消耗相同的讀取容量。(AWS:掃描表

數一數讀取單元

DynamoDB 以(RCU)來計量讀取。一個 RCU 可以買到對 一個最大 4 KB 的項做一次讀取;讀取的 成本是它的一半。更大的項向上取整到下一個 4 KB。(AWS:讀/寫 容量模式

設想一張分析表 ProductEvents。每一行是一個被追蹤的事件:

PK  = "TENANT#acme"
SK  = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytes

假設它存了 2,000,000 個事件,每個約 1 KB,全都歸在一個繁忙的租戶下。你 想要今天的結賬事件。條件反射式的做法:

Scan ProductEvents
FilterExpression: eventType = "checkout"

那個過濾器也許返回 40 行。但 Scan 先讀取了全部 2,000,000 個 項。按每個約 1 KB 計(每 4 KB 1 RCU,最終一致約為每 4 KB 0.5 RCU), 你計量了大約 250,000 RCU —— 並翻遍了約 2 GB 的資料 —— 才 交回 40 個項。

現在把訪問模式建模成一個鍵,改用 Query

Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"

這隻讀取一個分割區中匹配的那一小片。如果那 40 行結賬事件 加上當天的其他事件合計約 2 MB,你為約 2 MB 的讀取付費,而不是 2 GB。同樣的答案,成本卻只是一個零頭 —— 而且延遲會 隨著表的增長保持平穩。

Scan 與 Query,計量對比

Scan + 過濾器按鍵 Query
讀取表中的每一個一個分割區,由 SK 收窄
計費容量整張表,在過濾之前只有你那一小片裡的項
我們的示例約 250,000 RCU(約 2 GB)幾百個 RCU(約 2 MB)
延遲隨表大小增長隨表增長保持平穩
結果條數對成本毫無影響與你的付費相匹配

這張表編碼的教訓是:在 Scan 上,你的結果條數和你的賬單 毫無關係。而在 Query 上,二者彼此對應。

在 Scan 之前先做判斷

大多數意外的 Scan 都源自一個問題:我能說出我需要的那個分割區嗎? 如果能,那就是一次 Query。如果不能,修復之道是加一個鍵,而不是加一個更大的過濾器。

下面把這個判斷畫成流程圖。

需要讀取項知道分割區索引鍵嗎?Query —— 單一分割區能用 GSI 給它做鍵嗎?加一個 GSI,然後 QueryScan —— 最後手段

這條路徑幾乎總是終結於 Query;只有當沒有鍵 —— 無論是現成的 還是可新增的 —— 適配該訪問模式時,你才會跌落到 Scan

如果這個模式是真實且反覆出現的,但基表無法為它建鍵,那就是 加一個全域次要索引的訊號,好讓這個問題 變成一次 Query。事先圍繞你的訪問模式來建模你的鍵, 才是整場博弈的全部 —— 參見單表設計

寫出按鍵的查詢,而不是過濾器

當你確實需要一個鍵以外的條件時,請刻意地把它構建出來,而不是 把一切都傾倒進一個 FilterExpressionDynamoDB 運算式構建器會為你生成 KeyConditionExpression 和屬性預留位置,讓分割區索引鍵 和排序索引鍵來做收窄 —— 在 DynamoDB 計量讀取_之前_,而不是之後。

KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)

Scan 什麼時候其實沒問題

Scan 並非被禁止 —— 它只是錯誤的預設選項。當你真心是想 "讀取全部"時,它才是對的工具:

  • 手動執行的一次性匯出或回填。
  • 微小的配置/查詢表,整張表也就幾 KB。
  • 有意翻遍整張表的後臺作業。用 Segment / TotalSegments 把它們 拆分到多個 worker —— 一次 —— 而不是 一次漫長的順序爬取。(AWS:掃描表

另外請注意 PartiQL 救不了你:SELECT * FROM ProductEvents WHERE eventType = 'checkout' 若沒有鍵謂詞,就會直接編譯成一次 Scan。 這是同一個陷阱,只是穿了 SQL 的外衣。(完整的拆解參見 Query 與 Scan 對比。)

當你真的需要跨項分析 —— 一次 GROUP BY、一次 JOIN、一個 DynamoDB 無法表達的聚合 —— DynoTable 的 SQL Workbench 會在有界的結果集 上於用戶端執行它們,而不是用一次全表 Scan 去猛捶那張表。

後續步驟

定價計算器估算這兩種模式各自的 成本,閱讀 Query 與 Scan 對比瞭解 API 層面的 差異,然後下載 DynoTable,針對你自己的表執行這些操作,看清 每種做法實際讀取了多少個項。

已更新