DynamoDB composite keys are the secret sauce that lets you query your data in ways that seem impossible for a NoSQL database, like finding all orders for a specific customer and filtering them by date, all in a single, efficient request.
Let’s see this in action. Imagine a ProductCatalog table where we want to find all products within a specific category, and for each product, list its available variants.
// Sample data in ProductCatalog table
{
"PK": "CATEGORY#Electronics",
"SK": "PRODUCT#TV-LG-55",
"productName": "LG 55-inch 4K Smart TV",
"brand": "LG",
"price": 799.99
}
{
"PK": "CATEGORY#Electronics",
"SK": "PRODUCT#TV-LG-55#VARIANT#Black",
"color": "Black",
"stock": 15
}
{
"PK": "CATEGORY#Electronics",
"SK": "PRODUCT#TV-LG-55#VARIANT#Silver",
"color": "Silver",
"stock": 8
}
{
"PK": "CATEGORY#Electronics",
"SK": "PRODUCT#LAPTOP-DELL-XPS",
"productName": "Dell XPS 13 Laptop",
"brand": "Dell",
"price": 1299.00
}
{
"PK": "CATEGORY#Electronics",
"SK": "PRODUCT#LAPTOP-DELL-XPS#VARIANT#Silver",
"color": "Silver",
"stock": 25
}
Here, PK (Partition Key) is CATEGORY#Electronics for all items related to electronics. SK (Sort Key) is used to differentiate items within that category. Notice the pattern: PRODUCT#<productId> for the main product details and PRODUCT#<productId>#VARIANT#<variantId> for variant-specific information.
A common query is to get all product details and their variants for a given category. This is a Query operation using PK = "CATEGORY#Electronics" and a begins_with condition on SK for PRODUCT#:
// Example Query Request
{
"TableName": "ProductCatalog",
"KeyConditionExpression": "PK = :pk AND begins_with(SK, :sk_prefix)",
"ExpressionAttributeValues": {
":pk": {"S": "CATEGORY#Electronics"},
":sk_prefix": {"S": "PRODUCT#"}
}
}
This single query efficiently retrieves all top-level product entries and their associated variant entries, thanks to the composite key structure.
The fundamental problem composite keys solve is enabling efficient, multi-dimensional access to data within a single DynamoDB table. Without them, you’d often need multiple tables or secondary indexes for similar query patterns, increasing complexity and cost. The PK determines the physical partition where data resides, and the SK allows for ordered retrieval and filtering within that partition.
Internally, DynamoDB stores items with the same PK on the same partition (or across partitions if the partition grows very large, but the logical grouping remains). The SK is then used to sort these items on disk within that partition. This allows Query operations to be highly efficient because they only need to scan a specific partition and can use the sorted SK to quickly locate the desired items.
You control this behavior by carefully designing the string patterns for your PK and SK. Common patterns include:
- Hierarchical Data:
ENTITY_TYPE#<id>forPK,SUB_ENTITY#<id>forSK(as seen in the product example). - Adjacency Lists:
USER#<userId>forPK,FRIEND#<friendId>forSKto find friends of a user. OrUSER#<userId>forPK,SENT#<timestamp>forSKto find messages sent by a user. - Time Series:
DEVICE#<deviceId>forPK,TIMESTAMP#<timestamp>forSKto retrieve sensor readings for a device in chronological order.
The trick is to ensure that all items you’d typically query together share the same PK and have SK values that allow for precise filtering. The SK can be a single value, a structured string (like ID1#ID2#timestamp), or even a combination of attributes.
When designing your composite keys, think about your most frequent access patterns. If you often need to find all items of a certain type and then filter by a secondary attribute (like a date range or a status), a composite key is your answer. The PK would represent the primary entity type, and the SK would incorporate the secondary attribute, often with a prefix to distinguish different item types within the same PK. For example, PK: ORDER#123, SK: DETAIL#SKU-ABC for order item details and SK: SHIPPING#2023-10-27 for shipping information.
The sort key can be a surprisingly powerful tool for denormalization. You can store different types of related data under the same PK by using distinct prefixes in your SK. This allows you to retrieve an aggregate view of related information in a single Query operation, rather than performing multiple lookups or relying on complex secondary indexes. For instance, a PK of CUSTOMER#100 could have SKs like PROFILE, ORDERS#2023-10-26, ORDERS#2023-10-25, and PAYMENT_METHODS. A query for PK=CUSTOMER#100 and begins_with(SK, "ORDERS#") would fetch all orders for that customer efficiently.
The next step after mastering composite keys is understanding Global Secondary Indexes (GSIs) and how they leverage composite keys themselves to provide alternative access patterns to your data.