Beginner6 min read

DynamoDB Composite Primary Key

A composite primary key is two attributes: a partition key and a sort key. The partition key decides where an item lives; the sort key orders items inside that partition.

Coming from SQL, think of it less as a unique id column and more as GROUP BY partition, ORDER BY sort baked into the table itself.

What is a DynamoDB composite primary key?

A DynamoDB composite primary key combines two attributes: a partition key and a sort key. The partition key decides which physical partition an item lives in; the sort key orders items inside that partition. Together they form the item's unique identity and let a single Query return a sorted range instead of one item.

  • Two parts, two jobs. The partition key routes the item to a physical partition; the sort key orders every item that shares that partition key.
  • Uniqueness is the pair. Two items can share a partition key value as long as their sort keys differ — that's how one partition holds many rows.
  • The sort key is the whole point. It's what lets a Query return a range (>=, between, begins_with) instead of one item, with no Scan.
  • Keys must be scalars. Partition and sort keys can only be string, number, or binary — no maps, no lists (AWS docs).

Simple key vs composite key

A simple primary key is just a partition key. It uniquely identifies an item, and you read it back with GetItem. That's it — no range reads, no "give me the newest N".

A composite key adds the sort key, and that single addition is what makes DynamoDB feel like a database instead of a hash map.

Simple keyComposite key
AttributesPartition key onlyPartition key + sort key
UniquenessPartition key valueThe pair of values
Multiple items per partitionNoYes
Query a rangeNo (GetItem only)Yes (begins_with, between, >)
Natural fitLookup by idTime series, one-to-many, history

Model a sensor-readings table

Say you collect temperature samples from a fleet of field sensors. The access pattern is "get the readings for one device, newest first, within a time window". That's a textbook composite key.

Use the device id as the partition key and the reading timestamp as the sort key:

deviceIdreadingTstempChumidity
DEV#a1b22026-06-23T08:00:00Z21.448
DEV#a1b22026-06-23T08:05:00Z21.747
DEV#a1b22026-06-23T08:10:00Z22.146
DEV#c9d82026-06-23T08:00:00Z19.855

All three DEV#a1b2 readings land in the same partition, physically stored together and sorted by readingTs.

AWS calls the partition key the hash attribute and the sort key the range attribute — the sort key is a range you can scan within (AWS docs).

Here's how the items collapse into one item collection under each partition key:

Partition: DEV#a1b2readingTs 08:00readingTs 08:05readingTs 08:10Query deviceId = DEV#a1b2

One Query against the partition key reads every reading for that device, already in timestamp order — no sorting on the client, no second round trip.

Query the range, don't scan it

Because readingTs is an ISO-8601 string, it sorts lexicographically the same way it sorts chronologically. So a time-window read is a key-condition range, not a filter:

Query
deviceId  = "DEV#a1b2"
readingTs BETWEEN "2026-06-23T08:00:00Z" AND "2026-06-23T08:10:00Z"

That's a KeyConditionExpression — it narrows the read before DynamoDB returns data, so you pay only for the items in the window. A FilterExpression runs after the read and bills you for everything it scanned through; that's the Scan footgun in miniature.

The expression itself, with placeholders and typed values, is fiddly to write by hand. Build it visually with the DynamoDB Expression Builder and copy the exact KeyConditionExpression into your SDK call.

Design the sort key on purpose

The sort key isn't free metadata — it's the only lever for range reads, so shape it to your queries.

  • Use a sortable timestamp. ISO-8601 strings or zero-padded epoch numbers sort correctly; raw localised dates don't.
  • Prefix it for one-to-many overloading. A sort key like READING#2026-06-23T08:00:00Z lets you mix entity types under one partition and slice them with begins_with. That's the seam into single-table design.
  • Put the high-cardinality dimension in the partition key. Sensor id has thousands of values, so it spreads writes evenly. A low-cardinality partition key (say, region) creates a hot partition.

When a composite key bites you

It's a commitment, not a convenience. The trap: you pick a partition key, ship, then discover an access pattern that needs a different grouping — "all readings above 30°C across the whole fleet".

The base table can't answer that; the partition key is fixed. Your options are a global secondary index with a different key, or restructuring.

Enumerate your reads before you commit the key schema. Changing a primary key means a table migration, not an ALTER TABLE.

Next steps

Composite keys are the foundation under item collections, one-to-many relationships, and most useful index designs — read single-table design and GSI vs LSI next to see where they lead.

Sketch your KeyConditionExpression in the DynamoDB Expression Builder, then try DynoTable to browse your real partitions and watch the sort order line up against your own tables.

Updated