入門閱讀時間 4 分鐘

為什麼 DynamoDB Scan 又慢又貴

一個 Scan表中每個 item,並只在之後才過濾。它是 你出於 SQL 肌肉記憶伸手去拿的操作,也是那個悄悄 推高你帳單、同時讓你的延遲比你離開的 RDS 機器更糟的操作。

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

ScanFilterExpression 執行之前就讀取表中的每個 item,因此不論最終回傳多少筆資料,你都需要為讀取整張表付費,而且隨著表的增長速度也會越來越慢。解決方法幾乎總是建模——圍繞一個 key 來設計存取模式,讓 DynamoDB 只觸碰一個 partition,而非整張表。

  • 一個 Scan 每次都讀整張表。 是大小、而非你的結果筆數, 決定你付什麼、花多久。
  • FilterExpression 是一個關於成本的謊言。 它在讀取被 計量 之後 才執行,所以回傳 12 個 item 可能就讀取 1200 萬筆計費。
  • 一個 Scan 會隨你成長變慢。 一個 keyed Query 保持平坦 — 不論 表變多大,它只觸碰一個 partition。
  • 修正幾乎總是建模,而非調校。 如果你用 Scan 回答一個 例行問題,你就是漏了一個 key。

一個 Scan 究竟做什麼

從 SQL 過來,SELECT * FROM events WHERE type = 'checkout' 感覺免費 — 引擎有 index、或它沒有,但不論哪種你都拿到列回來。在 DynamoDB 中沒有查詢規劃器替你決定那件事。

一個 Scan 循序走過整張表,一次 1 MB,並把每一頁交給 你的 FilterExpression。不論過濾拒絕什麼,都仍被讀取、 仍被計量、仍在你的帳單上。(AWS:Scanning tables

那就是陷阱。過濾看起來像一個 WHERE 子句,但它改變 結果集,從不改變成本。不論有沒有過濾,一個 Scan 都消耗相同的 讀取容量。(AWS:Scanning tables

數一數讀取單位

DynamoDB 以 read capacity unit(RCU) 計量讀取。一個 RCU 買一次 對最多 4 KB 的 item 的強一致讀取;最終一致讀取 花一半。較大的 item 向上取整到下一個 4 KB。(AWS:Read/write capacity mode

拿一張分析表,ProductEvents。每一列是一個被追蹤的事件:

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

假設它存有 2,000,000 個事件,每個約 1 KB,全在一個忙碌的租戶下。你 想要今天的 checkout。那個反射性的動作:

Scan ProductEvents
FilterExpression: eventType = "checkout"

那個過濾可能回傳 40 列。但 Scan 先讀了全部 2,000,000 個 item。每個約 1 KB(每 4 KB 1 RCU,最終一致 ≈ 每 4 KB 0.5 RCU), 你計量了大約 250,000 RCU — 並翻過約 500 MB 的資料 — 才 交回 40 個 item。

現在把這個存取模式建模成一個 key 並改成 Query 它:

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

這只讀一個 partition 中被匹配的那一片。如果那 40 個 checkout 列 加上當天其他事件來到約 2 MB,你為約 2 MB 的讀取付費,而非 500 MB。相同的答案、極小一部分的成本 — 而且延遲隨表成長 保持平坦。

Scan vs Query,計量過

Scan + 過濾Keyed Query
讀取表中 每個 item一個 partition,由 SK 縮窄
計費容量整張表,在過濾之前只有你那一片裡的 item
我們的範例約 250,000 RCU(約 500 MB)數百 RCU(約 2 MB)
延遲隨表大小成長隨表成長保持平坦
結果筆數對成本毫無決定與你付的相符

這張表編碼的教訓:在一個 Scan 上,你的結果筆數與你的帳單 無關。在一個 Query 上,它們彼此相隨。

在 Scan 之前先決定

大多數意外的 Scan 來自一個問題:我能指名我需要的 partition 嗎? 如果能,它就是一個 Query。如果不能,修正是一個 key, 而非一個更大的過濾。

這裡是流程形式的決策。

需要讀取 item知道 partition key?Query 一個 partitionGSI 能為它建 key 嗎?加一個 GSI,然後 QueryScan 最後手段

那條路徑幾乎總是終於 Query;你只在沒有 key — 現有或可加的 — 適合那個存取模式時,才落到 Scan

如果那個模式是真實且反覆的,但 base table 無法為它建 key,那就是 加一個 Global Secondary Index 的訊號,讓那個問題 變成一個 Query。預先把你的 key 圍繞你的存取模式建模,就是 整場遊戲 — 請見 single-table design

寫那個 keyed query,而非一個過濾

當你確實需要一個 key 之外的條件時,刻意地建立它,而非 把一切都倒進一個 FilterExpressionDynamoDB Expression Builder 替你產生 KeyConditionExpression 與屬性佔位符,這樣 partition 與 sort key 就做那個縮窄 — 在 DynamoDB 計量讀取 之前,而非之後。

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

何時一個 Scan 其實沒問題

一個 Scan 不是被禁止的 — 它只是錯的預設。當你真的意指 「讀一切」時,它是對的工具:

  • 一次性匯出 或手動跑的回填。
  • 小型設定/查詢表,整張表只有幾 KB。
  • 背景工作,刻意翻過整張表。用 Segment / TotalSegments 把那些拆過多個 worker — 一個 parallel scan — 而非 一次漫長的循序爬行。(AWS:Scanning tables

並且注意 PartiQL 救不了你:SELECT * FROM ProductEvents WHERE eventType = 'checkout' 沒有 key 述詞,會直接編譯成一個 Scan。 它是穿著 SQL 外衣的同一個地雷。(完整拆解請見 Query vs Scan。)

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

下一步

capacity 計算機 估算任一模式的 成本、讀 Query vs Scan 看 API 層級的對照,並 下載 DynoTable 對你自己的表執行這些,並親眼 看著消耗的容量。

已更新