DynamoDB's Access-Pattern-First Design Model
DynamoDB rewards a habit that feels backwards if you come from SQL: you decide how you will read the data before you decide how to store it.
Busca en todas las páginas de la documentación
DynamoDB rewards a habit that feels backwards if you come from SQL: you decide how you will read the data before you decide how to store it.
In a relational database you model entities, normalize them into tables, and trust the query planner to join whatever you ask for later. DynamoDB has no joins and no query planner. The shape of your keys is the shape of your queries.
This page builds the mental model behind that constraint, so every other page in this section reads as an application of one idea.
Start with what a partition key actually does.
DynamoDB spreads your data across many physical partitions. When you write an item, DynamoDB hashes its partition key to decide which partition stores it. When you read, it hashes the same key to find that partition in one hop. That is why a Query on a single partition key is fast at any table size - it never touches the others.
An access pattern is one concrete question your application asks the data. "Get the user by id." "List all orders for a customer, newest first." "Find every open ticket assigned to an agent." Each is a read (or write) with known inputs.
The core rule follows directly: the inputs you have at read time must be the key you look up by. If you always fetch orders by customer, then customerId should be the partition key so that fetch is a single-partition Query. If instead you stored orders keyed only by orderId, listing a customer's orders would force a Scan of the whole table.
Here is the same lookup in both languages, using the document interface. The point is that the key you query by was chosen to match the pattern "orders for one customer."
# --- Python (boto3) ---
from boto3.dynamodb.conditions import Key
# Access pattern: "all orders for a customer" -> customerId is the partition key.
resp = table.query(KeyConditionExpression=Key("customerId").eq("c-1"))
print([o["orderId"] for o in resp["Items"]])// --- TypeScript (AWS SDK v3) ---
import { QueryCommand } from "@aws-sdk/lib-dynamodb";
// Access pattern: "all orders for a customer" -> customerId is the partition key.
const resp = await doc.send(new QueryCommand({
TableName: "Orders",
KeyConditionExpression: "customerId = :c",
ExpressionAttributeValues: { ":c": "c-1" },
}));
console.log((resp.Items ?? []).map((o) => o.orderId));The query is trivial because the design work happened earlier, when the key was chosen to fit the pattern.
The design process is an inventory, not an inspiration.
Before you create a table, write down every access pattern as a table of its own: the operation, the inputs you will have, and the result you expect. A short one might read:
| Access pattern | Inputs at runtime | Expected result |
|---|---|---|
| Get a customer | customerId | one customer item |
| List a customer's orders | customerId | many order items, sorted by date |
| Get one order | customerId, orderId | one order item |
| List orders by status | status | many order items across customers |
Now the keys almost choose themselves. customerId as the partition key serves the first three patterns. A sort key of orderId (or an order-date prefix) lets the second pattern return items already ordered and lets the third pinpoint one order. The fourth pattern uses a different input - status - which is not the table's key, so it needs a secondary index keyed on status.
This is where DynamoDB diverges hardest from SQL. In a relational schema you would normalize customers and orders into separate tables and JOIN them on demand. DynamoDB has no join, so you denormalize: you store related data together, or duplicate a field into an index, so that a single Query returns the whole answer. Reads are cheap and writes do a little more work to keep the duplicated data consistent - the opposite of the relational trade-off.
The sort key is what turns a flat key-value store into something that answers range questions. Because items under one partition key are stored sorted by their sort key, you get "newest first," "everything since a date," and "all items whose id begins with a prefix" for free, using begins_with, between, and comparison operators in the KeyConditionExpression.
Query and Scan are the two ways to read many items, and the model exists to keep you on Query. A Query targets one partition key and reads only matching items. A Scan reads every item in the table and filters afterward, so its cost grows with the table, not with the result. A design that forces a Scan for a common pattern is a design bug, not a tuning problem.
Once the inventory drives the keys, two larger patterns emerge.
The first is single-table design. Because keys can be generic (PK, SK) rather than entity-specific, you can store customers, orders, and line items in one table by giving each a key prefix like CUSTOMER#c-1 or ORDER#o-100. One Query on PK = CUSTOMER#c-1 can then return the customer and their orders together. This is powerful and also the most common source of over-engineering, so it has its own page.
The second is index overloading. A Global Secondary Index (GSI) is just another pair of keys projected over the same items, giving you a second access pattern. You can reuse one GSI for several patterns by writing carefully chosen values into its key attributes - the "orders by status" lookup above becomes a GSI whose partition key holds the status string.
The trade-off to accept up front: DynamoDB gives you flat, predictable latency and effortless scale, in exchange for committing to your access patterns early. Discovering a genuinely new pattern later is not fatal - you add a GSI, or backfill a new attribute - but it is real work, unlike adding a WHERE clause in SQL. That is why the inventory is the highest-leverage half hour in the whole project.
The habit to internalize: model the questions, not the nouns. Every time you reach for a new query, ask whether an existing key already answers it, whether a sort-key condition can, or whether you need a new index. If none fit, revisit the inventory before writing code around a Scan.
It means you list the exact reads and writes your app performs - with the inputs you will have at runtime - before you design the table, then choose keys and indexes that make each one a targeted lookup.
DynamoDB has no joins and no query planner. The only efficient read is a Query on a key you defined, so a pattern you did not plan for may force an expensive Scan or a new index.
The partition key decides which physical partition stores an item and is hashed for lookups. The sort key orders items within that partition, enabling range queries like "newest first" or "ids beginning with a prefix."
Query reads only items under one partition key and is efficient at any scale. Scan reads the entire table and filters afterward, so its cost grows with table size regardless of how many items match.
Without joins, the way to answer a question in one read is to store related data together or duplicate a field into an index. You trade a little extra write work for cheap, single-round-trip reads.
You typically add a Global Secondary Index keyed for the new pattern, and optionally backfill a new attribute onto existing items. It is real work but routine - not a schema rebuild.
No. A filter is applied after DynamoDB reads and meters the items, so it reduces what you get back but not the read capacity you consumed. Design a key or index to avoid over-reading instead.
No. It is a powerful technique for related entities queried together, but a simple key-value table is fine for simple needs. Reach for single-table design when your access patterns span multiple entity types.
All of the ones you know about, written down explicitly. The list does not need to be exhaustive forever, but the common, high-traffic reads must each map to a Query on a key or index before you create the table.
Stack versions: This page was written for boto3 1.43.x (Python 3.10+) and the AWS SDK for JavaScript v3 (Node.js 18+).
Revisado por Chris St. John·Última actualización: 23 jul 2026