Index Performance Optimization

Earn 25 points (50 with Pro) in two steps

  1. ① Read through the lesson — each section gets a ✓ as you scroll through it.
  2. ② When every section has a ✓, tap Complete lesson.

0 of 10 read · keep scrolling

✦ See fewer ads and earn double points — 50 a lesson instead of 25 — with Pro

Index Performance Optimization in Azure Cosmos DB

Introduction: The Foundation of Database Performance

When you work with Azure Cosmos DB, you are interacting with a globally distributed, multi-model database service designed for high availability and low latency. However, even the most powerful hardware and distributed architecture cannot compensate for inefficient data retrieval patterns. At the heart of every read operation in Cosmos DB lies the indexing engine. Understanding how to optimize this engine is the single most effective way to control your Request Unit (RU) consumption, reduce latency, and ensure your application scales predictably as your data volume grows.

An index is essentially a map that the database uses to locate data without having to scan every single document in a collection. By default, Cosmos DB indexes every property of every item, which provides a great "out of the box" experience but can become a significant performance bottleneck as your data model increases in complexity. If your indexing strategy is not aligned with your application's query patterns, you will find yourself consuming excessive RUs, facing throttled requests, and observing slow response times that frustrate end-users.

In this lesson, we will peel back the layers of the Cosmos DB indexing engine. We will explore how indexing policies work, how to customize them to fit specific workload requirements, and the trade-offs involved in balancing write performance against read efficiency. By the end of this guide, you will be equipped to design indexing strategies that support high-performance applications while keeping operational costs under control.


Not read yet

Understanding the Cosmos DB Indexing Engine

To optimize your indexing strategy, you must first understand what the indexing engine is actually doing. Every time you insert, update, or delete a document, the Cosmos DB engine automatically updates the index. This means that while indexing makes reads faster, it introduces a cost during write operations. Every property included in the index adds a small amount of overhead to the ingestion process.

By default, Cosmos DB uses a "Consistent" indexing mode. This ensures that when you perform a point read or a query, the results are always consistent with the most recent write. This is the gold standard for many applications, but it requires that the index be updated synchronously with the write operation. If you have a write-heavy workload, this synchronous update can significantly increase the cost of your write operations.

The Components of an Indexing Policy

An indexing policy is defined as a JSON document associated with each container. It dictates which paths are included or excluded from the index, the type of index (e.g., range, hash, or spatial), and the indexing mode. By default, the policy looks like this:

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/*"
        }
    ],
    "excludedPaths": [
        {
            "path": "/\"_etag\"/?"
        }
    ]
}

This default policy is "all-inclusive." It indexes every path except for the system-generated _etag property. While convenient for development, it is rarely the most efficient choice for a production environment, especially when you have large documents with many nested properties that are never queried.

Callout: Consistent vs. Lazy Indexing In older iterations of database systems, "lazy" indexing was common, where the index was updated asynchronously. In modern Cosmos DB, the "Lazy" indexing mode is deprecated. You now choose between "Consistent" (default) and "None." If you need to disable indexing for specific collections to save on write costs, you must set the indexing mode to "None." This is useful for temporary data or collections where you only ever perform point reads by ID.


Not read yet

Practical Strategies for Index Optimization

Optimizing your indexing strategy starts with identifying your query patterns. You cannot optimize what you do not understand. Before changing your indexing policy, you should use the Azure Portal's "Metrics" blade or the Cosmos DB Query Stats to see which queries are consuming the most RUs and whether they are performing full container scans.

1. The Principle of Least Inclusion

The most important rule in index optimization is to index only what you need. If your application queries by category and price, there is no reason to index a large description field or an array of tags that you only retrieve via point reads. By excluding unnecessary paths, you reduce the write overhead and the storage cost of the index itself.

2. Utilizing Range Indexes for Comparisons

By default, Cosmos DB creates range indexes for all paths. Range indexes are necessary for equality comparisons (=) as well as range comparisons (>, <, >=, <=). If you know that you will only ever query a specific property using equality, you might consider using a hash index if it were available (though Cosmos DB primarily relies on range indexes for most scenarios).

3. Handling Large Documents and Arrays

Arrays present a unique challenge in Cosmos DB indexing. When you index an array, the engine creates an entry for every single element within that array. If you have a document with an array containing thousands of items, indexing that path can explode the size of your index and lead to massive RU spikes during writes. Always exclude large arrays from your indexing policy unless you absolutely need to query the contents of those arrays.


Not read yet

Implementing Custom Indexing Policies

Let’s walk through a scenario where we want to optimize a container that stores product data. The documents look like this:

{
    "id": "prod-123",
    "name": "Wireless Mouse",
    "category": "Electronics",
    "price": 29.99,
    "internal_metadata": {
        "warehouse_code": "WH-01",
        "last_scanned": "2023-10-01"
    },
    "tags": ["peripheral", "office", "wireless"]
}

If our application only ever searches by category and price, we can significantly optimize the index by excluding everything else.

Step-by-Step: Updating the Indexing Policy

  1. Navigate to your Cosmos DB account in the Azure portal.
  2. Select the "Data Explorer" tab and locate your container.
  3. Click on the "Settings" tab for that container.
  4. Locate the "Indexing Policy" section.
  5. Modify the JSON to explicitly include only the necessary paths.
{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [
        {
            "path": "/category/?"
        },
        {
            "path": "/price/?"
        }
    ],
    "excludedPaths": [
        {
            "path": "/*"
        }
    ]
}

Explanation of this configuration:

  • We set the excludedPaths to /*, which acts as a global "exclude all" rule.
  • We then explicitly add the includedPaths for category and price.
  • The /? at the end of the path indicates that we want to index the value at that specific path.
  • Any property not explicitly added will now be ignored by the indexing engine, significantly reducing the write cost for every document update.

Warning: The "Exclude All" Trap Be very careful when using /* in your excludedPaths. If you accidentally exclude a property that your application relies on for filtering, those queries will be forced to perform a full scan. A full scan is essentially a "table scan" in relational terms, and it will consume a massive amount of Request Units, likely leading to immediate performance degradation. Always test your queries in the Query Explorer after changing an index policy.


Not read yet

Advanced Indexing: Composite Indexes

Composite indexes are required when you have queries that filter by multiple properties or sort by multiple properties. A standard index handles single-path queries efficiently, but if you have a query like SELECT * FROM c WHERE c.category = 'Electronics' ORDER BY c.price DESC, the engine needs a composite index to perform that operation efficiently.

Without a composite index, the engine must fetch all documents matching the category, load them into memory, and then perform an in-memory sort. This is extremely expensive in terms of RUs.

Configuring a Composite Index

To support the query above, you would update your policy to include a compositeIndexes block:

"compositeIndexes": [
    [
        {
            "path": "/category",
            "order": "ascending"
        },
        {
            "path": "/price",
            "order": "descending"
        }
    ]
]

When you define a composite index, the order of the paths matters. The query must match the order defined in the index. If you define the index as category then price, a query filtering by price then category will not be able to use that specific composite index.


Not read yet

Best Practices and Industry Standards

To maintain a healthy Cosmos DB indexing strategy, adhere to the following guidelines:

  • Monitor RU Consumption: If you see high RU costs for simple queries, check the "Query Stats" to see if the query is using the index or performing a scan.
  • Use Point Reads Whenever Possible: If you are retrieving a single document by its id and partition key, an index is not strictly required. Point reads are the most efficient way to interact with Cosmos DB and are much cheaper than queries.
  • Avoid Over-Indexing: Do not index properties that you never use in WHERE, ORDER BY, or JOIN clauses. Every indexed property is a tax on your write performance.
  • Test in Development: Never deploy an indexing policy change directly to production without testing it against a representative dataset. Use the Azure Cosmos DB Emulator to simulate production workloads.
  • Keep Documents Flat: Wherever possible, avoid deeply nested JSON structures. Deep nesting makes indexing policies more complex to manage and can lead to accidental indexing of large, unnecessary data blobs.

Comparison: Indexing Options

Feature Consistent Indexing No Indexing
Write Performance Slower (due to overhead) Faster
Read Performance High (for indexed queries) Very Slow (full scans)
Consistency Strong/Bounded Staleness N/A
Best Use Case Production apps with queries Temp data or ID-only lookups

Not read yet

Common Pitfalls and How to Avoid Them

Pitfall 1: Indexing Everything

Many developers leave the default policy enabled because it is "safe." However, as your database grows to millions of items, the index itself can become larger than the data. This increases storage costs and slows down the indexing engine as it tries to maintain a massive index structure.

The Fix: Periodically audit your queries. If a property isn't being queried, remove it from the includedPaths or use the excludedPaths pattern to keep the index lean.

Pitfall 2: Forgetting the Partition Key

The partition key is automatically indexed. You cannot exclude it. However, developers often forget that their queries should always include the partition key to be as efficient as possible. A cross-partition query is inherently more expensive than a single-partition query, regardless of how well your indexing policy is tuned.

The Fix: Always design your data model such that your most common queries include the partition key.

Pitfall 3: Ignoring "Order By" Requirements

If you have a query that returns results in a specific order, you must ensure that an index exists that supports that sort order. If you try to sort by a property that is not indexed, the query will fail or perform a scan.

The Fix: If you need to sort by a specific property, ensure it is included in your index. If you need to sort by multiple properties, you must define a composite index.


Not read yet

The Role of TTL (Time to Live) and Indexing

Time to Live (TTL) is a feature that allows you to automatically expire documents after a set period. While TTL is primarily a data lifecycle feature, it interacts with the index. When a document expires, the index must also be updated to remove the references to that document.

If you have a high volume of documents expiring simultaneously, you may see a spike in RU consumption. This is because the background process responsible for cleaning up expired documents and updating the index is working harder. If your indexing policy is overly complex, this cleanup process becomes even more resource-intensive.

Note: If you are using TTL, ensure that your indexing policy is as lean as possible. By excluding unnecessary properties, you reduce the amount of work the cleanup process has to perform, leading to smoother resource utilization over time.


Advanced Query Optimization Techniques

Even with a perfect indexing policy, some queries can be problematic. For example, queries that use functions in the WHERE clause, such as SELECT * FROM c WHERE UPPER(c.name) = 'PRODUCT', can often bypass the index.

Why Functions Bypass the Index

The indexing engine creates entries based on the actual values stored in the document. When you wrap a property in a function like UPPER(), the database cannot look up the value in the index because the index doesn't store the "uppercased" version of your data. It only stores the raw value.

The Fix: Store the data in the format you intend to query it. If you need case-insensitive searches, store a name_lowercase property alongside the original name property and query against the name_lowercase field. This allows the index to work as intended.

Handling IN and OR Clauses

Queries using IN or OR can sometimes be inefficient. While the indexing engine can handle these, they can lead to large result sets that consume many RUs.

The Fix: Whenever possible, break OR queries into separate queries or use the IN operator carefully. If you find yourself using OR frequently, evaluate whether your data model could be denormalized to avoid the need for complex filtering.


Not read yet

Monitoring and Troubleshooting

How do you know if your indexing strategy is working? Cosmos DB provides excellent diagnostic tools.

  1. Request Charge: Every response from the Cosmos DB SDK includes a x-ms-request-charge header. If this number is consistently high, your query is likely inefficient.
  2. Query Stats: The x-ms-document-query-metrics header provides detailed information about how the query was executed, including the number of documents retrieved and the number of index hits.
  3. Azure Monitor: You can set up alerts for high RU consumption. If a specific query pattern suddenly starts consuming more RUs, you will be notified before it impacts your users.

Step-by-Step: Analyzing a Slow Query

If a query is slow, follow this procedure:

  1. Copy the query into the Azure Portal Query Explorer.
  2. Run the query and click on the "Query Stats" tab.
  3. Look for the "Index hit ratio." A low ratio indicates that the query is scanning many documents that don't match the criteria.
  4. Check if the query is performing a cross-partition scan.
  5. Adjust the index policy or the query structure accordingly.

Not read yet

Future-Proofing Your Indexing Strategy

As your application evolves, your indexing needs will change. What works for a prototype will not work for a production system with millions of documents.

  • Start Simple: Don't over-engineer your index on day one. Start with the default policy, but monitor your RU usage closely.
  • Iterative Refinement: As you identify your core query patterns, start narrowing your index to include only those properties.
  • Document Your Strategy: Keep a record of why specific paths are included or excluded. This will be invaluable when a new developer joins the team and needs to understand why certain queries are fast while others are slow.
  • Stay Updated: Microsoft frequently updates the Cosmos DB engine. Keep an eye on the official documentation for new indexing features, such as improved support for spatial queries or new index types.

Key Takeaways

  1. Indexing is a Trade-off: Every indexed property improves read performance but increases write latency and RU consumption. Balance this based on your application's read/write ratio.
  2. Exclude Unnecessary Paths: The default "all-inclusive" index is rarely optimal for production. Use an "exclude-all" strategy and explicitly add only the paths you actually query.
  3. Composite Indexes are Essential: For queries involving multi-property filters or sorts, composite indexes are not optional; they are required to prevent expensive in-memory processing.
  4. Data Modeling Matters: How you structure your JSON affects index efficiency. Avoid deep nesting and store data in the format you intend to query to avoid using functions in WHERE clauses.
  5. Monitor to Optimize: Use the Request Charge and Query Metrics to identify inefficient queries. Never assume your indexing strategy is perfect; let the metrics guide your refinements.
  6. Point Reads are King: If your application can retrieve data by ID and partition key, do so. This bypasses the complexity of indexing altogether and provides the most performant access pattern.
  7. Test Before You Deploy: Always validate index policy changes in a non-production environment. A poorly configured index can cause immediate and significant performance degradation.

By applying these principles, you move from being a passive user of Cosmos DB to an active architect of your data layer. A well-tuned indexing strategy is the difference between a database that struggles under load and one that provides consistent, lightning-fast performance at any scale. Continue to refine your approach, monitor your metrics, and keep your data model aligned with your access patterns.

Not read yet

Each section gets a ✓ as you scroll through it. Tap the button to jump to the next one.