DynamoDB Data Modeling
Complete the full lesson to earn 25 points
Work through each section, then tap “Mark as Complete” on the last one.
✦ Skip the page breaks and see fewer ads — read each lesson on a single page with Pro
DynamoDB Data Modeling: Mastering NoSQL Schema Design
Introduction: Why Data Modeling Matters in DynamoDB
When moving from a traditional relational database (RDBMS) like MySQL or PostgreSQL to Amazon DynamoDB, many developers make the mistake of attempting to replicate their existing normalized schemas. In a relational database, you define your tables first based on the entities in your application, and then you figure out how to query them using joins. In DynamoDB, the philosophy is reversed: you must define your access patterns first and then design your table structure to support those specific queries.
DynamoDB is a non-relational, key-value store that is built for massive scale and predictable performance. Because it does not support complex joins, subqueries, or cross-table transactions in the way SQL does, the way you arrange your data on disk is the single most important factor in your application’s performance and cost. If you model your data poorly, you will find yourself performing expensive "scan" operations that read every item in your table, which kills performance and inflates your AWS bill.
This lesson is designed to take you from a basic understanding of key-value storage to an expert-level grasp of single-table design, secondary indexes, and efficient access patterns. We will explore how to think about data in terms of "collections" rather than "tables" and how to map your application’s requirements directly onto the DynamoDB storage engine.
The Fundamentals of DynamoDB Schema Design
To understand DynamoDB modeling, you must first understand the core components of a DynamoDB item: the Partition Key (PK) and the Sort Key (SK). Together, these form the Primary Key of your table.
The Partition Key (PK)
The Partition Key is the hash attribute. DynamoDB uses the value of this attribute to determine which physical partition—or "bucket"—of data your item resides in. When you perform a GetItem or Query request, you must provide the Partition Key to allow DynamoDB to route your request to the correct server. If you omit the PK, the database has no idea where to look, forcing a full table scan.
The Sort Key (SK)
The Sort Key is optional, but it is essential for complex modeling. If you include a Sort Key, DynamoDB stores items with the same Partition Key physically together, sorted by the Sort Key value. This allows you to perform range queries—for example, "give me all orders for UserID 123 where the timestamp is between Monday and Friday." Without a Sort Key, you are limited to fetching a single item by its Partition Key.
Callout: The "Single-Table Design" Philosophy In traditional SQL modeling, you create a table for every entity (e.g., Users, Orders, Products). In DynamoDB, the industry standard is to use a "Single-Table Design." This involves putting all your different entity types into one large table. You distinguish between them using prefixes (e.g.,
USER#123for a user record andORDER#456for an order record). This allows you to retrieve an entire tree of related data in a single request, drastically reducing latency.
Step-by-Step: Designing for Access Patterns
Let’s walk through a real-world scenario: an E-commerce platform. We need to support the following access patterns:
- Retrieve a user's profile.
- Retrieve all orders placed by a specific user.
- Retrieve the details of a specific order.
Step 1: Identify Entities and Attributes
First, we list our entities:
- User:
UserId,Name,Email,JoinedDate - Order:
OrderId,UserId,TotalAmount,Status,OrderDate
Step 2: Define the Primary Key Structure
We will use a generic PK and SK naming convention. This is a best practice because it allows us to store different entity types in the same table.
| Entity | Partition Key (PK) | Sort Key (SK) |
|---|---|---|
| User Profile | USER#<UserId> |
METADATA#<UserId> |
| Order Header | USER#<UserId> |
ORDER#<OrderId> |
Step 3: Mapping the Access Patterns
Pattern 1: Get User Profile
- Query:
PK = 'USER#123',SK = 'METADATA#123' - This returns the user record directly.
- Query:
Pattern 2: Get All Orders for a User
- Query:
PK = 'USER#123',SK begins_with 'ORDER#' - Because all orders for a specific user share the same PK and are grouped by the SK, this query retrieves every order for that user in one efficient operation.
- Query:
Pattern 3: Get Specific Order
- Query:
PK = 'USER#123',SK = 'ORDER#999' - This is a direct lookup.
- Query:
Tip: Use Meaningful Prefixes Using prefixes like
USER#,ORDER#, orPRODUCT#in your keys is not just for readability. It allows you to use thebegins_withfilter in your queries to fetch groups of items, such as "all orders for a user" or "all items in an order."
Advanced Modeling: Global Secondary Indexes (GSI)
What happens when you need to query data using an attribute that isn't your primary key? For example, what if you need to find a user by their Email address instead of their UserId?
In DynamoDB, you use Global Secondary Indexes (GSI). A GSI is essentially a "view" of your table that is projected onto a different partition key. When you create a GSI, you define a new PK and SK. DynamoDB automatically replicates the data from your base table to the index.
Implementing an Email Lookup
- Base Table: PK =
USER#<UserId>, SK =METADATA - GSI:
- GSI Partition Key =
USER_EMAIL - GSI Sort Key = (none, or specific attributes)
- GSI Partition Key =
When you insert a user record, DynamoDB automatically populates the GSI. You can then query the GSI by USER_EMAIL to find the corresponding UserId.
Warning: GSI Write Costs Every time you write an item to your base table, DynamoDB must update all of your GSIs. If you have many GSIs, your write costs will increase significantly. Only create GSIs for access patterns that are absolutely required for your application to function.
Code Example: Implementing the Design
Below is a practical example using the AWS SDK for JavaScript (v3). We will demonstrate how to store a user and an order, and how to query them.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand, QueryCommand } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
// 1. Storing data
async function createUserAndOrder(userId, email, orderId, amount) {
const params = {
TransactItems: [
{
Put: {
TableName: "EcommerceTable",
Item: {
PK: `USER#${userId}`,
SK: `METADATA#${userId}`,
Email: email,
Type: "User"
}
}
},
{
Put: {
TableName: "EcommerceTable",
Item: {
PK: `USER#${userId}`,
SK: `ORDER#${orderId}`,
Amount: amount,
Type: "Order"
}
}
}
]
};
await docClient.send(new TransactWriteCommand(params));
}
// 2. Querying all orders for a user
async function getUserOrders(userId) {
const params = {
TableName: "EcommerceTable",
KeyConditionExpression: "PK = :pk AND begins_with(SK, :sk)",
ExpressionAttributeValues: {
":pk": `USER#${userId}`,
":sk": "ORDER#"
}
};
const result = await docClient.send(new QueryCommand(params));
return result.Items;
}
Explaining the Code
- TransactWriteCommand: We use a transaction here to ensure that either both the User and the Order are created, or neither is. This maintains data integrity.
- KeyConditionExpression: This is the heart of the query. By using
begins_with(SK, 'ORDER#'), we are telling DynamoDB to scan only the items belonging to this user that start with the "ORDER#" prefix. This is extremely fast because it happens within a single partition.
Common Pitfalls and How to Avoid Them
1. The "Scan" Trap
The most common mistake is using the Scan operation. A scan reads every single item in your table and then filters them in memory. On a table with a few hundred items, this is fine. On a table with millions of items, this will time out and cost a fortune. Always design your table so that your primary access patterns are serviced by Query or GetItem.
2. Hot Partitions
If you choose a Partition Key that isn't distributed well, you can create a "hot partition." For example, if you use Date as your Partition Key, every single request that comes in on that day will hit the same physical server. This creates a bottleneck. Always choose a Partition Key with high cardinality, such as a unique ID, to ensure your data is spread evenly across the DynamoDB cluster.
3. Over-Normalization
Newcomers often try to create "Relationship" tables (like a User_Orders mapping table). In DynamoDB, this is usually an anti-pattern. If you find yourself needing to perform a join, it is almost always better to denormalize your data. Duplicate the necessary information into the item where you need it, rather than trying to link two records together.
Callout: When to Denormalize Denormalization is the process of copying data into multiple places. In SQL, this is bad because it leads to inconsistency. In DynamoDB, it is a feature. By storing the
UserNameinside theOrderitem, you save a second query to theUserrecord when you display the order history.
Industry Best Practices for Schema Design
To ensure your DynamoDB implementation remains performant as your application grows, follow these industry-standard practices:
- Use Generic Attribute Names: Use
PKandSKinstead ofUserIdorOrderIdfor your primary keys. This makes your table flexible, allowing you to store multiple entity types (Users, Orders, Inventory, Logs) in the same table without naming conflicts. - Keep Items Small: DynamoDB has a 400KB limit per item. If your data exceeds this, you should either split the item into multiple records (linked by the same PK) or move large blobs (like images) to Amazon S3 and store only the URL in DynamoDB.
- Use TTL (Time to Live): If you are storing temporary data like session tokens or logs, use the TTL feature. It allows you to set an expiration timestamp on an item, and DynamoDB will automatically delete it for you at no extra cost.
- Monitor Throughput: Use CloudWatch to track your consumed Read and Write Capacity Units (RCUs/WCUs). If you see spikes, investigate which access pattern is causing the surge.
- Avoid Large Attribute Names: While JSON keys are descriptive, remember that you pay for storage based on the size of the attribute names.
UserEmailAddresstakes more space thanEmail. Keep your attribute names short but clear.
Comparison: SQL vs. NoSQL Modeling
It is helpful to visualize the difference in how you approach the problem.
| Feature | Relational (SQL) | NoSQL (DynamoDB) |
|---|---|---|
| Schema | Rigid, defined upfront | Flexible, defined by access |
| Joins | Supported (at a cost) | Not supported |
| Scaling | Vertical (bigger server) | Horizontal (more partitions) |
| Data Access | Query-driven | Pattern-driven |
| Transactions | ACID compliant | Limited to single/small groups |
Detailed Implementation: The "Adjacency List" Pattern
The "Adjacency List" is a common design pattern used to model relationships in DynamoDB. It allows you to represent a graph-like structure (e.g., a social network or a directory) within a single table.
Scenario: A Social Media Network
You need to store Users and their Followers.
- User Record:
PK = USER#123,SK = PROFILE - Follower Record:
PK = USER#123,SK = FOLLOWS#USER#456
If you want to see who a user follows, you query PK = USER#123 and SK begins_with FOLLOWS#. This is an efficient way to store relationships without needing a separate relational table.
Why this works:
- Single Request: You get the user's profile and their list of followers in one round trip.
- Efficient Filtering: You can use the
SKto filter the relationship types easily. - Scalability: As the user grows to thousands of followers, the query performance remains constant because the data is localized to one partition.
Handling Large Data Collections
Sometimes, a single partition key might contain too much data. For example, if a celebrity user has 10 million followers, storing all of them under one PK will exceed the partition size limit.
The Solution: Partition Splitting
You can add a "suffix" to your Partition Key to artificially split the data:
PK = USER#123#0PK = USER#123#1PK = USER#123#2
By distributing the follower records across these three partitions, you prevent any single partition from becoming too large or too "hot." Your application logic simply needs to know which partition to query, or it can perform a parallel query across all of them if necessary.
Step-by-Step: Migrating from SQL to DynamoDB
If you are tasked with migrating an existing SQL application to DynamoDB, follow this process:
- Audit Your Queries: Run a log analysis on your SQL database. Identify the top 5 most frequent
SELECTstatements. These are your "Access Patterns." - Draft the Schema: Create a spreadsheet. List each Access Pattern in the rows and your proposed table columns in the columns. Ensure every access pattern can be satisfied with a
Queryon aPK/SKcombination. - Handle Relationships: Determine which relationships are "one-to-one" (can be merged into one item) and which are "one-to-many" (can be handled with an Adjacency List).
- Implement Secondary Indexes: If an access pattern requires searching by a field other than the Primary Key (e.g.,
Email), plan your GSI. - Testing: Run a load test using the AWS SDK. Ensure that your queries are returning the expected data in a single request. If you find yourself needing to make two separate calls to the database to render one page, go back to step 2 and denormalize.
Common Questions (FAQ)
Q: Can I change my Primary Key later?
A: No. In DynamoDB, the Primary Key is immutable. If you realize your schema is wrong, you must create a new table, migrate the data, and update your application code. This is why spending time on schema design upfront is critical.
Q: How do I perform a "Join" in DynamoDB?
A: You don't. You either denormalize your data (copy the related information into the item) or you perform two separate queries in your application code. Most developers find that two fast, targeted queries are faster and more reliable than one complex SQL join.
Q: What if I need to search by multiple criteria?
A: If you need to search by Status AND Date AND Category, you should use a GSI with a composite key that combines these fields, or use the FilterExpression parameter. Note that FilterExpression happens after the data is read from the disk, so it does not save you on read costs.
Key Takeaways for Success
- Access Patterns First: Never design your tables based on your entities. Design them based on the specific questions your application will ask the database.
- Embrace Denormalization: Storing redundant data is not a failure; it is a strategy to ensure high performance and low latency.
- Master the PK/SK: The combination of Partition Key and Sort Key is the most powerful tool in your arsenal. Use them to group related data together physically.
- Avoid Scans: Scans are the enemy of performance. If your code uses a scan, rethink your schema design.
- Use GSIs Sparingly: Secondary indexes are useful but add cost and complexity to your write operations. Only use them when absolutely necessary.
- Think in Collections: Remember that you are building a document-based, key-value store. Think about how to group items into logical collections so they can be retrieved together.
- Test at Scale: A schema that works with 10 items might fail with 10 million. Always consider how your keys will distribute across partitions as your data grows.
By adhering to these principles, you will move beyond treating DynamoDB as a "simple key-value store" and begin using it as a highly sophisticated, high-performance engine capable of handling the most demanding application workloads. Designing for DynamoDB is a shift in mindset, but once you master the art of access-pattern-driven schema design, the performance gains are undeniable.
Continue the course
Enjoying the courses?
Everything stays free. Pro shows fewer ads, doubles your daily points limit so you progress twice as fast, and lets you read each lesson on one page.
- ✓ Fewer advertisements
- ✓ 2× daily points limit
- ✓ Distraction-free lessons