PK/SK design, item collections, GSIs and sparse indexes, hot partitions, and conditional writes.
Published September 24, 2026
Amazon DynamoDB is a fully managed key-value and document database. It promises single-digit-millisecond latency at practically any scale, but only for access patterns you designed the keys for. Like Cassandra, it rewards access-pattern-first modeling, and the central skill is designing keys, not tables.
Every item (row) has a primary key, in one of two forms:
PK): a pure key-value lookup.PK + SK): items sharing a partition key form an item collection, stored together and sorted by the sort key.The operations that matter:
| Operation | What it does | Cost profile |
|---|---|---|
GetItem | Fetch one item by its full key | Cheapest, O(1) |
Query | Fetch items from one partition key, optionally filtered by a sort-key condition (=, <, between, begins_with) | Efficient: reads only the matching range |
Scan | Read the whole table | Expensive and slow. Avoid on hot paths |
A FilterExpression on a Query is applied after the items are read, so you still pay for everything read. Filters trim responses, they don't make reads efficient. Efficiency comes only from keys.
Advanced designs use generic attribute names (PK, SK), and encode the entity type into the key values. That lets different kinds of items share one table, and one partition:
| PK | SK | Other attributes |
|---|---|---|
CUSTOMER#c42 | PROFILE | name, email, tier |
CUSTOMER#c42 | ORDER#2026-03-01#o981 | status, total |
CUSTOMER#c42 | ORDER#2026-03-14#o1002 | status, total |
ORDER#o981 | ITEM#1 | sku, qty, price |
ORDER#o981 | ITEM#2 | sku, qty, price |
This supports several access patterns with plain Queries:
GetItem(PK = CUSTOMER#c42, SK = PROFILE).Query(PK = CUSTOMER#c42, SK begins_with "ORDER#"), reading backwards (ScanIndexForward = false). Putting the date in the sort key makes the order chronological.Query(PK = CUSTOMER#c42) returns both kinds of item from the same collection. This is effectively a pre-joined result.Query(PK = ORDER#o981, SK begins_with "ITEM#").This is single-table design: related entities live in one table, keyed so that the reads that go together are stored together. It minimizes round trips, and it's why DynamoDB can serve complex screens with one or two requests.
When you need to query by a different attribute, add a GSI: a copy of selected attributes, re-keyed by different partition and sort keys, maintained automatically and asynchronously.
Example: "find orders by status, for the operations dashboard". Add attributes GSI1PK = STATUS#SHIPPED and GSI1SK = 2026-03-14#o1002 to order items, and create a GSI on them. Then Query(GSI1, GSI1PK = STATUS#SHIPPED) lists shipped orders by date.
Things to know about GSIs:
GSI2PK only on orders that need manual review, and the index becomes a small, efficient queue of exactly those orders.Throughput is spread across physical partitions by partition key. A single partition key receiving a disproportionate share of traffic (a viral product, one huge tenant, "today's" date as a key) hits per-partition limits, and gets throttled, even when the table's total capacity is plenty.
Mitigations:
VOTES#item42#0 … #9), spread writes across the suffixes, and sum across them when reading.attribute_not_exists(PK) to create only if absent, or version = :expected for optimistic locking.TransactWriteItems applies up to 100 writes across items and tables atomically, for example decrementing stock and creating an order together. Transactions cost roughly twice as much as normal writes, so use them where atomicity is required.Single-table design shines when access patterns are well known and stable, and latency at scale matters: e-commerce order flows, gaming profiles, SaaS tenant data. Its costs are real, though:
For early-stage products whose queries change weekly, a relational database, or a simpler one-table-per-entity DynamoDB design, is often the more pragmatic starting point.
Q: What's the difference between Query and Scan? A: Query reads items from a single partition key, optionally narrowed by sort-key conditions, so it touches only the relevant items. Scan reads the entire table, then optionally filters. It's slow and expensive, and should stay out of request paths.
Q: Why put multiple entity types in one table? A: Items that are read together can share a partition key, so one Query returns them all (for example, a customer's profile and recent orders). That replaces joins and multiple round trips, which DynamoDB doesn't support efficiently.
Q: What is a sparse index? A: A GSI whose key attributes exist on only some items. Only those items appear in the index, which makes it a compact, efficient way to query a subset, such as "orders awaiting review", without scanning everything.
Q: How do you avoid hot partitions? A: Use high-cardinality partition keys, spread heavy write traffic for a single logical key across several suffixed keys, and cache hot reads. Avoid keys such as the current date or a status value that concentrate traffic.
Q: Are GSIs strongly consistent? A: No. Global secondary indexes are updated asynchronously, and support only eventually consistent reads. If a read must reflect a write immediately, query the base table by its primary key, which supports strongly consistent reads.