Beginner7 min read

DynamoDB Composite Primary Key: Partition + Sort Key Explained

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).

The items collapse into one 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.

What that window costs

Ten readings at 2 KB each in the partition sum to 20 KB metered. DynamoDB rounds reads up per 4 KB block, so an eventually-consistent Query on that window costs 3 read request units (20 KB → five 4 KB blocks × 0.5 RCU each). A strongly-consistent read on the same window costs 5 units (one RCU per block). Fetch the same ten rows with ten separate GetItem calls and the minimum-one-block rule applies per item → 5 units eventually consistent, 10 strongly consistent — before counting extra round trips.

Read patternItemsData touchedEC read units
One Query, readingTs between start and end1020 KB3
10 × GetItem on full composite key1020 KB5
Query partition only, filter humidity in app1020 KB3
Scan table, filter deviceId + time windowallwhole tabletable-sized

Pass ReturnConsumedCapacity: TOTAL while prototyping; the pricing calculator turns that unit count into a monthly line item once you know requests per second.

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 is the only lever for range reads, so shape it to your queries.

  • Use a sortable timestamp. ISO-8601 strings or epoch numbers sort correctly; raw localized dates don't.
  • Prefix it for one-to-many . 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 .

A partition key with only fifty distinct values on a 10,000-write-per-second stream concentrates ~200 WCU per logical partition before DynamoDB splits — fine at prototype scale, painful at production scale. Prefer identifiers that grow with your fleet (device id, tenant id, session id) over coarse groupings unless you deliberately colocate a bounded dataset.

When a composite key bites you

Composite keys are a commitment. 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.

In DynoTable, open the table browser and inspect a composite-key table side by side with its GSIs — sort order on the sort key is visible row-by-row, which makes off-by-one timestamp formats obvious before they reach production.

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, emit the full paginated Query in the query builder, then try DynoTable to browse your real partitions and watch the sort order line up against your own tables.

Updated