DEV Community

Cover image for DynamoDB Composite Primary Key
DynoTable
DynoTable

Posted on • Originally published at dynotable.com

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 key Composite key
Attributes Partition key only Partition key + sort key
Uniqueness Partition key value The pair of values
Multiple items per partition No Yes
Query a range No (GetItem only) Yes (begins_with, between, >)
Natural fit Lookup by id Time 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:

deviceId readingTs tempC humidity
DEV#a1b2 2026-06-23T08:00:00Z 21.4 48
DEV#a1b2 2026-06-23T08:05:00Z 21.7 47
DEV#a1b2 2026-06-23T08:10:00Z 22.1 46
DEV#c9d8 2026-06-23T08:00:00Z 19.8 55

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:

DynamoDB Composite Primary Key

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:

Example Notes
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.

Warning

The partition key in a Query must be an exact equality (=). Only the sort
key
accepts ranges. You can't ask for "all readings across every device after
8am" on the base table — that needs a different key design or a secondary index.

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

Top comments (0)