How to Query DynamoDB: A Beginner's Guide
A beginner-friendly guide to querying DynamoDB — GetItem, Query and Scan, key conditions, plus AWS CLI and SDK examples and common mistakes to avoid.
On this page
- The three ways to read data
- Understand the primary key first
- GetItem: fetch one known item
- Query: fetch related items
- Narrowing with the sort key
- Querying by a non-key attribute
- Filtering vs. key conditions
- Scan: the last resort
- Don’t forget pagination
- Prefer PartiQL? You can use SQL-like syntax
- Common beginner mistakes
If you’re coming from SQL, querying DynamoDB feels alien at first: there’s no SELECT ... WHERE that searches arbitrary columns, and no joins. Instead you have three distinct operations, and picking the right one is most of the skill. This guide walks through all three with runnable examples.
The three ways to read data
| Operation | Returns | Requires | Use when |
|---|---|---|---|
GetItem | One item | Full primary key | You know the exact item |
Query | Many items | Exact partition key | You want related items in one partition |
Scan | Everything (paged) | Nothing | Last resort / batch jobs |
The golden rule: Query, don’t Scan. A Query jumps straight to a partition and reads only what you ask for. A Scan reads the entire table. The full tradeoff is in query vs scan.
Understand the primary key first
Every DynamoDB table has a primary key that is either:
- Partition key only — e.g.
userId. Each value must be unique. - Partition key + sort key — e.g.
userId(partition) +orderDate(sort). Items sharing a partition key are stored together, sorted by the sort key.
GetItem needs the complete key. Query needs the partition key and lets you filter on the sort key. You can’t query by anything else without an index. See primary keys for how to choose them.
GetItem: fetch one known item
When you have the full key, GetItem is the cheapest and fastest read.
aws dynamodb get-item \
--table-name Users \
--key '{"userId": {"S": "user-123"}}'
In the AWS SDK for Python (boto3), the resource API hides the verbose type annotations:
import boto3
table = boto3.resource("dynamodb").Table("Users")
resp = table.get_item(Key={"userId": "user-123"})
item = resp.get("Item") # None if not found
By default GetItem is eventually consistent. Pass ConsistentRead=True to get the latest write at double the read cost — see consistency.
Query: fetch related items
Query returns all items with a given partition key. You express the condition with a KeyConditionExpression.
from boto3.dynamodb.conditions import Key
# All orders for one customer
resp = table.query(
KeyConditionExpression=Key("customerId").eq("cust-9")
)
for item in resp["Items"]:
print(item)
Narrowing with the sort key
If the table has a sort key, you can add a condition to it — this is where Query gets powerful:
# Orders for a customer in June 2026, oldest first
resp = table.query(
KeyConditionExpression=Key("customerId").eq("cust-9")
& Key("orderDate").between("2026-06-01", "2026-06-30")
)
Sort-key operators you can use: eq, lt, lte, gt, gte, between, and begins_with. The partition key must use eq — exact match only.
Two more useful options:
ScanIndexForward=Falsereturns items in descending sort-key order (e.g. newest first).Limit=Ncaps items per page (note: it’s a page size, not a total — see pagination below).
# Most recent 10 orders
resp = table.query(
KeyConditionExpression=Key("customerId").eq("cust-9"),
ScanIndexForward=False,
Limit=10,
)
The same query on the CLI:
aws dynamodb query \
--table-name Orders \
--key-condition-expression "customerId = :c AND begins_with(orderDate, :d)" \
--expression-attribute-values '{":c": {"S": "cust-9"}, ":d": {"S": "2026-06"}}'
Note the :c and :d placeholders — these are expression attribute values, the parameterized way to pass values. They keep your queries safe and let you use reserved words; see expressions for the details.
Querying by a non-key attribute
Query can’t search arbitrary attributes. To query by, say, email when your key is userId, create a Global Secondary Index keyed on email, then query the index:
resp = table.query(
IndexName="email-index",
KeyConditionExpression=Key("email").eq("ana@example.com"),
)
This is the right answer almost every time you’re tempted to Scan. See secondary indexes.
Filtering vs. key conditions
A FilterExpression looks like a WHERE clause, but it’s applied after items are read — you still pay to read everything the key condition matched, and only then are non-matching items dropped from the result. Use filters to trim already-targeted results, never as a substitute for a key condition.
# Reads all of cust-9's orders, then drops the unshipped ones
resp = table.query(
KeyConditionExpression=Key("customerId").eq("cust-9"),
FilterExpression=Key("status").eq("SHIPPED"), # billed on pre-filter size
)
Scan: the last resort
Scan reads every item in the table, page by page. It’s appropriate for one-off migrations, exports, or small lookup tables — never for user-facing reads on a large table.
resp = table.scan(FilterExpression=Key("status").eq("PENDING"))
Don’t forget pagination
A single Query or Scan returns at most 1 MB of data. If there’s more, the response includes a LastEvaluatedKey; pass it back as ExclusiveStartKey to get the next page. Forgetting this means silently missing results.
items, last_key = [], None
while True:
kwargs = {"KeyConditionExpression": Key("customerId").eq("cust-9")}
if last_key:
kwargs["ExclusiveStartKey"] = last_key
resp = table.query(**kwargs)
items.extend(resp["Items"])
last_key = resp.get("LastEvaluatedKey")
if not last_key:
break
Full details in pagination.
Prefer PartiQL? You can use SQL-like syntax
If the operation-based API feels clunky, DynamoDB also supports PartiQL, a SQL-compatible query language. It still maps to the same Query/Scan rules under the hood — a SELECT with a partition key in the WHERE clause runs as a Query; without one it runs as a Scan. See PartiQL.
SELECT * FROM Orders WHERE customerId = 'cust-9'
Common beginner mistakes
- Partition key mismatch. Matches are exact and case-sensitive.
User#123≠USER#123. - Using a FilterExpression for the key. The key goes in
KeyConditionExpression; filters can’t reduce cost. - Scanning when a Query or GSI would do. This is the #1 cost and latency bug.
- Ignoring
LastEvaluatedKey. You’ll think data is missing when it’s just on page two. - Expecting strong consistency on a GSI. GSIs are always eventually consistent.
A quick way to build confidence is to run these operations against real data and watch what comes back. A native DynamoDB GUI like Tablyne lets you compose a Query visually — pick the table or index, set the key condition — and see the items and consumed capacity immediately, which makes the Query-vs-Scan distinction click faster than reading about it. Once the patterns are familiar, the SDK calls feel natural. Next, read query vs scan to internalize when each operation is appropriate.
Frequently asked questions
What's the difference between Query and GetItem in DynamoDB?
GetItem fetches a single item by its full primary key (partition key, plus sort key if the table has one). Query fetches multiple items sharing the same partition key, narrowed by an optional sort-key condition. Use GetItem when you know the exact item, Query when you want a set of related items.
Can I query DynamoDB without the partition key?
Not with Query — it always requires an exact partition key value. To search by a non-key attribute you either create a Global Secondary Index keyed on that attribute and Query the index, or fall back to a Scan, which reads the whole table and is slow and expensive.
Why does my DynamoDB query return no items even though they exist?
The usual cause is a partition key mismatch — DynamoDB requires an exact, case-sensitive match on the partition key, so 'User#123' won't match 'USER#123'. Also check that you're using a KeyConditionExpression, not a FilterExpression, for the key, and that you're querying the right table or index.