Azure Cosmos DB: Complete Hierarchical Partition Key Guide

Cosmos DB’s partition key choice determines the scalability and cost of your database. A poor choice leads to hot partitions, throttling, and eventual redesign. Azure recently introduced Hierarchical Partition Keys—multi-level partitioning that distributes load more evenly in multi-tenant and time-series scenarios. This comprehensive guide covers partition key design fundamentals, when to use hierarchical keys, and migration strategies for existing containers.

Partition Key Fundamentals

Every Cosmos DB container is partitioned by a single property (or path). Items with the same partition key value form a “logical partition” stored together on the same physical partition. Key constraints:

  • Logical partition limit: 20 GB per partition key value
  • Operations: Queries and transactions scoped to single partition are fastest
  • Write distribution: High-frequency writes to one partition key = hot partition

The Multi-Tenant Problem

Consider a SaaS application with tenant data:

Partition Key: /tenantId

Tenant A: 500 MB, 10 writes/sec
Tenant B: 15 GB, 1000 writes/sec (hot partition!)
Tenant C: 50 MB, 1 write/sec

Tenant B causes throttling for everyone because it overwhelms its physical partition. Traditional solutions involve synthetic partition keys (e.g., tenantId_hash), but these break query efficiency.

Hierarchical Partition Keys

Hierarchical partition keys allow up to 3 levels of partitioning:

var containerProperties = new ContainerProperties
{
    Id = "orders",
    PartitionKeyPaths = new List<string> 
    { 
        "/tenantId",    // Level 1
        "/year",        // Level 2  
        "/orderId"      // Level 3
    }
};
flowchart TB
    subgraph Container ["Orders Container"]
        subgraph T1 ["Tenant A"]
            T1Y1["2022"]
            T1Y2["2023"]
        end
        
        subgraph T2 ["Tenant B (Large)"]
            T2Y1["2022 - Split across partitions"]
            T2Y2["2023 - Split across partitions"]
        end
        
        subgraph T3 ["Tenant C"]
            T3Y1["2022"]
        end
    end
    
    style T2Y1 fill:#C8E6C9,stroke:#2E7D32
    style T2Y2 fill:#C8E6C9,stroke:#2E7D32

Now Tenant B’s 2022 data can be split across multiple physical partitions, eliminating the hot partition while maintaining query locality.

Query Patterns

// Most efficient: All 3 levels specified
var query = container.GetItemQueryIterator<Order>(
    "SELECT * FROM c WHERE c.tenantId = 'A' AND c.year = 2022 AND c.orderId = 'O123'");

// Efficient: Prefix match (first 2 levels)
var query = container.GetItemQueryIterator<Order>(
    "SELECT * FROM c WHERE c.tenantId = 'A' AND c.year = 2022");

// Cross-partition: Only first level
var query = container.GetItemQueryIterator<Order>(
    "SELECT * FROM c WHERE c.tenantId = 'A'");

// Fan-out: No partition key (avoid in production)
var query = container.GetItemQueryIterator<Order>(
    "SELECT * FROM c WHERE c.total > 1000");

Design Considerations

Level Order Matters

Order keys from most restrictive (most queries include it) to least restrictive. Example: tenantId → year → region

Cardinality at Each Level

Ensure enough cardinality at each level for even distribution. If Level 2 has only 2 values, partitioning benefits are limited.

Migration Strategy

Existing containers cannot be modified to use hierarchical keys. Migration requires:

// 1. Create new container with hierarchical keys
var newContainer = await database.CreateContainerAsync(new ContainerProperties
{
    Id = "orders-v2",
    PartitionKeyPaths = new List<string> { "/tenantId", "/year", "/orderId" }
});

// 2. Use Change Feed to migrate data
var changeFeedProcessor = container.GetChangeFeedProcessorBuilder<Order>(
    "migration",
    async (changes, token) =>
    {
        foreach (var order in changes)
        {
            await newContainer.CreateItemAsync(order, 
                new PartitionKeyBuilder()
                    .Add(order.TenantId)
                    .Add(order.Year)
                    .Add(order.OrderId)
                    .Build());
        }
    })
    .WithStartFromBeginning()
    .Build();

Key Takeaways

  • Hierarchical partition keys solve multi-tenant hot partition problems
  • Use up to 3 levels of partitioning
  • Order keys from most to least restrictive
  • Queries with partition key prefixes are efficient
  • Migration requires new container and Change Feed

Discover more from C4: Container, Code, Cloud & Context

Subscribe to get the latest posts sent to your email.

Leave a comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.