DynamoDB Primary Keys: How They Work and How to Choose One
Every DynamoDB item is identified by a primary key. That key determines where the item is physically stored, which queries are possible, and which queries will never be possible without a migration or an index.
It’s the least reversible decision in a DynamoDB schema. You cannot alter a table’s key schema after creation. Getting it right matters more than anything else you’ll decide.
Two kinds of primary key
DynamoDB offers exactly two shapes, chosen at table creation:
Simple primary key
A partition key alone. One item per key value. The table is a key-value store.
PK: USER#u_01 → { name: "Alice", email: "alice@acme.com" }
PK: USER#u_02 → { name: "Bob", email: "bob@acme.com" }
You can GetItem by exact key and PutItem to overwrite. You cannot Query, because there’s nothing to range over.
Composite primary key
A partition key plus a sort key. Many items per partition key value, physically ordered by sort key.
PK: TENANT#t_01 SK: #METADATA → { name: "Acme Corp" }
PK: TENANT#t_01 SK: USER#u_01 → { name: "Alice" }
PK: TENANT#t_01 SK: USER#u_02 → { name: "Bob" }
PK: TENANT#t_01 SK: PROJECT#01HVMK3P2Q → { title: "Q1 Roadmap" }
Now Query is available, and with it range conditions on the sort key:
Query(pk = TENANT#t_01) → the whole tenant
Query(pk = TENANT#t_01, sk begins_with "USER#") → just the users
Choose composite unless you’re certain you want a key-value store. Adding a constant sort key like #METADATA to what is currently a single item costs almost nothing and leaves the door open to co-locating related items later. Going the other way is a table migration.
The rules that govern every key
These apply to primary keys and to index keys alike.
Only three types are valid. String (S), Number (N), or Binary (B). No booleans, no maps, no lists, no sets. See DynamoDB Data Types for the full picture.
Keys are required on every item. An item without the table’s key attributes is rejected. Unlike every other attribute in DynamoDB, key attributes are not optional.
Keys are immutable. There is no UpdateItem that changes a key. Changing one means delete the old item and put a new one. That’s two operations, not atomic unless you wrap them in a transaction, and any concurrent reader may see neither.
This is the rule people violate most often, and always the same way: composing a key from a mutable business value. Email addresses change. Display names change. Statuses change constantly. Compose keys from immutable identifiers only.
Together they must be unique. For a simple key, the partition key alone is unique. For a composite key, the pair is unique - the same partition key with different sort keys is not just allowed, it’s the point.
Values have size limits. Partition key values up to 2048 bytes, sort key values up to 1024 bytes. Generous, but deeply nested composite key hierarchies can approach the sort key limit.
Empty strings are not allowed in keys. They’re permitted for regular attributes since 2020, but not for key attributes.
What the partition key does
The partition key is hashed to select a physical storage partition. Two consequences follow, and they drive most schema design:
Co-location. Items sharing a partition key value live together and can be fetched in one Query. This is what makes single-table design possible.
Distribution. Traffic spreads across partitions by key value. A value receiving disproportionate traffic becomes a hot partition and throttles, even when the table has capacity to spare.
The exact-match constraint is the one that surprises people arriving from SQL. You must supply the complete partition key value on every Query and GetItem:
Query(pk = "TENANT#t_01") ✅
Query(pk begins_with "TENANT#") ❌ not possible
Query(pk between "TENANT#a" and "#z") ❌ not possible
DynamoDB has to compute a hash, so partial values are meaningless. Range operators are a sort key feature only.
What the sort key does
The sort key orders items within a partition and enables range queries: =, begins_with, between, <, <=, >, >=, plus ScanIndexForward: false to reverse.
begins_with is the workhorse of single-table design. Because entity types are prefixed into sort key values, one condition acts simultaneously as a type filter and a range query.
Sort keys can also encode hierarchies by concatenating values from most general to most specific. LOCATION#US#CA#SAN_FRANCISCO can be queried at country, state, or city level from one key structure. Full treatment in Composite Key.
Primary keys versus indexes
A common source of confusion, worth stating plainly.
| Primary key | LSI | GSI | |
|---|---|---|---|
| Required | Yes | No | No |
| Created after table exists | No | No | Yes |
| Different partition key | n/a | No, same PK | Yes |
| Consistency | Strong available | Strong available | Eventual only |
| Capacity | Table’s | Table’s | Its own |
| Limit per table | 1 | 5 | 25 |
A Local Secondary Index gives an alternative sort key over the same partition key. It must be created with the table, and it constrains any single partition to 10 GB. LSIs are rarely worth their inflexibility.
A Global Secondary Index is effectively a separate table maintained by DynamoDB, with its own partition and sort keys. It can be added later. It is always eventually consistent, and every write to the base table that touches its key attributes is replicated and charged.
The design principle: serve as many access patterns as possible from the primary key. Every GSI adds a write unit per item on every write. Add them for the patterns the primary key genuinely cannot serve, usually cross-partition lookups like login-by-email.
Choosing your key
A working sequence:
1. List every access pattern first. Not some of them. Every one. “Get user by ID.” “List a tenant’s projects, newest first.” “Find a user by email at login.” This step is not optional, and skipping it is the root cause of most DynamoDB schemas that end up needing a migration.
2. Find the entity that everything hangs off. In multi-tenant SaaS it’s the tenant. In e-commerce it’s often the customer or the order. That entity’s identifier is your leading partition key candidate.
3. Check cardinality and distribution. Many distinct values, spread evenly under real traffic. tenantId is high cardinality but skewed if one enterprise customer dominates. status is low cardinality and almost always the wrong choice for a base table partition key.
4. Check availability at query time. Callers must have the exact value. If a lookup only ever has a projectId in hand and your key requires tenantId plus a timestamp, that pattern cannot be served. This is where schemas quietly break.
5. Design the sort key to serve the remaining patterns. Entity prefixes for type filtering, hierarchy for granularity, a sortable identifier like a ULID when you need both ordering and direct lookup.
6. Add GSIs only for what’s left. Then verify: every access pattern should resolve to a GetItem or a Query. Any that resolves to a Scan means step 1 was incomplete.
Common mistakes
Composing keys from mutable values. Emails, names, statuses. Keys cannot be updated.
Choosing a simple primary key by default. Composite costs nothing extra and preserves optionality.
Using the sort key as a filter for something you can’t query on. You can filter on a sort key prefix, never a suffix. If the component you need to query by is at the end, the hierarchy is in the wrong order.
Numeric values in string keys without padding. ORDER#10 sorts before ORDER#2 in UTF-8 byte order. Zero-pad, or use a ULID.
Adding a GSI to paper over a key design problem. Sometimes right, often a permanent tax on every write to avoid a one-time redesign. Check whether the primary key could serve the pattern first.
Assuming you can change it later. You cannot. Key schema is fixed at table creation, and changing it means a new table and a full migration.
Worked examples
- SaaS Multi-Tenant - 4 entities, 10 access patterns, one tenant-scoped partition
- E-Commerce Orders - dual-key order access, and why the same order is written under two partition keys
Related terms
- Partition Key · Sort Key · Composite Key
- Hot Partition · Sparse Index
- DynamoDB Data Types - what can and cannot be a key
Primary key design is the decision you make first, with the least information, and can least afford to get wrong. I’m building singletable.dev to let you see the consequences of a key structure before you deploy it.