DynamoDB sort keys are the secret sauce that lets you query data in ranges and order it efficiently, but their true power lies in how they organize your entire table, not just individual items.

Let’s see this in action. Imagine a GameScores table. We want to retrieve all scores for a specific player, sorted by their score, and then find all players who scored above a certain threshold in a particular game.

Here’s how our table and a query might look:

Table Schema:

  • Partition Key: gameId (String)
  • Sort Key: score (Number)
  • Attributes: playerId (String)

Example Data:

gameId score playerId
game-123 5000 player-A
game-123 7500 player-B
game-123 6200 player-C
game-456 4000 player-A
game-456 8000 player-D
game-456 5500 player-E

Query 1: Get all scores for game-123, sorted by score (descending).

{
    "TableName": "GameScores",
    "KeyConditionExpression": "gameId = :gid",
    "ExpressionAttributeValues": {
        ":gid": {"S": "game-123"}
    },
    "ScanIndexForward": false
}

Result:

gameId score playerId
game-123 7500 player-B
game-123 6200 player-C
game-123 5000 player-A

Query 2: Get all scores for game-456 greater than or equal to 5000.

{
    "TableName": "GameScores",
    "KeyConditionExpression": "gameId = :gid AND score >= :s",
    "ExpressionAttributeValues": {
        ":gid": {"S": "game-456"},
        ":s": {"N": "5000"}
    }
}

Result:

gameId score playerId
game-456 8000 player-D
game-456 5500 player-E

The gameId (partition key) groups all items for a specific game together. Within that group, the score (sort key) orders these items numerically. When you query, DynamoDB first finds all items with the matching gameId. Then, it efficiently scans only that subset of items based on the sort key condition (score >= :s or ScanIndexForward: false). This is incredibly efficient because DynamoDB doesn’t have to scan the entire table.

The real magic happens when you design your sort key to represent relationships or hierarchies. For instance, you could use a composite sort key to store multiple types of data within a single partition.

Consider a UserProfile table.

  • Partition Key: userId (String)
  • Sort Key: entityType_entityId (String, e.g., "POST#12345", "COMMENT#67890", "FOLLOWERS#XYZ")
  • Attributes: content, timestamp, followerId

With this schema, all data related to a userId is in one partition. You can then use sort key prefixes to fetch specific types of related data:

  • Fetch all posts by a user: entityType_entityId BETWEEN "POST#" AND "POST#zzzzzz"
  • Fetch all comments on a specific post: entityType_entityId STARTS_WITH "COMMENT#POSTID_" (if you structured your entityId to include the parent post ID)
  • Fetch recent activity (posts and comments): entityType_entityId < "FOLLOWERS#" (assuming followers are added last)

The internal structure of a DynamoDB partition is a sorted list of all items belonging to that partition key. The sort key dictates the order of items within this list. When you perform a query with a sort key condition, DynamoDB navigates this sorted list directly, using binary search principles to find the starting point and then scanning sequentially until the condition is no longer met. This is why range queries and sorting on the sort key are so fast – it’s an optimized traversal of an already ordered data structure.

Most people think of sort keys for simple ordering, like scores or timestamps. The counterintuitive power is realizing you can embed complex relational data and filter it using string prefixes or range conditions on a single composite sort key attribute, effectively creating "edges" in a graph-like structure within a single partition. This allows you to retrieve highly related data with a single query, avoiding expensive joins or multiple round trips.

The next hurdle is understanding how to effectively use Global Secondary Indexes (GSIs) with sort keys to query across different partition key values.

Want structured learning?

Take the full Dynamodb course →