The first-table quickstart in the Core Services section covered create, put, get, and a simple query. This page is the next layer: the everyday modeling vocabulary you need before single-table design and transactions make sense.
Each example is small and uses the document interface, so you work with plain objects rather than DynamoDB's typed attribute values. Read them in order - keys, then range queries, then indexes, then updates and batches.
Model a table whose partition key groups items and whose sort key both orders and identifies them.
# --- Python (boto3) ---# PK groups by customer; SK is an order id prefixed so items sort together.table.put_item(Item={"pk": "CUSTOMER#c-1", "sk": "ORDER#2026-01-05#o-100", "total": 42})table.put_item(Item={"pk": "CUSTOMER#c-1", "sk": "ORDER#2026-02-11#o-101", "total": 17})
// --- TypeScript (AWS SDK v3) ---import { PutCommand } from "@aws-sdk/lib-dynamodb";// PK groups by customer; SK is an order id prefixed so items sort together.await doc.send(new PutCommand({ TableName: "AppData", Item: { pk: "CUSTOMER#c-1", sk: "ORDER#2026-01-05#o-100", total: 42 } }));await doc.send(new PutCommand({ TableName: "AppData", Item: { pk: "CUSTOMER#c-1", sk: "ORDER#2026-02-11#o-101", total: 17 } }));
The partition key (pk) decides physical placement; all of one customer's items share a partition.
The sort key (sk) both orders items and makes each unique within the partition.
Prefixes like ORDER# and a date let you query subsets by sort-key condition later.
Generic key names (pk/sk) leave room to store more entity types in the same table.
Read a subset of one partition using begins_with, between, and comparisons - all evaluated on the key, so they are efficient.
# --- Python (boto3) ---from boto3.dynamodb.conditions import Key# All of this customer's orders (sort key begins with ORDER#).resp = table.query( KeyConditionExpression=Key("pk").eq("CUSTOMER#c-1") & Key("sk").begins_with("ORDER#"))print([i["sk"] for i in resp["Items"]])
// --- TypeScript (AWS SDK v3) ---import { QueryCommand } from "@aws-sdk/lib-dynamodb";// All of this customer's orders (sort key begins with ORDER#).const resp = await doc.send(new QueryCommand({ TableName: "AppData", KeyConditionExpression: "pk = :p AND begins_with(sk, :prefix)", ExpressionAttributeValues: { ":p": "CUSTOMER#c-1", ":prefix": "ORDER#" },}));console.log((resp.Items ?? []).map((i) => i.sk));
Sort-key conditions only work inside a single partition key - the pk equality is required.
begins_with, between, <, and > all apply to the sort key in a KeyConditionExpression.
Because items are stored sorted, "newest first" is just ScanIndexForward=False.
These conditions are metered on matched items only, unlike a post-read filter.
A GSI projects a new partition/sort key over the same items, enabling a query the base key cannot serve. Define it at create time (or add later with UpdateTable).
Send many puts (and deletes) in batches of up to 25, instead of one request per item.
# --- Python (boto3) ---# batch_writer buffers and flushes in 25-item batches, retrying unprocessed items.with table.batch_writer() as batch: for n in range(100): batch.put_item(Item={"pk": "CUSTOMER#c-1", "sk": f"EVENT#{n:04d}", "kind": "click"})
A Global Secondary Index (GSI) has a completely different partition key from the base table and can be added or removed at any time. It is eventually consistent and provisioned separately. Use it for most "second access pattern" needs.
A Local Secondary Index (LSI) shares the base table's partition key but adds a different sort key. It supports strongly consistent reads but must be created with the table and can never be added later, and it counts against a 10 GB per-partition limit. Reach for an LSI only when you need a strongly consistent alternate sort on the same partition.
Reads default to eventually consistent. Pass ConsistentRead=True on a GetItem or Query against the base table or an LSI when you must see the latest write. GSIs cannot be read strongly consistently at all.
Filtering when you meant to query. A FilterExpression runs after the read and still bills for scanned items. Fix: narrow with a KeyConditionExpression or an index instead.
Forgetting reserved words.status, name, size, and many others are reserved. Fix: use an ExpressionAttributeNames placeholder like #s.
Trying to add an LSI later. LSIs must be defined at table creation. Fix: plan LSIs up front, or use a GSI, which can be added anytime.
Batching more than 25 items.BatchWriteItem rejects more than 25 requests. Fix: chunk into 25s (boto3's batch_writer does this for you).
Assuming UpdateItem only updates. With no condition, UpdateItem inserts a new item if the key is absent. Fix: add attribute_exists(pk) when you require the item to pre-exist.
Whenever you need to store multiple related items under one partition key and query ranges of them - "all orders for a customer," "events since a date." The sort key both orders and uniquely identifies items within the partition.
What is the difference between a GSI and an LSI?
A GSI has a different partition key, is eventually consistent, and can be added anytime. An LSI shares the table's partition key with a different sort key, supports strong consistency, but must be created with the table and cannot be added later.
Does UpdateItem replace the whole item?
No. It applies only the changes in your UpdateExpression - SET, REMOVE, ADD, DELETE on specific attributes - leaving everything else untouched. It is the right tool for changing one field.
Why do I need ExpressionAttributeNames?
Many attribute names (status, name, size, ttl) are DynamoDB reserved words and cannot appear directly in an expression. A #name placeholder mapped through ExpressionAttributeNames sidesteps the reservation.
How many items can I write in one batch?
BatchWriteItem accepts up to 25 put or delete requests per call. The boto3 batch_writer context manager chunks larger loads into 25-item batches and retries unprocessed items automatically.
Can I read a GSI with strong consistency?
No. GSIs are always eventually consistent because DynamoDB replicates writes into them asynchronously. If you need a strongly consistent alternate sort, use an LSI, which must be defined at table creation.
What is a sparse index?
An index that only contains items which have its key attributes. If you write the index's partition-key attribute onto just some items, the index holds only those - a handy way to query "items in state X" cheaply.
Does batching reduce my capacity cost?
It reduces network round trips and per-request overhead, but each item still consumes its own read or write capacity. Batching is about efficiency and latency, not a capacity discount.