Skip to content
AWS aws databases 5 min read

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 typeMade ofUniqueness ruleQuery power
Simple primary keyPartition key onlyThe partition key value must be uniqueFetch one item by its exact key
Composite primary keyPartition key + sort keyThe 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 (only ACTIVE/INACTIVE) or CountryCode is a classic hot-partition trap. A handful of values means a handful of partitions soak up everything. Prefer high-cardinality keys such as UserId, OrderId, or DeviceId.

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 Users table keyed by UserId.
  • 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 UserId partition key with an OrderDate sort 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

  1. Open the DynamoDB console and choose TablesCreate table.
  2. Enter a Table name, for example AppData.
  3. Under Partition key, type UserId and choose type String.
  4. Tick Add sort key, type SK (or a descriptive name like OrderDate), and choose String.
  5. 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-1 is 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 ThrottledRequests and 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.
Last updated June 15, 2026
Was this helpful?