Warum ein DynamoDB-Scan langsam und teuer ist
Ein Scan liest jedes Item in der Tabelle und filtert erst danach. Es ist
die Operation, nach der du aus SQL-Muskelgedächtnis greifst — und die, die still
deine Rechnung hochtreibt, während deine Latenz schlechter wird als die RDS-Box,
die du verlassen hast.
Warum ist mein DynamoDB-Scan langsam und teuer?
Ein Scan liest jedes Item in der Tabelle, bevor die FilterExpression läuft,
also zahlst du, die ganze Tabelle zu lesen, egal wie wenige Rows zurückkommen —
und er wird langsamer, wenn die Tabelle wächst. Der Fix ist fast immer eine keyed
Query: modelliere das Access Pattern um einen Key, sodass DynamoDB eine
Partition berührt statt alles.
- Ein
Scanliest die ganze Tabelle, jedes Mal. Größe, nicht deine Result-Count, entscheidet, was du zahlst und wie lange es dauert. - Die
FilterExpressionlügt über Kosten. Sie läuft nachdem der Read metered ist, also kann das Zurückgeben von 12 Items das Lesen von 12 Millionen abrechnen. - Ein
Scanwird langsamer, wenn du wächst. Eine keyedQuerybleibt flach — sie berührt eine Partition, egal wie groß die Tabelle wird. - Der Fix ist fast immer Modeling, nicht Tuning. Wenn du
Scanst, um eine Routinefrage zu beantworten, fehlt dir ein Key.
Was ein Scan wirklich tut
Aus SQL fühlt sich SELECT * FROM events WHERE type = 'checkout' gratis an —
die Engine hat einen Index oder nicht, aber so oder so bekommst du Rows zurück.
In DynamoDB entscheidet kein Query Planner das für dich.
Ein Scan läuft die gesamte Tabelle sequenziell ab, 1 MB pro Seite, und reicht
jede Seite an deine FilterExpression. Was der Filter verwirft, ist trotzdem
gelesen, trotzdem metered und trotzdem auf deiner Rechnung.
(AWS: Scanning tables)
Das ist die Falle. Der Filter sieht aus wie eine WHERE-Klausel, ändert aber das
Result Set, nie die Kosten. Ein Scan verbraucht dieselbe Read Capacity, ob ein
Filter da ist oder nicht. (AWS: Scanning tables)
Die Read Units zählen
DynamoDB metert Reads in (RCUs). Eine RCU kauft einen einzelnen Read eines Items bis 4 KB; Reads kosten die Hälfte. Größere Items runden auf die nächsten 4 KB auf. (AWS: Read/write capacity mode)
Nimm eine Analytics-Tabelle ProductEvents. Jede Row ist ein getracktes Event:
PK = "TENANT#acme"
SK = "TS#2026-06-23T14:08:55Z#evt_9f3a"
attrs: eventType, sessionId, userId, payloadBytesAngenommen, sie hält 2.000.000 Events à ~1 KB, alle unter einem busy Tenant. Du willst die Checkouts von heute. Der reflexhafte Move:
Scan ProductEvents
FilterExpression: eventType = "checkout"
Dieser Filter liefert vielleicht 40 Rows. Aber der Scan hat zuerst alle
2.000.000 Items gelesen. Bei ~1 KB je (1 RCU pro 4 KB, eventually consistent ≈
0,5 RCU pro 4 KB) hast du grob 250.000 RCUs metered — und ~2 GB Daten
gepaged — um 40 Items zurückzugeben.
Jetzt modelliere das Access Pattern als Key und Query es:
Query ProductEvents
PK = "TENANT#acme"
AND SK begins_with "TS#2026-06-23"
Das liest nur den gematchten Slice einer Partition. Wenn diese 40 Checkout-Rows plus die anderen Events des Tages ~2 MB ergeben, zahlst du für ~2 MB Reads, nicht 2 GB. Dieselbe Antwort, ein winziger Bruchteil der Kosten — und die Latenz bleibt flach, wenn die Tabelle wächst.
Scan vs Query, metered
| Scan + Filter | Keyed Query | |
|---|---|---|
| Reads | Jedes Item in der Tabelle | Eine Partition, per SK verengt |
| Abgerechnete Capacity | Ganze Tabelle, vor dem Filter | Nur die Items in deinem Slice |
| Unser Beispiel | ~250.000 RCUs (~2 GB) | ein paar hundert RCUs (~2 MB) |
| Latenz | Wächst mit Tabellengröße | Flach, wenn die Tabelle wächst |
| Result count | Entscheidet nichts über Kosten | Passt zu dem, wofür du zahlst |
Bei einem Scan sind Result count und Rechnung unverbunden. Bei einer Query
tracken sie einander.
Entscheide, bevor du scannst
Die meisten versehentlichen Scans kommen von einer Frage: kann ich die
Partition nennen, die ich brauche? Wenn ja, ist es eine Query. Wenn nein, ist
der Fix ein Key, kein größerer Filter.
Hier die Entscheidung als Flow.
Der Pfad endet fast immer bei Query; du fällst nur zu Scan durch, wenn kein
Key — vorhanden oder hinzufügbar — zum Access Pattern passt.
Wenn das Pattern real und wiederkehrend ist, die Base Table es aber nicht keyen
kann, ist das das Signal, einen
Global Secondary Index hinzuzufügen, damit die Frage
eine Query wird. Keys um Access Patterns zu modellieren ist das ganze Spiel —
siehe Single-Table Design.
Schreib die keyed Query, nicht einen Filter
Wenn du eine Condition jenseits des Keys brauchst, baue sie bewusst, statt
alles in eine FilterExpression zu kippen. Der
DynamoDB Expression Builder generiert
KeyConditionExpression und Attribut-Placeholder für dich, sodass Partition und
Sort Key die Verengung machen — bevor DynamoDB den Read metert, nicht danach.
KeyConditionExpression: PK = :tenant AND begins_with(SK, :day)
Wann ein Scan wirklich fine ist
Ein Scan ist der falsche Default für Routine-Queries. Er ist das richtige Tool,
wenn du wirklich „alles lesen“ meinst:
- One-off Exports oder Backfills von Hand.
- Winzige Config-/Lookup-Tabellen, wo die ganze Tabelle ein paar KB ist.
- Background Jobs, die die volle Tabelle absichtlich pagen. Splittet die über
Worker mit
Segment/TotalSegments— ein — statt eines langen sequenziellen Crawls. (AWS: Scanning tables)
Und beachte: PartiQL rettet dich nicht. SELECT * FROM ProductEvents WHERE eventType = 'checkout' ohne Key-Prädikat kompiliert geradewegs zu einem Scan.
Derselbe Fallstrick in SQL-Kleidung. (Siehe
Query vs Scan für den vollen Breakdown.)
Wenn du wirklich Cross-Item-Analytics brauchst — ein GROUP BY, ein JOIN, ein
Aggregat, das DynamoDB nicht ausdrücken kann — führt DynoTables SQL Workbench sie
clientseitig über ein begrenztes Resultset aus, statt die Tabelle mit einem Full
Scan zu hämmern.
Nächste Schritte
Schätze, was jedes Pattern kostet, mit dem Pricing Calculator, lies Query vs Scan für den API-Level-Kontrast, und lade DynoTable herunter, um das gegen deine eigenen Tabellen zu fahren und zu sehen, wie viele Items jeder Ansatz wirklich liest.