為什麼 DynamoDB Scan 又慢又貴
一個 Scan 讀 表中每個 item,並只在之後才過濾。它是
你出於 SQL 肌肉記憶伸手去拿的操作,也是那個悄悄
推高你帳單、同時讓你的延遲比你離開的 RDS 機器更糟的操作。
為什麼我的 DynamoDB Scan 又慢又貴?
Scan 在 FilterExpression 執行之前就讀取表中的每個 item,因此不論最終回傳多少筆資料,你都需要為讀取整張表付費,而且隨著表的增長速度也會越來越慢。解決方法幾乎總是建模——圍繞一個 key 來設計存取模式,讓 DynamoDB 只觸碰一個 partition,而非整張表。
- 一個
Scan每次都讀整張表。 是大小、而非你的結果筆數, 決定你付什麼、花多久。 FilterExpression是一個關於成本的謊言。 它在讀取被 計量 之後 才執行,所以回傳 12 個 item 可能就讀取 1200 萬筆計費。- 一個
Scan會隨你成長變慢。 一個 keyedQuery保持平坦 — 不論 表變多大,它只觸碰一個 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,
而非一個更大的過濾。
這裡是流程形式的決策。
那條路徑幾乎總是終於 Query;你只在沒有 key — 現有或可加的 —
適合那個存取模式時,才落到 Scan。
如果那個模式是真實且反覆的,但 base table 無法為它建 key,那就是
加一個 Global Secondary Index 的訊號,讓那個問題
變成一個 Query。預先把你的 key 圍繞你的存取模式建模,就是
整場遊戲 — 請見 single-table design。
寫那個 keyed query,而非一個過濾
當你確實需要一個 key 之外的條件時,刻意地建立它,而非
把一切都倒進一個 FilterExpression。
DynamoDB 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 對你自己的表執行這些,並親眼 看著消耗的容量。