DynamoDB Query vs Scan in AWS: Examples, FilterExpression, Pagination

DynamoDB has two operations for retrieving multiple items: Query, which reads a specific partition using the partition key, and Scan, which reads every item in the table. Understanding the difference, and when each applies, is foundational to building performant DynamoDB applications.

DynamoDB is not a SQL database. It does not support arbitrary WHERE clauses or joins. The access patterns you define at schema-design time determine which attributes become partition keys, sort keys, and indexes. Those choices are what make it possible to Query efficiently instead of Scanning. If you are new to DynamoDB’s data model, read DynamoDB Data Model: Tables, Items, and Keys first. This page builds on that foundation.

By the end of this page you will know when to use Query versus Scan, how KeyConditionExpression and FilterExpression differ, how pagination works and where it can silently break application code, and when a Global Secondary Index (GSI) is the right solution rather than a filter.

Simple explanation

The filing cabinet analogy

Think of a DynamoDB table like a filing cabinet where every drawer is labelled with a partition key value. All items with the same partition key go in the same drawer, sorted inside by the sort key.

A Query is like opening one specific drawer and retrieving the files inside. You can narrow the search to a range of files within that drawer using the sort key, but you only ever open one drawer.

A Scan is like opening every drawer in the cabinet, reading every file, and then discarding anything that does not match. It always reads everything, even the drawers you do not need.

This distinction matters because DynamoDB charges you for what it reads, not what it returns. A Query that matches 10 items reads 10 items. A Scan that finds 10 items out of one million reads one million items, and charges for all of them.

If you are coming from SQL: DynamoDB’s Query is similar to SELECT ... WHERE partition_key = x, but the schema design (not the query) determines what that condition can efficiently express. If your current schema cannot answer a query with a Query operation, the solution is almost always to add a GSI, not to use a Scan.

When to use Query

Good fits for Query
  • Fetching all orders for one user. The table is keyed on userId (partition key) and orderId (sort key). A Query on userId = “user-123” returns all orders for that user without reading anyone else’s data.
  • Loading recent events for one entity. A table of audit logs keyed on resourceId (partition key) and timestamp (sort key). Query with a BETWEEN condition on the sort key returns logs for a specific resource within a time range.
  • Querying an alternate access pattern via a GSI. Your table is keyed on userId, but you need to find all items with status = “PENDING”. A GSI with status as the partition key allows a Query against that index instead of a Scan against the whole table.

Query is the wrong choice when you genuinely do not know the partition key value at query time. In that case, ask whether a GSI can provide the missing access pattern, or whether the table’s key structure needs rethinking entirely. See Choosing the Right AWS Storage Service if you are still deciding whether DynamoDB fits your use case.

How DynamoDB Query works

Every Query must specify the partition key value using KeyConditionExpression. DynamoDB routes the request directly to the partition that stores items with that key. It never reads other partitions.

The sort key is optional. If omitted, all items in the partition are returned. If included, DynamoDB narrows the result to items matching the sort key condition within that partition. Sort key conditions support equality, ranges, and prefix matching. See the Sort key condition operators section below.

Query can target three things:

  • The base table directly: using the table’s partition key and sort key.
  • A Local Secondary Index (LSI): shares the table’s partition key but uses a different sort key, allowing range queries on the alternate sort attribute.
  • A Global Secondary Index (GSI): has its own partition key and optional sort key, effectively a separate queryable view of the data with different access characteristics.

The choice of what to query against is determined by your schema. The DynamoDB Data Model page covers how LSIs and GSIs are defined and when each is appropriate.

Query vs Scan

Scan is expensive in production

A Scan on a table with one million items reads all one million items, regardless of how many you actually need. On a busy table this consumes massive read capacity, is unpredictably slow, and can throttle other operations. If you are calling Scan from an API endpoint, it will eventually cause you problems as the table grows. Design your schema around Query from day one.

PropertyQueryScan
RequiresPartition key value (mandatory); sort key condition (optional)Nothing. Reads all items in the table or index.
What it readsOnly items in the specified partitionEvery item in the table or index
Read capacity consumedProportional to items read within the partitionProportional to the entire table size
SpeedMilliseconds, regardless of overall table sizeScales with table size; slow and unpredictable on large tables
Best use caseAll production reads with a known partition keyOne-off admin tasks, data migrations, very small tables only
Common mistakeUsing FilterExpression as a substitute for a GSIUsing Scan in a production API path on a growing table

Scan does have legitimate uses: running a one-time data migration, exporting a small lookup table, or inspecting a development table with a few dozen items. The problem is using Scan in a production read path where the table will grow over time.

Sort key condition operators

The sort key supports these operators in a KeyConditionExpression:

OperatorSyntaxExample
Equals=orderId = :id
Less than<orderId < :cutoff
Less than or equal<=createdAt <= :date
Greater than>score > :minScore
Greater than or equal>=score >= :minScore
BetweenBETWEEN :val1 AND :val2date BETWEEN :start AND :end
Begins withbegins_with(attr, :prefix)begins_with(orderId, :prefix)

Query examples

These examples use an Orders table with userId (partition key, String) and orderId (sort key, String). The orderId values use an ISO date prefix (for example 2025-03-15#abc123) so that lexicographic sort order matches chronological order. For more on using the AWS CLI, see AWS CLI Explained for Beginners.

Query by partition key#

Fetch all orders for a specific user. No sort key condition is given, so all items in the partition are returned.

aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid" \
  --expression-attribute-values '{":uid": {"S": "user-123"}}'

The response contains all items where userId is user-123, sorted ascending by orderId.

Query with begins_with on the sort key#

Fetch all orders for a user created in March 2025. The sort key prefix 2025-03 matches any orderId beginning with that string.

aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid AND begins_with(orderId, :prefix)" \
  --expression-attribute-values '{
    ":uid": {"S": "user-123"},
    ":prefix": {"S": "2025-03"}
  }'

Returns only items where orderId starts with 2025-03, which covers all orders placed in March 2025.

Query with BETWEEN on the sort key#

Fetch all orders for a user within a defined date range.

aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid AND orderId BETWEEN :start AND :end" \
  --expression-attribute-values '{
    ":uid": {"S": "user-123"},
    ":start": {"S": "2025-01-01"},
    ":end": {"S": "2025-03-31"}
  }'

Returns all orders for user-123 where orderId falls lexicographically between the two boundary values, covering January through March 2025.

Query newest-first with —scan-index-forward false#

By default, Query returns items sorted ascending by sort key. Pass --scan-index-forward false to reverse the order and return the most recent items first.

aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid" \
  --expression-attribute-values '{":uid": {"S": "user-123"}}' \
  --scan-index-forward false \
  --limit 10

Returns the 10 most recent orders for user-123 based on descending orderId sort order.

Query a GSI for an alternate access pattern#

Suppose a GSI named status-createdAt-index exists, with status as the GSI partition key and createdAt as the GSI sort key. This allows querying all orders with a specific status without knowing the userId.

aws dynamodb query \
  --table-name Orders \
  --index-name status-createdAt-index \
  --key-condition-expression "#s = :status" \
  --expression-attribute-names '{"#s": "status"}' \
  --expression-attribute-values '{":status": {"S": "PENDING"}}' \
  --scan-index-forward false

The #s alias is required because status is a DynamoDB reserved word. This returns all orders with status = "PENDING", sorted by createdAt descending. Without this GSI, finding items by status would require a Scan.

How to read the response

A Query response has several fields worth understanding:

  • Items: the list of items that matched the query, after FilterExpression has been applied.
  • Count: the number of items in Items. This is the post-filter count.
  • ScannedCount: the number of items read from storage before FilterExpression was applied. If no FilterExpression is set, Count and ScannedCount are equal. If they differ, the gap is how many items were read and then discarded by the filter.
  • LastEvaluatedKey: present when there are more results beyond the current page. If absent, the response is the final page.

A response can look successful (HTTP 200, no error) and still be incomplete. If LastEvaluatedKey is present, there are more items. Your application must issue another request.

The silent incomplete result trap

A page can return zero items in Items while still including a LastEvaluatedKey. This happens when a FilterExpression discards every item on a 1MB page. An empty Items array does not mean you have reached the end. Pagination must continue as long as LastEvaluatedKey is present.

FilterExpression explained

A FilterExpression applies additional conditions to items after they have been read from storage, before results are returned.

# Read all orders for user-123, then return only those with status = "SHIPPED"
aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid" \
  --filter-expression "#s = :status" \
  --expression-attribute-names '{"#s": "status"}' \
  --expression-attribute-values '{
    ":uid": {"S": "user-123"},
    ":status": {"S": "SHIPPED"}
  }'

Key things to understand:

Filtering happens after reading. DynamoDB first reads all items matching the KeyConditionExpression, then applies the filter and discards non-matching items. The read capacity for discarded items has already been consumed.

FilterExpression does not reduce read capacity. If a user has 10,000 orders and 5 have status = "SHIPPED", this query reads all 10,000 and returns 5. The read cost is 10,000 items, not 5.

Key attributes belong in KeyConditionExpression, not FilterExpression. The partition key and sort key must go in KeyConditionExpression. Placing them in FilterExpression returns an error.

Reserved words need expression attribute names. DynamoDB has hundreds of reserved words (status, name, date, type, size, and many more). If your attribute name is reserved, alias it with a placeholder like #s and map it in --expression-attribute-names.

A filtered page can return zero items and still have a LastEvaluatedKey. If every item on a 1MB page is discarded by the filter, Items is empty but LastEvaluatedKey may still be present. Pagination must continue.

Warning: FilterExpression is not free

If a FilterExpression consistently discards most of what was read, it is a sign the schema needs a GSI. A filter that throws away 95% of results means you are reading and paying for 20x more data than necessary. Add a GSI so you can Query the matching items directly instead of filtering after the fact.

Pagination

DynamoDB returns at most 1MB of data per request. This limit applies before FilterExpression runs. If 2MB of items match the KeyConditionExpression, the first page holds approximately 1MB of pre-filter items, even if the filter would discard most of them.

When more results remain, the response includes a LastEvaluatedKey. Pass that key as ExclusiveStartKey in the next request to continue from where you left off.

# First page
aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid" \
  --expression-attribute-values '{":uid": {"S": "user-123"}}' \
  --limit 10

# Next page — use LastEvaluatedKey from the previous response as ExclusiveStartKey
aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid" \
  --expression-attribute-values '{":uid": {"S": "user-123"}}' \
  --exclusive-start-key '{"userId": {"S": "user-123"}, "orderId": {"S": "2025-03-15#abc123"}}' \
  --limit 10

When LastEvaluatedKey is absent, you have reached the last page.

Cursor-based, not offset-based. DynamoDB pagination works like a cursor. Each page gives you a token for the next page, not a page number. You cannot request “page 5” directly. You must start at page 1 and follow the LastEvaluatedKey chain. “Jump to page N” features are not a natural fit for DynamoDB and require workarounds.

Common pagination mistakes:

  • Assuming the first response is complete. Always check for LastEvaluatedKey. On a growing table, this assumption will silently break over time.
  • Stopping when Items is empty. An empty Items array does not mean pagination is finished. Check LastEvaluatedKey, not just the item count.
  • Using --limit without pagination logic. --limit N caps how many items are read per page (before filtering), not the total items returned. Your application still needs to handle LastEvaluatedKey.
  • Treating page tokens as stable addresses. LastEvaluatedKey is only valid for the immediate continuation of a specific query. Do not store tokens as page number mappings or share them across different queries.
Cursor vs offset pagination

SQL databases let you write OFFSET 50 LIMIT 10 to jump to page 6 directly. DynamoDB does not work this way. Each page only knows about the next page, not an arbitrary one. Design your pagination UI around next/previous navigation. Trying to implement “jump to page N” requires storing and walking through all prior tokens, which is impractical at scale.

ProjectionExpression

By default, DynamoDB returns all attributes for every item. If items have many attributes but your application only needs a few, use ProjectionExpression to limit which attributes are included in the response.

# Return only orderId, status, and total — not all attributes
aws dynamodb query \
  --table-name Orders \
  --key-condition-expression "userId = :uid" \
  --expression-attribute-values '{":uid": {"S": "user-123"}}' \
  --projection-expression "orderId, #s, total" \
  --expression-attribute-names '{"#s": "status"}'

ProjectionExpression reduces response payload and bandwidth. Returning fewer attributes means smaller responses, lower data transfer costs, and faster parsing in your application.

Common misconception about RCUs

ProjectionExpression does not reduce provisioned throughput consumption. Read capacity units (RCUs) are calculated based on the full size of the item as stored on disk, not the subset you project. A 4KB item costs 0.5 RCUs whether you return all its attributes or just one. Use ProjectionExpression to save bandwidth, not to reduce your RCU bill.

Query vs GetItem vs BatchGetItem

These three operations are often confused but serve different purposes:

OperationWhat it doesRequiresBest for
GetItemRetrieves a single item by its exact primary keyFull primary key (partition key + sort key if the table has one)Fetching one known item by exact key
BatchGetItemRetrieves multiple known items in a single API callFull primary key for each itemFetching a set of known items from one or more tables
QueryRetrieves multiple items by partition key, with optional sort key conditionsPartition key value; sort key condition is optionalFetching all items in a partition, or a range within a partition

Use GetItem when you know the exact key for a single item. It is the most efficient single-item operation and supports strongly consistent reads.

Use BatchGetItem when you have a list of specific item keys from different partitions or tables, and want to retrieve them all at once. It is far more efficient than looping over individual GetItem calls. One BatchGetItem request can fetch up to 100 items, with a total payload limit of 16MB.

Use Query when you want all items sharing a partition key, or a specific range of items within a partition defined by a sort key condition.

None of these replace Scan. If you cannot use GetItem, BatchGetItem, or Query because the partition key is unknown, the right answer is a GSI, not a Scan.

When to add a GSI instead of filtering

A Global Secondary Index (GSI) lets you Query on a completely different partition key from the base table. Add a GSI when:

  • A FilterExpression consistently discards most of what was read
  • You need to look up items by an attribute that is not the table’s partition key
  • The only way to answer a production query with the current schema is a Scan

Example: finding items by status

A table keyed on userId stores order records. You want a monitoring dashboard showing all orders with status = "PENDING".

Without a GSI, you Scan the entire table and filter on status, reading every order regardless of status. With a GSI on status (partition key) and createdAt (sort key), you Query the GSI directly, reading only the matching items.

Example: looking up by secondary identifier

A user table is keyed on userId (a UUID). Support queries need to find users by email address.

Without a GSI, you Scan and filter on email, reading every user record. With a GSI on email (partition key), you Query the GSI for email = "user@example.com" and retrieve the item in one step.

Example: time-based queries across all records

An events table is keyed on eventId. You need to query all events in a given time range across all event IDs.

Without a GSI, only a Scan can answer this. With a GSI on a synthetic partition key (for example entityType = "EVENT") and createdAt as the sort key, you Query the GSI with a BETWEEN condition on createdAt.

GSIs replicate data asynchronously from the base table, so reads against a GSI are eventually consistent, not strongly consistent. In Provisioned mode they have their own throughput settings. The DynamoDB Data Model page covers GSI design patterns and their tradeoffs in detail.

You can also restrict access to specific items via IAM using the dynamodb:LeadingKeys condition, which is useful if users should only query their own partition. See DynamoDB Security: IAM, Encryption, and Access Control for how fine-grained access control works alongside query patterns.

Common mistakes

  1. Using Scan in production on large tables. A Scan reads every item in the table sequentially. On a million-item table it is slow, expensive, and can throttle other operations sharing the same provisioned throughput. Design your schema to support Query from the start.
  2. Expecting FilterExpression to reduce costs. FilterExpression only reduces the items in the response, not the read capacity consumed. Items are charged as read before the filter runs. A filter discarding 99% of results costs the same as no filter at all.
  3. Ignoring pagination. If your application calls Query and treats the first response as complete, it will silently return incomplete data whenever results exceed 1MB. Always check for LastEvaluatedKey and loop until it is absent.
  4. Using Query for an access pattern that needs a GSI. If you are writing a FilterExpression because you do not know the partition key value, the access pattern needs a GSI, not a filter. A filter is not a substitute for a well-designed index.
  5. Assuming ProjectionExpression makes queries cheaper in RCUs. ProjectionExpression reduces response payload size and bandwidth. It does not reduce RCUs consumed, which are calculated from the full item size on disk.
  6. Putting key attributes in FilterExpression. The partition key and sort key belong in KeyConditionExpression. Placing them in FilterExpression returns an error.
  7. Stopping pagination on empty pages. A page with zero items in Items is not necessarily the last page. If LastEvaluatedKey is present, continue iterating. The next page may contain results.
  8. Treating DynamoDB like ad hoc SQL. DynamoDB requires access-pattern-driven schema design. If you find yourself needing FilterExpressions across many different attributes, reconsider the schema or whether DynamoDB is the right tool. See RDS vs DynamoDB in AWS for guidance on which fits your use case.

Frequently asked questions

What is the difference between Query and Scan in DynamoDB?

Query reads only the items in a specific partition, using the partition key to locate them efficiently. Scan reads every item in the entire table, then applies any filter you specify. Query is fast and cost-efficient at any table size. Scan is slow and expensive on large tables because it reads everything regardless of how many results you need.

Can I query DynamoDB without the partition key?

No. A Query always requires the partition key value. If you need to query by a different attribute, you must create a Global Secondary Index (GSI) with that attribute as the partition key. Without a GSI, you would need to Scan the table, which reads every item and is expensive.

Does FilterExpression reduce read capacity consumed?

No. FilterExpression is applied after items are read from storage. The read capacity consumed is based on how many items were read before filtering, not how many were returned. If a partition has 10,000 items but your filter matches only 5, you still consume capacity for all 10,000.

How does pagination work in DynamoDB?

DynamoDB returns at most 1MB of data per request. If more items match your query, the response includes a LastEvaluatedKey. Pass that key as ExclusiveStartKey in the next request to continue from where you left off. Repeat until LastEvaluatedKey is absent from the response. This is cursor-based pagination, not offset-based, so you cannot jump to an arbitrary page number.

When should I create a GSI in DynamoDB?

Create a GSI when you need to query by an attribute that is not the table partition key. If your table is keyed on userId but you also need to look up items by email or status, create a GSI with email or status as the partition key. A GSI lets you Query efficiently on the new key instead of Scanning the whole table.

Last verified: 5 April 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.