Ordered Key Sharding in DynamoDB

genericlemon241 pts0 comments

Ordered key sharding in DynamoDB - death and gravity

Ordered key sharding in DynamoDB

June 2026<br>∙ eight minute read

PyCoder's Weekly<br>HN<br>Bluesky<br>Reddit<br>--><br>linkedin<br>Twitter

So, you want to keep a sorted index in DynamoDB,<br>but for whatever reason<br>– usually throughput-related –<br>it won't fit on a single partition . What do you do?

Today, we look at the available solutions,<br>do the math, and find out which is best.

Tip

This worked example is part of my DynamoDB crash course series.

Contents

Requirements

A sparse index is almost enough

But scan results are not ordered

But a single partition key causes throttling

But random suffixes are random

But hash suffixes are not ordered

But there are a lot of first characters

But some first bytes need multiple shards

But tries and prefix ranges are complicated

But the prefix distribution can change

Requirements #

Say you're using single table design<br>with a table of artists, albums, and songs.1

You keep an artist's items in a single collection<br>(aka same partition key),<br>and use sort keys artist, album#{Album}, and song#{Album}#{Song},<br>depending on their type:

# table Music (partition key: Artist, sort key: sk)<br>Solar Fields: !btree<br>'album#Leaving Home': { Album: Leaving Home, ... }<br>'artist': { ... }<br>'song#Leaving Home#Air Song': { ... }<br>'song#Leaving Home#Monogram': { ... }

To list albums without doing a full table scan,<br>you need a global secondary index.

Let's come up with some reasonable requirements; the GSI should support:

items up 500 bytes (we project additional attributes besides the keys)

10,000 queries/second, max 100 items/query, sorted alphabetically<br>list all albums

list albums by title

10,000 writes/second (to avoid write throttling during imports)

A sparse index is almost enough #

One way to do it is to use a dedicated<br>sparse index,<br>taking advantage of the fact that<br>items with missing index keys don't appear in the index.

If only albums have an Album attribute, we just create a new GSI:

# GSI Albums (partition key: Album, sort key: Artist)<br>Leaving Home: !btree<br>International Pony: { sk: 'album#Leaving Home', ... }<br>Solar Fields: { sk: 'album#Leaving Home', ... }<br>Heavy Migration: !btree<br>Dday One: { sk: 'album#Heavy Migration', ...}

If songs have an Album too,<br>we add a dedicated AlbumsPK attribute instead.

In many ways, this is the ideal solution.<br>To list all albums, we scan the index.<br>To list albums by title, we query an index partition key.<br>We have lots of unique partition keys<br>with items spread pretty evenly across them,<br>which should prevent throttling.

But scan results are not ordered #

...except scan results are not ordered,<br>so we're missing the sorted alphabetically part.

What is ordered are sort keys,<br>so we can use a single index collection instead:

# GSI GSI1 (partition key: gsi1pk, sort key: gsi1sk)<br>'albums': !btree<br>Heavy Migration: { Artist: Dday One, sk: 'album#Heavy Migration', ... }<br>Leaving Home: { Artist: Solar Fields, sk: 'album#Leaving Home', ... }<br>Leaving Home: { Artist: International Pony, sk: 'album#Leaving Home', ... }

This is also seemingly ideal.<br>To list all albums, we query the entire index partition key.<br>To list albums by title, we use a sort key.<br>The results are sorted as required,<br>and there's no limit on the number of items in a collection.

But a single partition key causes throttling #

However, there are<br>per-partition limits of<br>24 MB/s for reads and<br>1 MB/s for writes.

Let's see how they compare to our requirements:

reads: 500 bytes/item * 10k queries/s * 100 items/query = 500 MB/s (~21x )

writes: 500 bytes/item * 10k items/s = 5 MB/s (5x )

Uh-oh, turns out we need 21 times the throughput one partition can deliver.

One way to spread the load is<br>sharding,<br>using multiple synthetic partition keys of the form album#{shard_id}.<br>A common option for the shard id is a random number from a known range,<br>e.g. album#{randrange(21)}:

# GSI GSI1 (partition key: gsi1pk, sort key: gsi1sk)<br>'album#1': !btree<br>Leaving Home: { Artist: Solar Fields, ... }<br>'album#12': !btree<br>Heavy Migration: { Artist: Dday One, ... }<br>'album#20': !btree<br>Leaving Home: { Artist: International Pony, ... }

To list all albums, query each shard in turn:

for shard in range(21):<br>for item in dynamodb.query(f"album#{shard}"):<br>yield item

But random suffixes are random #

There's a problem, though –<br>with random shard ids we can't easily list albums by title,<br>since albums with the same title may end up on any shard.

A better option is to calculate the shard id<br>from the album title using a hash function:

def hash(s):<br>return int.from_bytes(sha256(s.encode()).digest())

def album_shard_id(album_title):<br>return hash(album_title) % 21

# GSI GSI1 (partition key: gsi1pk, sort key: gsi1sk)<br>'album#6': !btree<br>Leaving Home: { Artist: Solar Fields, ... }<br>Leaving Home: { Artist: International Pony, ... }<br>'album#8': !btree<br>Heavy Migration: { Artist: Dday One, ... }

To list albums by title:

dynamodb.query(f"album#{album_shard_id(album_title)}",...

album partition albums leaving home artist

Related Articles