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.