DynamoDB Queries vs Scans
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: Mastering Queries vs. Scans
In the world of cloud-native development, choosing the right database access pattern is often the difference between a high-performing application and one that suffers from latency spikes and ballooning costs. Amazon DynamoDB, as a NoSQL database, presents a unique paradigm shift for developers coming from traditional relational database backgrounds. In SQL, we are accustomed to writing complex SELECT statements with JOIN operations that the database engine optimizes on our behalf. In DynamoDB, however, the structure of your data and the way you retrieve it are inextricably linked. Understanding the fundamental difference between a Query and a Scan is the most critical skill for anyone building applications on AWS.
This lesson explores these two retrieval methods in depth. We will break down how they function under the hood, when to use each, the performance implications of choosing incorrectly, and the design patterns that allow you to avoid the "Scan" trap entirely. By the end of this guide, you will be able to architect your data models to favor efficient queries, ensuring your applications remain responsive as they scale to millions of users.
Understanding the Core Concepts
To grasp the difference between a Query and a Scan, we must first visualize how DynamoDB stores data. DynamoDB partitions data across many physical servers based on the Partition Key (PK) of your table. When you perform an operation, the request is routed to the specific partition where your data lives.
What is a Query?
A Query operation in DynamoDB is a targeted retrieval process. When you issue a Query, you are telling DynamoDB exactly which Partition Key you are interested in. Because the database knows exactly which partition holds the data for that key, it can jump straight to that location and retrieve only the items that match your criteria. This is an O(1) or O(log n) operation in terms of efficiency, depending on the complexity of your Sort Key (SK) conditions.
What is a Scan?
A Scan operation, by contrast, is a brute-force approach. It tells DynamoDB to examine every single item in the entire table. It reads the data from every partition, regardless of whether that partition contains the information you are looking for. If your table has 100 items, a Scan reads 100 items. If your table has 100 million items, a Scan reads 100 million items. This is an O(n) operation where 'n' is the total number of items in the table, making it extremely expensive and slow as your data grows.
Callout: The "Library" Analogy Imagine your data is stored in a massive library. A Query is like using the card catalog to find the exact aisle and shelf where a specific book is located; you walk straight there and pick it up. A Scan is like walking into the library and checking every single book on every shelf from the front door to the back wall just to see if you can find the one you want. One is precise and efficient; the other is exhaustive and exhausting.
Deep Dive: The Query Operation
The Query operation is the bread and butter of performant DynamoDB applications. It is designed to be fast and predictable. To use a Query effectively, you must understand the relationship between the Partition Key and the optional Sort Key.
Structure of a Query
A Query requires at least the Partition Key to be specified. You can further refine the results using a Sort Key condition. For example, if you have a table of Orders where the Partition Key is CustomerId and the Sort Key is OrderId, you can query for all orders belonging to a specific customer.
# Example: Querying orders for a specific customer using Boto3
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Orders')
response = table.query(
KeyConditionExpression=Key('CustomerId').startswith('USER_123')
)
items = response['Items']
In the example above, the KeyConditionExpression is the key to efficiency. By specifying the CustomerId, you are limiting the scope of the operation to only those partitions managed by that specific ID.
Why Queries are Efficient
Queries are efficient because they leverage the physical indexing of the database. When you provide a Partition Key, DynamoDB performs a hash lookup to identify the physical node. The Sort Key allows for range-based retrieval, such as "get all orders from the last 30 days." Because the data is physically sorted by the Sort Key within the partition, DynamoDB does not have to perform any additional sorting or filtering logic at the application level—it simply reads the contiguous block of data.
Note: Even with a Query, if you retrieve a massive amount of data, DynamoDB will paginate the results. You must handle the
LastEvaluatedKeyin your application logic to ensure you fetch all pages of data when the result set exceeds 1MB.
Deep Dive: The Scan Operation
A Scan operation should be treated with extreme caution. While it is technically possible to use it to retrieve data, it should rarely be the default choice in a production environment. However, there are specific scenarios where Scans are necessary, such as performing a full table migration or generating a report that requires analyzing every record.
When is a Scan Appropriate?
There are very few "good" reasons to use a Scan. Some acceptable use cases include:
- Initial Table Analysis: When you are first developing and your table has fewer than 100 items.
- Periodic Batch Processing: Running an offline job once a week to calculate global statistics that cannot be derived from a specific partition.
- Data Migrations: When you need to read every single item to reformat it or move it to a new table structure.
The Cost of Scans
The cost of a Scan is calculated based on the number of items read, not the number of items returned. If you have a table with 1,000,000 items and you perform a Scan with a filter that only matches 10 items, you are still charged for reading 1,000,000 items. This makes Scans financially dangerous if your table grows unexpectedly.
# Example: A Scan operation that should be avoided in high-traffic production
response = table.scan(
FilterExpression=Attr('Status').eq('PENDING')
)
In the code above, the FilterExpression is applied after the data is read from the disk. DynamoDB reads the entire table, brings it into memory, filters out the non-matching items, and then sends you the result. You pay for the read throughput of the entire table, even if the result set is tiny.
Comparison: Query vs. Scan
To help you decide which tool to reach for, consider the following comparison table:
| Feature | Query | Scan |
|---|---|---|
| Performance | High (O(1) or O(log n)) | Low (O(n)) |
| Cost | Low (Pay for what you retrieve) | High (Pay for what you read) |
| Scalability | Constant speed regardless of table size | Gets slower as table size increases |
| Filtering | Uses Key conditions (Efficient) | Uses Filter Expressions (Post-read) |
| Best For | Routine data retrieval | Full table analysis/migrations |
Best Practices for Data Retrieval
To build applications that are both cost-effective and fast, you must adopt a "Query-first" mindset. This involves careful schema design.
1. Design for your Access Patterns
Before you write a single line of code, map out your requirements. If you need to find users by email, but your primary key is UserId, you cannot query by email. You should either:
- Create a Global Secondary Index (GSI) on the
Emailfield. - Redesign your table to make
Emailthe primary key.
2. Use Global Secondary Indexes (GSIs)
GSIs are the secret weapon for developers who need to query data in multiple ways. A GSI is essentially a new view of your table with a different Partition Key and Sort Key. You can have up to 20 GSIs per table, which allows you to query your data by various attributes without resorting to a Scan.
Callout: Global Secondary Indexes vs. Local Secondary Indexes A Local Secondary Index (LSI) uses the same Partition Key as your main table but a different Sort Key. These must be created at table creation time. A Global Secondary Index (GSI) allows you to choose a completely different Partition Key and can be added or removed at any time. In modern DynamoDB design, GSIs are used far more frequently than LSIs because they offer more flexibility.
3. Avoid "Large" Scans
If you absolutely must scan a large table, use "Parallel Scans." DynamoDB allows you to divide a scan into multiple segments. You can initiate several parallel threads, each scanning a different segment of the table. This significantly reduces the time it takes to complete the scan, though it still consumes the same amount of read capacity.
4. Paginate Your Results
Always assume your query will return more data than can fit in a single response. Your code should check for the LastEvaluatedKey in the response object. If it exists, you must make a subsequent call, passing that key as the ExclusiveStartKey to fetch the next "page" of data. Failing to do this is a common source of bugs where developers think their data is missing.
5. Use Projections
Whether you are performing a Query or a Scan, only request the fields you actually need. By using ProjectionExpression, you reduce the amount of data transferred over the network and can sometimes reduce the amount of Read Capacity Units (RCUs) consumed if you are using specific consistency levels.
Common Pitfalls and How to Avoid Them
Even experienced developers fall into traps with DynamoDB. Let's look at the most frequent mistakes.
Mistake 1: Filtering on the Client Side
A common mistake is to Query a wide range of data and then filter the results in your application code (e.g., using a Python list comprehension). This is wasteful. Instead, use the KeyConditionExpression to filter on the database side. If the logic is too complex for a Key condition, use a FilterExpression in your Query. While a FilterExpression still consumes the read capacity of the items it evaluates, it at least saves the bandwidth of transferring those items to your application.
Mistake 2: Ignoring Provisioned Throughput
If you perform a Scan on a table with a low throughput limit, you will quickly exhaust your RCUs, resulting in ProvisionedThroughputExceededException errors. If you must scan, consider using "Eventually Consistent" reads, which cost half as much as "Strongly Consistent" reads. However, be aware that your scan might not include the absolute most recent writes.
Mistake 3: Over-indexing
While GSIs are useful, they come at a cost. Every time you write an item to your main table, DynamoDB must also update every GSI. If you have 20 GSIs, a single write operation triggers 20 additional write operations in the background. This can significantly increase your write costs and latency. Only create an index if you have a specific, recurring query pattern that requires it.
Mistake 4: Misunderstanding "All Attributes"
A Scan usually defaults to returning all attributes. If your items are large (e.g., they contain binary data or large blobs of JSON), a Scan will quickly blow through your memory limits. Always specify a ProjectionExpression to limit the data retrieved to only what is necessary for the current task.
Step-by-Step: Implementing a Search Pattern
Let's walk through a common scenario: Implementing a search for orders by Status.
- Analyze the Requirement: We need to find all orders where the status is "SHIPPED".
- Evaluate the Approaches:
- Scan: Simple to write, but expensive and slow as the table grows.
- GSI: Create an index where the Partition Key is
Statusand the Sort Key isOrderId.
- Execution:
- Create the GSI on the
Orderstable. - Update the application code to perform a
Queryagainst the GSI instead of the main table.
- Create the GSI on the
- Result: The application now performs a targeted query on the index, returning results in milliseconds regardless of whether the table has 1,000 or 1,000,000,000 items.
Tip: When using a GSI, you don't need to project all attributes into the index. You can project only the keys and the necessary status fields to keep the index size small and the costs lower.
Advanced Strategies: Handling High-Volume Data
As your data grows, even efficient Queries can run into bottlenecks. Here are advanced strategies to maintain performance.
Sparse Indexes
A sparse index is a GSI where only items that contain the index's key attributes are indexed. If you have a table of 1 million users, but only 1,000 of them are "Premium," you can create a GSI with a PremiumStatus attribute. Only the 1,000 premium users will appear in the index. This makes the index incredibly small, fast to query, and cheap to maintain.
Querying with Multiple Conditions
If you need to query by multiple attributes that aren't part of your primary key, you can combine a KeyConditionExpression with a FilterExpression.
- Example: You query by
CustomerId(Key) and then use aFilterExpressionto only show items whereAmount > 100. - This is much more efficient than a full scan because the
KeyConditionExpressionnarrows the search space to a single partition first, and theFilterExpressiononly runs on that small subset of data.
Capacity Planning
Always monitor your CloudWatch metrics for ConsumedReadCapacity and ThrottledRequests. If you see spikes in throttled requests, it is a clear indicator that your Query patterns are either too broad (scanning instead of querying) or that your traffic has outgrown your provisioned capacity. In such cases, enable "On-Demand" capacity mode, which allows DynamoDB to scale automatically based on your traffic patterns.
Summary Checklist for Developers
To ensure you are using DynamoDB effectively, run through this checklist for every new feature you implement:
- Is my primary key strategy optimal? Does it support my most frequent access pattern?
- Am I using a Query instead of a Scan? If not, is there a compelling reason why?
- Have I created a GSI for this access pattern? If I'm filtering on a non-key attribute, an index is almost always better.
- Am I using Projection Expressions? Don't fetch data you aren't going to display or process.
- Is pagination implemented? Never assume a query returns the full result set in one go.
- Have I considered the cost of the index? Are the writes to the GSI worth the read performance gains?
Key Takeaways
- Precision Matters: The Query operation is your primary tool for retrieving data. It is precise, efficient, and cost-effective because it targets specific partitions.
- Scans are a Last Resort: A Scan reads every single item in your table. Use it only for administrative tasks, migrations, or analysis on very small datasets.
- Design for Access Patterns: DynamoDB requires you to know how you will retrieve data before you design your table. Use Global Secondary Indexes to support multiple query patterns.
- Filter at the Database Level: Always prefer
KeyConditionExpressionorFilterExpressionover filtering data in your application code. This reduces network traffic and keeps your application logic clean. - Pagination is Mandatory: Always handle the
LastEvaluatedKeyin your code to ensure your application can handle result sets of any size. - Monitor Your Metrics: Use CloudWatch to keep an eye on read throughput and throttling. If your application is struggling, it is likely because you are scanning when you should be querying.
- Sparse Indexes Save Money: Use sparse indexes to index only a subset of your data, keeping your indexes small and your costs down.
By mastering the distinction between Queries and Scans, you are not just writing code; you are architecting for the cloud. You are moving from a mindset of "getting the data somehow" to "getting the data exactly where it lives." This discipline will serve you well as you build larger, more complex systems that rely on DynamoDB as their backbone. Remember, in DynamoDB, your data model is your performance model. Plan it carefully, query it intentionally, and your services will scale effortlessly.
Frequently Asked Questions (FAQ)
Q: Does a Scan ever get faster if I add more Read Capacity?
A: Yes, in the sense that you can read more items per second. However, the fundamental complexity remains O(n). You are simply throwing more money at an inefficient operation rather than fixing the underlying architectural issue.
Q: Can I perform a Query on a GSI just like a regular table?
A: Absolutely. A GSI acts like a standalone table for the purpose of querying. You can use the same Query API calls, just target the index name instead of the table name.
Q: What happens if I perform a Query that matches zero items?
A: You receive an empty list of items. You are still charged for the read operation, but only for the index or partition metadata lookup, which is very inexpensive compared to scanning the entire table.
Q: Is there any way to join data in DynamoDB like in SQL?
A: No. DynamoDB does not support joins. If you find yourself needing to join data, you should either denormalize your data (store everything in one item) or perform two separate queries in your application code and combine them. Denormalization is the standard approach in NoSQL design.
Q: What is the maximum size of a single Query result?
A: A single Query response is limited to 1MB of data. If your result set is larger than 1MB, you must use the LastEvaluatedKey to fetch the remaining data in subsequent requests.
Q: When should I choose On-Demand vs. Provisioned Capacity?
A: Use Provisioned Capacity if your traffic is predictable and steady, as it is generally cheaper. Use On-Demand if your traffic is unpredictable, bursty, or if you are in the early stages of development and don't yet know what your traffic patterns will look like.
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