Skip to content

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 AND between the PK and SK conditions (you cannot use only a SK condition without a PK)
  • Using sk = :prefix instead of begins_with(sk, :prefix) (these are different operations)
  • Mixing up pk and sk attribute 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

  1. Print the actual stored SK. Run a GetItem on a known item and inspect the sk attribute value exactly. What you see is what you need to match as a prefix.
  2. Check delimiter. ORDER vs ORDER# are different strings.
  3. Check capitalisation. DynamoDB string comparison is case-sensitive. order# does not match ORDER#.
  4. Check the partition key. If PK doesn’t match, no SK filtering happens at all. You get zero results regardless.
  5. 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.

Tejovanth N

Tejovanth builds on DynamoDB in production: rasika.life, rekha.app, rrmstays. All single-table with ElectroDB.

LinkedIn codeculturecob.com

Related

Production issue

You're likely losing money on this in production.

A wrong partition key or missing GSI is a live cost problem. Get a DynamoDB schema review before your next deploy — async, fixed price, 5 business days.