Pre-Joining Data on Disk
In a relational database, a JOIN is a runtime operation: the database engine scans two tables and combines matching rows in memory. At small scale this is fine. At millions of rows per second, it becomes a bottleneck — you're doing expensive computation on every read.
DynamoDB's answer is to eliminate the join entirely by storing related data together at write time. The "join" happens once when you write the data, not on every read. This trades write complexity for read simplicity and speed.
The Adjacency List Pattern
An adjacency list stores an entity and all its related entities under the same Partition Key. The Sort Key distinguishes the entity type.
Example: Blog application
PK: USER#101 SK: PROFILE Name: "Alice", Email: "[email protected]"
PK: USER#101 SK: POST#2024-001 Title: "DynamoDB Tips", Views: 1200
PK: USER#101 SK: POST#2024-002 Title: "AWS Lambda Guide", Views: 800
PK: USER#101 SK: FOLLOW#USER#55 FollowedAt: "2024-03-01"
The Query:
Query(
TableName: "AppTable",
KeyConditionExpression: "PK = :pk",
ExpressionAttributeValues: { ":pk": "USER#101" }
)
This single operation returns Alice's profile, all her posts, and all her follows — from one physical location on disk. No network hops between tables. No in-memory joins.
Filtering Within an Item Collection
Use begins_with on the Sort Key to retrieve only a subset of related items:
# Get only Alice's posts
Query WHERE PK = "USER#101" AND SK begins_with "POST#"
# Get only Alice's follows
Query WHERE PK = "USER#101" AND SK begins_with "FOLLOW#"
The architectural implication: Single-table design makes reads extremely fast and cheap, but it requires careful upfront planning. Adding a new access pattern after launch may require backfilling data or adding a GSI — neither is trivial at scale. This is why the access pattern enumeration step is non-negotiable.