Partition Key
The partition key is the attribute DynamoDB hashes to decide which physical storage partition an item lives on. It’s the first component of every primary key, it’s mandatory, and it’s the single most consequential decision in a DynamoDB schema.
Also called the hash key in older AWS documentation and in some SDKs. The terms are interchangeable.
What it actually does
DynamoDB runs the partition key value through an internal hash function. The output determines which partition stores the item. Items with the same partition key value are guaranteed to land on the same partition, physically adjacent, sorted by sort key.
Two consequences follow, and everything else about partition key design derives from them:
Co-location. All items sharing a partition key can be retrieved in one Query. This is what makes single-table design possible: put a tenant’s users, projects, and metadata under TENANT#t_01 and one query returns them all.
Distribution. Traffic to your table is distributed by partition key value. If one value receives a disproportionate share of reads or writes, that partition throttles while the rest of the table sits idle. This is a hot partition.
The exact-match constraint
This is the constraint that surprises people coming from SQL, and it shapes the entire schema.
You must supply the exact partition key value on every Query and GetItem. No ranges, no prefixes, no begins_with, no wildcards. DynamoDB has to compute a hash, so it needs the complete value.
Query(pk = "TENANT#t_01") ✅ exact match
Query(pk begins_with "TENANT#") ❌ not possible
Query(pk between "TENANT#a" and "TENANT#z") ❌ not possible
Sort keys support range operators. Partition keys never do.
The practical implication: you can only query data you can name. If your application needs to fetch a project but only ever has the projectId in hand, then projectId had better be reachable, either as a partition key or through a GSI. A schema where a common lookup requires information the caller doesn’t have is a broken schema, and the only remedy is a Scan or a migration.
The one way to enumerate without knowing values is a Scan, which reads the entire table. In production, treat any access pattern that resolves to a Scan as a design defect.
Choosing a good partition key
High cardinality. You want many distinct values, spread evenly. userId, tenantId, orderId are good. status, country, isActive are bad as base table partition keys because a handful of values absorb all the traffic.
Even access distribution. Cardinality alone isn’t enough. A million distinct tenantId values still produce a hot partition if one enterprise tenant generates 60% of requests. Consider the shape of your traffic, not just the shape of your data.
Available at query time. The caller must have the value. A partition key derived from data the caller doesn’t possess forces an extra lookup or a GSI.
Stable. Partition keys are immutable. Changing one means delete-and-recreate, which is not a transaction unless you make it one. Never use a mutable business value like an email address or a display name.
Partition key overloading
In single-table design, one partition key attribute holds different value patterns for different entity types. The attribute is generically named pk, and the prefix carries the type:
| pk value | Entity |
|---|---|
TENANT#t_01 | Tenant, and everything scoped to it |
ORDER#01HVMK3P2Q | Order, and its line items |
USER_EMAIL#alice@acme.com | GSI lookup for login |
The prefixes must never collide across entity types. This is why the TYPE#value convention exists: it makes collision essentially impossible and makes the key self-documenting in the console.
Common mistakes
Low cardinality. pk: STATUS#pending seems reasonable until every pending order in the system is on one partition. This is the most frequent cause of throttling on an otherwise healthy table. Fix with write sharding or a better key.
Sequential or time-based values. pk: 2026-08-05 concentrates every write for the day on one partition, then moves to a new one at midnight. High cardinality, terrible distribution. Time-based partition keys are almost always wrong unless combined with a shard suffix.
Using it as a filter. pk: TENANT#t_01#ACTIVE splits a tenant across partitions by status and breaks the co-location that made the tenant partition useful. Status belongs in the sort key or a sparse index, not the partition key.
Assuming you can add one later. You cannot change a table’s key schema after creation. Changing the partition key means creating a new table and migrating every item.
Forgetting GSI partition keys have the same rules. A GSI partition key needs cardinality and even distribution just as much as the base table’s. A static GSI partition key like TENANT_LIST is a deliberate hot key, acceptable only when read volume is genuinely low.
Related terms
- Sort Key — the second key component, which supports range queries
- Composite Key — partition key plus sort key together
- Hot Partition — what a bad partition key produces
- Write Sharding — the mitigation when you can’t avoid a low-cardinality key
Partition key mistakes are expensive because they’re discovered in production and fixed with migrations. I’m building singletable.dev to make key structure and its consequences visible while you’re still designing.