DynamoDB Composite Sort Key begins_with Not Working: Fix It
begins_with on a composite sort key returns zero results even though you know the data is there. This is almost always a template format mismatch. The string you’re querying with doesn’t match the string that was stored.
The most common cause: delimiter mismatch
You stored items with:
sk = "ORDER#01HV..."
But you’re querying with:
KeyConditionExpression: 'sk begins_with :prefix',
ExpressionAttributeValues: { ':prefix': 'ORDER' } // missing the # delimiter
Result: zero matches. DynamoDB begins_with is an exact string prefix. 'ORDER' does not match 'ORDER#01HV...'. It only matches strings that literally start with 'ORDER', not 'ORDER#'.
Fix:
ExpressionAttributeValues: { ':prefix': 'ORDER#' } // include the delimiter
ElectroDB: using begins_with correctly
In ElectroDB, you don’t write begins_with directly. You use .begins():
// Query all orders within a customer partition
const { data } = await OrderEntity.query
.byCustomer({ customerId: "cust_01" })
.begins({ orderId: "" }) // begins with ORDER# prefix
.go()
One catch: this still might not work if you’re not starting the composite correctly. In ElectroDB, .begins() takes the leading composite attributes of the sort key. If your SK template is ORDER#${orderId}, then .begins({ orderId: "" }) queries for ORDER# (because it renders the template with an empty orderId). This is correct.
The error people hit: passing the sort key prefix string directly instead of the entity attribute:
// WRONG: passing raw string
.begins('ORDER#')
// RIGHT: passing composite attributes
.begins({ orderId: "" })
ElectroDB renders the SK template from the composite attributes. Pass the attributes, not the rendered string.
Raw SDK: the full correct query
await dynamodb.query({
TableName: 'MyTable',
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
ExpressionAttributeValues: {
':pk': { S: 'CUSTOMER#cust_01' },
':prefix': { S: 'ORDER#' }, // note the # delimiter and exact capitalisation
},
}).promise()
Common mistakes in this exact call:
- Missing the
ANDbetween the PK and SK conditions (you cannot use only a SK condition without a PK) - Using
sk = :prefixinstead ofbegins_with(sk, :prefix)(these are different operations) - Mixing up
pkandskattribute names if your table uses different names (e.g.,PK,SK,hash_key)
The begins_with condition requires a known partition key
begins_with on a sort key always requires an exact partition key match. You cannot use begins_with across all partitions. That would be a scan. The KeyConditionExpression must always include pk = :value.
If you’re trying to find all items of a certain type across all partitions (e.g., all Order records in a single-table design regardless of customer), that requires a GSI, not a begins_with query.
Debugging checklist
- Print the actual stored SK. Run a
GetItemon a known item and inspect theskattribute value exactly. What you see is what you need to match as a prefix. - Check delimiter.
ORDERvsORDER#are different strings. - Check capitalisation. DynamoDB string comparison is case-sensitive.
order#does not matchORDER#. - Check the partition key. If PK doesn’t match, no SK filtering happens at all. You get zero results regardless.
- Verify the table/index. If you’re querying a GSI, confirm the item actually has the GSI key attributes set. Items without GSI keys don’t appear in the GSI.
Why this is especially common in single-table design
Single-table design uses composite sort key prefixes extensively: ORDER#<id>, MEMBER#<id>, PROJECT#<id>. This is exactly where begins_with earns its keep. Getting the delimiter and capitalisation exactly right matters. A common pattern to avoid mistakes:
// Define prefix constants to reuse across writes and queries
const PREFIX = {
ORDER: 'ORDER#',
MEMBER: 'MEMBER#',
META: '#METADATA',
} as const
// Write
{ pk: `CUSTOMER#${customerId}`, sk: `${PREFIX.ORDER}${orderId}` }
// Query
KeyConditionExpression: 'pk = :pk AND begins_with(sk, :prefix)',
ExpressionAttributeValues: {
':pk': `CUSTOMER#${customerId}`,
':prefix': PREFIX.ORDER, // guaranteed to match, same constant
}
Defining prefix constants once and using them in both writes and queries eliminates the category of mismatch error entirely.
The E-Commerce Orders pattern uses composite sort keys with begins_with for AP2 (all orders for a customer) and AP4 (all items in an order). See how the key templates are defined and queried consistently.