DynamoDB Partition & Sort Keys
Every item you store in Amazon DynamoDB (a fully managed NoSQL key-value and document database) needs a primary key that uniquely identifies it. That key is not just a label — it decides where your data physically lives and how fast you can read it back. Getting the key design right is the single most important decision you make in DynamoDB, because the database has no “add an index later and forget about it” safety net like a relational database does. This page explains the two parts of a DynamoDB primary key and how to choose them well.
The two kinds of primary key
DynamoDB gives you two primary key shapes. You pick one when you create the table, and you cannot change it afterward.
| Key type | Made of | Uniqueness rule | Query power |
|---|---|---|---|
| Simple primary key | Partition key only | The partition key value must be unique | Fetch one item by its exact key |
| Composite primary key | Partition key + sort key | The combination must be unique (the same partition key can repeat) | Fetch one item, or a range of items that share a partition key |
A partition key (sometimes called the hash key) is a single attribute. DynamoDB runs its value through an internal hash function to decide which physical storage partition holds the item. A sort key (sometimes called the range key) is an optional second attribute. Items that share a partition key are stored together, physically sorted by the sort key. That group is called an item collection.
Why the partition key controls performance
DynamoDB spreads your table across many physical partitions behind the scenes. The partition key decides which partition an item lands on. If your partition key has lots of distinct, evenly-used values, traffic spreads out smoothly across partitions. If most requests hammer a single key value, all that traffic lands on one partition — a hot partition.
A hot partition gets throttled (requests rejected with a ProvisionedThroughputExceededException) even when the table as a whole has plenty of capacity. This surprises people: the dashboard shows 90% of capacity unused, yet requests fail. The fix is almost always a better partition key.
Gotcha: A low-cardinality partition key like
Status(onlyACTIVE/INACTIVE) orCountryCodeis a classic hot-partition trap. A handful of values means a handful of partitions soak up everything. Prefer high-cardinality keys such asUserId,OrderId, orDeviceId.
When to use a simple vs composite key
- Use a simple (partition-only) key when each item is independent and you always look it up by its full ID. Example: a
Userstable keyed byUserId. - Use a composite (partition + sort) key when you have one-to-many relationships and want to fetch many related items in one query. Example: a
UserIdpartition key with anOrderDatesort key lets you read all of one user’s orders, newest first, in a single request. - Avoid cramming a one-to-many relationship into a simple key by string-concatenating IDs — you lose the ability to query ranges.
Modeling one-to-many with the sort key
The sort key is what makes DynamoDB powerful beyond a plain key-value store. Because items in a collection are stored sorted, you can run a Query with conditions like begins_with, between, >, or < on the sort key.
A common pattern is to put an entity type prefix in the sort key:
PK (UserId) SK
USER#42 PROFILE#42
USER#42 ORDER#2026-01-15
USER#42 ORDER#2026-03-02
USER#42 ADDRESS#home
Now Query where PK = USER#42 AND begins_with(SK, "ORDER#") returns just that user’s orders, already in date order — no scan, no filter on the whole table.
Creating a table with a composite key
Console steps
- Open the DynamoDB console and choose Tables → Create table.
- Enter a Table name, for example
AppData. - Under Partition key, type
UserIdand choose type String. - Tick Add sort key, type
SK(or a descriptive name likeOrderDate), and choose String. - Leave Default settings (on-demand capacity) for now, then choose Create table.
AWS CLI
aws dynamodb create-table \
--table-name AppData \
--attribute-definitions \
AttributeName=UserId,AttributeType=S \
AttributeName=SK,AttributeType=S \
--key-schema \
AttributeName=UserId,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST
Output:
{
"TableDescription": {
"TableName": "AppData",
"KeySchema": [
{ "AttributeName": "UserId", "KeyType": "HASH" },
{ "AttributeName": "SK", "KeyType": "RANGE" }
],
"TableStatus": "CREATING",
"TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/AppData"
}
}
KeyType=HASH is the partition key; KeyType=RANGE is the sort key. The attribute types are S (string), N (number), or B (binary).
Querying the item collection
aws dynamodb query \
--table-name AppData \
--key-condition-expression "UserId = :u AND begins_with(SK, :prefix)" \
--expression-attribute-values '{":u":{"S":"USER#42"},":prefix":{"S":"ORDER#"}}'
Output:
{
"Items": [
{ "UserId": {"S":"USER#42"}, "SK": {"S":"ORDER#2026-01-15"}, "Total": {"N":"49.99"} },
{ "UserId": {"S":"USER#42"}, "SK": {"S":"ORDER#2026-03-02"}, "Total": {"N":"12.50"} }
],
"Count": 2,
"ScannedCount": 2
}
Infrastructure as code (CloudFormation)
Resources:
AppDataTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: AppData
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: UserId
AttributeType: S
- AttributeName: SK
AttributeType: S
KeySchema:
- AttributeName: UserId
KeyType: HASH
- AttributeName: SK
KeyType: RANGE
Cost tip: Key design itself is free, but it directly drives your bill. On-demand pricing in
us-east-1is roughly $1.25 per million writes and $0.25 per million reads (2026). A hot partition forces you to over-provision or eats retries, so a good key is also a cheaper key.
Best practices
- Choose a high-cardinality partition key (many distinct values) so traffic spreads evenly across partitions.
- Match the partition key to how you read data — the value you always know at query time is usually the right partition key.
- Use the sort key to model one-to-many relationships and entity prefixes (
ORDER#,PROFILE#) so one query returns related items. - Watch the
ThrottledRequestsand per-partition metrics in CloudWatch to catch hot partitions early. - If a naturally low-cardinality value must be the partition key, add a write-sharding suffix (e.g.
STATUS#ACTIVE#3) to spread load across more partitions. - Decide simple vs composite before creating the table — you cannot change the key schema later without rebuilding the table.