Beginner7 min read

DynamoDB Query vs Scan: Which to Use, and Why (w/ Examples)

Query reads a single by partition key (optionally narrowing the sort key); Scan reads the whole table and filters afterwards. They look similar in the API but they bill — and scale — completely differently.

When should I use Query vs Scan in DynamoDB?

Use Query whenever you can name the partition you need — it reads one and bills only for matched items. Reach for Scan only for one-off exports or tiny tables; it reads every item and bills the whole table before any FilterExpression runs. On real data, Query wins.

  • Query is targeted: you pay for the items in the matched partition.
  • Scan is exhaustive: you pay to read every item, then throw most away with a FilterExpression that runs after the read is metered.

On a table of any real size, a Scan with a filter is the classic "why is my bill huge and my latency worse than RDS" footgun.

Side by side

QueryScan
ReadsOne partition (by PK)Every item in the table
Billed capacityItems matched in the partitionWhole table, before filtering
FilterExpressionApplied after the read — still billed for the readSame — filtering never cuts cost
LatencyFlat as the table growsGrows with table size
Pagination1 MB/page → LastEvaluatedKey1 MB/page; parallelisable
Use it forKnown access patternsOne-off exports, tiny config tables

A FilterExpression runs after DynamoDB meters the read, on both operations. A Scan that "returns 10 rows" can bill for reading a million — filtering is a convenience, never a cost control.

What a full Scan actually costs

Put numbers on it. DynamoDB meters reads in 4 KB units: a read costs one per 4 KB, an read half that. Query and Scan sum the size of every item they touch — not every item they return — and round up to the next 4 KB.

Take a 1-million-item table averaging 2 KB per item (~2 GB of data), and an access pattern that needs 10 of those items:

Items readData meteredRead units (eventually consistent)
Scan + FilterExpression1,000,000~2 GB~262,000
Query on a matching key1020 KB3

Same 10 items, five orders of magnitude apart — and the Scan bills that ~262,000 every time it runs, whether the filter matches ten items or none. On billing those are request units straight onto the bill; on tables a big Scan competes with production traffic for throughput and can throttle it into ProvisionedThroughputExceededException.

Three more cost facts that surprise people:

  • Select: COUNT is not free. A counting Query or Scan consumes exactly the same read capacity as reading the items — it just doesn't return them.
  • Limit caps items evaluated, not items matched. Combined with a filter, a page can come back empty while still billing a full page of reads.
  • You never have to guess. Pass ReturnConsumedCapacity: TOTAL and every response reports the capacity it just consumed.
On-Demand vs Provisioned cost
100 /s
100 /s
1 KB
50 GB

On-Demand

$209.60/ month

Provisioned

Cheaper
$69.44/ month

Prices: US East (N. Virginia), strongly consistent reads, no Free Tier. Estimate only — excludes backups & transfer. Provisioned needs 100 RCU / 100 WCU.

Check what your own items weigh with the item size calculator, then turn read units into a monthly bill with the pricing calculator.

Use Query

Query  PK = "USER#42"  AND  SK begins_with "ORDER#"

If you find yourself reaching for Scan to answer a common access pattern, that is a modelling signal: add a Global Secondary Index so the pattern becomes a Query.

The choice comes down to one question — can you name the partition you need?

YesNoYesNoAccess patternPartition key known?Query reads one partitionCan a GSI key it?Add a GSIScan reads the whole table

If the key is known you Query; if not, add a GSI to make it one, and fall back to Scan only when no key fits.

When Scan is fine

One-off exports, tiny config tables, and background jobs that page through the whole table deliberately. Use Segment/TotalSegments to split a Scan across workers (a — see parallel Scans in DynamoDB) when you genuinely must read everything, and page it properly with LastEvaluatedKey (pagination guide). If a Scan you already run is the problem, why Scan is slow and expensive walks the triage.

A reflexive SELECT * FROM table over DynamoDB is the same anti-pattern in PartiQL clothing — it compiles to a Scan. When you really do need cross-item analytics (a GROUP BY, a JOIN, an aggregate), DynoTable's SQL Workbench runs them client-side over a bounded result set instead of hammering the table.

Try DynoTable to run and inspect these queries against your own tables — it shows the consumed capacity of every operation it runs.

From Scan reflex to modeled Query

Teams coming from SQL often reach for Scan first. The remediation path is almost always structural:

  1. Name the partition — what id scopes the read (USER#42, GAME#7)?
  2. Add a GSI if the partition key differs from the base table — see GSI vs LSI.
  3. Emit the Query — the query builder composes index, key condition, projection, filter, limit, and pagination in one runnable program.

Sketch overloaded keys in the single-table design tool when several entity types share a table; the planner shows which patterns become partition queries versus which ones still require a scan.

Strongly consistent Query on the base table

Query against the base table or an LSI accepts ConsistentRead=true when you need read-after-write freshness. GSIs remain eventual-only — a pattern that Scan-s substitutes with a filtered GSI still cannot strong-read the index. See consistency modes for the 2× RCU trade-off.

PartiQL SELECT is still a Scan without keys

SELECT * FROM "Orders" without a partition-key predicate compiles to a table Scan — same cost as the API-level Scan. Adding WHERE pk = 'U#1' turns it into a Query when pk is the table's partition key. The FAQ does DynamoDB support SQL? quotes the parser errors when you ask for SQL features DynamoDB does not implement.

Observability during refactors

Turn on ReturnConsumedCapacity: TOTAL while replacing Scan calls. Log ConsumedCapacity.CapacityUnits per endpoint before and after you add an index — the drop from table-sized reads to partition-sized reads is the proof your modeling change worked.

DynoTable surfaces consumed capacity on each operation in the UI, which makes side-by-side comparisons against a staging table quick during refactors.

When Scan remains legitimate

ScenarioWhy Scan is OKMitigation
Nightly export of entire tableFull read is the goalParallel segments + off-peak window
Config table under 1 MBCost is negligibleStill prefer GetItem by known key if possible
One-time migration auditRare, boundedPage with LastEvaluatedKey, throttle WCU
Analytics not modeled in keysHonest full readDynoTable SQL Workbench over bounded exports

For recurring analytics, model a summary item or stream-derived aggregate instead of paying table-sized reads on a schedule.

Updated