In this post we are going to cover partitioning and partition keys in Azure Cosmos DB. These are 7 real Cosmos DB interview questions for data engineers, asked the way they come up on a real project, and the last one is a scenario: a multi-tenant SaaS app stores data for thousands of tenants, a few of them very large, and you design the partitioning.
The partition key is one property we choose when we create a container. Cosmos DB uses it to spread storage and throughput, and it decides which queries are cheap and how far a transaction reaches. Interviewers ask about it because it's the one design choice we can't undo in place. A bad key shows up later as a hot partition, throttled requests or expensive fan-out queries.
Part 2 of the Real Time Azure Cosmos DB Interview Questions and Answers series on TechBrothersIT.
- The short answer
- Let's set up the project
- Question 1: A teammate asks what a partition key is and why it matters so much. How do you explain it?
- Question 2: The team must choose a partition key for an orders container. How do you pick a good one?
- Question 3: One partition gets most of the traffic and requests are throttled. How do you find and fix a hot partition?
- Question 4: A query is slow and expensive because it fans out to every partition. What is a cross-partition query and how do you avoid it?
- Question 5: Someone asks about the 20 GB logical partition limit. What happens, and how do hierarchical partition keys help?
- Question 6: A teammate wants to change the partition key of a live container. What are the options?
- Scenario 7: A multi-tenant SaaS app stores data for thousands of tenants, a few of them very large. How do you design the partitioning?
- The full script
The short answer
The partition key groups items into logical partitions (max 20 GB) placed on physical partitions (10,000 RU/s, 50 GB). Pick a key with high cardinality, an even spread, and a place in your main filters; use hierarchical keys for big tenants. It can't be changed in place: copy to a new container.
Let's set up the project
We run the official Azure Cosmos DB Linux emulator locally, and small Python scripts that use the azure-cosmos client, so every output is real. Features the emulator doesn't support are shown as code. common.py connects with the COSMOS_ENDPOINT and COSMOS_KEY environment variables, creates the shop database, and mine() keeps only our own fields.
import os
from azure.cosmos import CosmosClient
client = CosmosClient(os.environ["COSMOS_ENDPOINT"],
credential=os.environ["COSMOS_KEY"])
shop = client.create_database_if_not_exists("shop")
def mine(item):
"""Your own fields only: drop _rid, _etag, _ts and friends."""
return {k: item[k] for k in sorted(item) if not k.startswith("_")}Question 1: A teammate asks what a partition key is and why it matters so much. How do you explain it?
The partition key is a property we choose when we create a container. Items with the same value form a logical partition, and Cosmos DB places those on physical partitions, which it splits automatically as data grows.
It matters because the key decides:
- How storage and throughput spread
- Which queries are cheap
- The transaction scope: one logical partition
- And it's fixed at creation: we can't change it in place
Let's see it. orders.py creates an orders container keyed by /customerId and writes six orders for customers c1, c2 and c3. Then it does a point read, a read with the wrong key, and counts the items per key value. We run python orders.py.
from collections import Counter
from azure.cosmos import PartitionKey
from azure.cosmos.exceptions import CosmosResourceNotFoundError
from common import shop
orders = shop.create_container_if_not_exists(
"orders", partition_key=PartitionKey(path="/customerId"))
for n, cust in enumerate(["c1", "c1", "c2", "c3", "c3", "c3"], 1):
orders.upsert_item({"id": f"o-{n}", "customerId": cust,
"total": 20 + n + 0.5})
print("partition key:", orders.read()["partitionKey"])
o = orders.read_item("o-4", partition_key="c3") # id + key
print("point read:", o["id"], o["customerId"], o["total"])
try:
orders.read_item("o-4", partition_key="c1") # wrong key
except CosmosResourceNotFoundError as e:
print("o-4 under c1:", e.status_code, "not found")
keys = orders.query_items("SELECT VALUE c.customerId FROM c",
enable_cross_partition_query=True)
print("items per logical partition:", sorted(Counter(keys).items()))$ python orders.py
partition key: {'kind': 'Hash', 'paths': ['/customerId'], 'version': 2}
point read: o-4 c3 24.5
o-4 under c1: 404 not found
items per logical partition: [('c1', 2), ('c2', 1), ('c3', 3)]The key is 'paths': ['/customerId'], kind Hash. The point read of o-4 needs both its id and its partition key value, c3, and returns o-4 c3 24.5. The same id under c1 is 404 not found: the key is part of the address. The counts per key value are the logical partitions: ('c1', 2), ('c2', 1), ('c3', 3).
Know the limits:
| Limit | Value |
|---|---|
| logical partition | 20 GB |
| physical partition | 50 GB |
| physical partition | 10,000 RU/s |
The key is part of the address. A point read needs the id and the key value, and the wrong key is a 404.
Question 2: The team must choose a partition key for an orders container. How do you pick a good one?
Three tests:
- High cardinality - many distinct values
- Even spread - of storage and of requests
- In your main filters - equality on the key: one partition per query
Let's test the candidates on 1,000 generated orders. candidates.py loads them, then for each property it counts the distinct values and the share of the biggest logical partition. We run python candidates.py.
import random
from collections import Counter
from azure.cosmos import PartitionKey
from common import shop
random.seed(7)
STATUSES = ["new", "paid", "shipped", "shipped"] # half shipped
history = shop.create_container_if_not_exists(
"order_history", partition_key=PartitionKey(path="/customerId"))
for n in range(1000):
history.upsert_item({
"id": f"h-{n}", "customerId": f"c{random.randint(1, 400)}",
"status": random.choice(STATUSES),
"orderDate": f"2026-10-{random.randint(25, 30)}"})
docs = list(history.read_all_items())
print("orders:", len(docs))
for key in ["status", "orderDate", "customerId", "id"]:
sizes = Counter(d[key] for d in docs)
biggest = sizes.most_common(1)[0][1] / len(docs)
print(f"{key:<11} distinct {len(sizes):>4}"
f" | biggest logical partition {biggest:.1%}")$ python candidates.py
orders: 1000
status distinct 3 | biggest logical partition 46.3%
orderDate distinct 6 | biggest logical partition 18.5%
customerId distinct 369 | biggest logical partition 0.9%
id distinct 1000 | biggest logical partition 0.1%status has distinct 3, and the biggest holds 46.3% of the orders: hot by design. orderDate has distinct 6 here, but in real life every new order lands on today's date, so all writes hit one partition. customerId has distinct 369, the biggest at 0.9%, and the "my orders" page filters on it. id spreads perfectly at 0.1%, but one customer's orders would then fan out.
| Candidate | Verdict |
|---|---|
| status | few values: hot |
| orderDate | today takes every write |
| customerId | spread + in the filter: pick |
| id | spread, but "my orders" fans out |
Customer id wins.
Build a synthetic key, like customer id plus month. And for one huge customer past 20 GB, use hierarchical keys.
Question 3: One partition gets most of the traffic and requests are throttled. How do you find and fix a hot partition?
Throughput is divided evenly across physical partitions, and one physical partition serves at most 10,000 RU/s. So when one key value takes most of the traffic, its partition throttles with 429 while the others sit idle.
Let's build a skewed key. hot.py writes events for five tenants into tenant_events, keyed by /tenantId, with contoso far bigger than the rest. The emulator has one physical partition and never throttles, so we show the skew, not the 429s. We run python hot.py.
from collections import Counter
from azure.cosmos import PartitionKey
from common import shop
events = shop.create_container_if_not_exists(
"tenant_events", partition_key=PartitionKey(path="/tenantId"))
plan = {"contoso": 800, "fabrikam": 80, "adatum": 60,
"litware": 40, "woodgrove": 20} # events per tenant
for tenant, count in plan.items():
for n in range(count):
events.upsert_item({"id": f"{tenant}-{n}", "tenantId": tenant,
"type": "page_view"})
q = "SELECT VALUE c.tenantId FROM c"
sizes = Counter(events.query_items(
q, enable_cross_partition_query=True))
total = sum(sizes.values())
for tenant, n in sizes.most_common():
print(f"{tenant:<10} {n:>4} items {n / total:.0%}")$ python hot.py
contoso 800 items 80%
fabrikam 80 items 8%
adatum 60 items 6%
litware 40 items 4%
woodgrove 20 items 2%contoso has 800 items, 80%. The next one, fabrikam, has 8%. On Azure, a key like contoso is what makes one partition hot.
Here's how we find it on Azure:
- Insights › Throughput - Normalized RU Consumption (%) By PartitionKeyRangeID
- The sign - one range at 100%, the others at 30% or less
- Diagnostic logs - which logical keys use the RUs
With diagnostic logs on, a Log Analytics query names the hot keys:
// Which logical partition keys use the request units?
CDBPartitionKeyRUConsumption
| where TimeGenerated >= ago(24h)
| where CollectionName == "tenant_events"
| where isnotempty(PartitionKey)
| summarize sum(RequestCharge) by PartitionKey, OperationName,
bin(TimeGenerated, 1s)
| order by sum_RequestCharge descAnd the fixes:
| Fix | When |
|---|---|
| a better key: new container (e.g. tenantId, userId) | lasting skew: the real fix |
| raise the RU/s | short term; one key value still caps at 10,000 RU/s |
| redistribute throughput across partitions (preview) | short term: a predictable hot partition |
Neither short-term fix lifts one key value past the 10,000 RU/s cap. The lasting fix is a better key, like tenant then user as a hierarchical key, in a new container.
Is it real skew, or a temporary spike? And keep diagnostic logs on only while you investigate: Log Analytics costs money.
Question 4: A query is slow and expensive because it fans out to every partition. What is a cross-partition query and how do you avoid it?
A cross-partition query has no partition key in its filter, so it runs on every physical partition. Microsoft documents about 2 to 3 extra request units per physical partition, so the cost grows with the container.
Let's run three queries on order_history, keyed by /customerId. The first passes partition_key='c42'. The second puts the key in the WHERE clause instead. The third filters on status only. We run python xquery.py.
from common import shop
history = shop.get_container_client("order_history") # /customerId
count = "SELECT VALUE COUNT(1) FROM c"
print("c42, partition_key='c42':",
list(history.query_items(count, partition_key="c42")))
in_where = count + " WHERE c.customerId = 'c42'"
print("c42, key in the WHERE:", list(history.query_items(
in_where, enable_cross_partition_query=True)))
by_status = "SELECT c.id, c.customerId FROM c WHERE c.status = @s"
rows = list(history.query_items(
by_status, parameters=[{"name": "@s", "value": "shipped"}],
enable_cross_partition_query=True))
print("status shipped, no key:", len(rows), "orders from",
len({r["customerId"] for r in rows}), "logical partitions")$ python xquery.py
c42, partition_key='c42': [2]
c42, key in the WHERE: [2]
status shipped, no key: 463 orders from 289 logical partitionsThe first two both return [2]: a key in the WHERE clause also routes the query to one partition. The third must search every partition: 463 orders from 289 logical partitions. On Azure, that's the fan-out cost. The emulator has one physical partition, so we can't see it here.
How do we avoid it?
- Key from your main filters - the hot path stays in one partition
- Pass partition_key - in the SDK, or the key in the WHERE clause
- Second access pattern - a copy keyed for it, synced by the change feed
- Global secondary index - that copy, managed for you (preview)
In Python, we pass partition_key=, or set enable_cross_partition_query=True on purpose.
A rare admin report can fan out. The hot path shouldn't.
Question 5: Someone asks about the 20 GB logical partition limit. What happens, and how do hierarchical partition keys help?
One logical partition, meaning one key value, holds at most 20 GB. Then writes to it fail. Support can raise it temporarily, without SLA guarantees meanwhile. The real answer is a key that never gets there.
Hierarchical keys allow up to three levels, like tenant id, then user id, with kind MultiHash. Each full key is a logical partition, and a tenant prefix can pass 20 GB and 10,000 RU/s. Point reads need the full key.
Let's try it. hpk.py creates user_sessions keyed by /tenantId then /userId, writes four sessions, and reads them back with the full key, with prefixes and with the tenant alone. We run python hpk.py.
from azure.cosmos import PartitionKey
from azure.cosmos.exceptions import CosmosHttpResponseError
from common import shop
sessions = shop.create_container_if_not_exists(
"user_sessions", partition_key=PartitionKey(
path=["/tenantId", "/userId"], kind="MultiHash"))
rows = [("s1", "contoso", "u1"), ("s2", "contoso", "u1"),
("s3", "contoso", "u2"), ("s4", "adatum", "u1")]
for sid, tenant, user in rows:
sessions.upsert_item({"id": sid, "tenantId": tenant,
"userId": user})
print(sessions.read()["partitionKey"])
s = sessions.read_item("s3", partition_key=["contoso", "u2"])
print("point read, full key:", s["id"])
for prefix in (["contoso"], ["contoso", "u1"]):
found = sessions.query_items("SELECT VALUE c.id FROM c",
partition_key=prefix)
print("prefix query", prefix, "->", list(found))
try:
sessions.read_item("s3", partition_key="contoso")
except CosmosHttpResponseError as e:
print("point read, tenant only:", e.status_code,
str(e).split(".")[0])$ python hpk.py
{'kind': 'MultiHash', 'paths': ['/tenantId', '/userId'], 'version': 2}
point read, full key: s3
prefix query ['contoso'] -> ['s1', 's2', 's3']
prefix query ['contoso', 'u1'] -> ['s1', 's2']
point read, tenant only: 400 (BadRequest) Invalid partition key value array providedThe key is 'kind': 'MultiHash', 'paths': ['/tenantId', '/userId']. The point read with the full key returns s3. Prefix queries work: ['contoso'] returns ['s1', 's2', 's3'], and ['contoso', 'u1'] returns ['s1', 's2']. A point read with only the tenant is rejected: the emulator says 400 (BadRequest) Invalid partition key value array provided. And a query that filters only on user id, the second level, fans out.
Plan for the limits:
- New containers only - can't add it later
- API for NoSQL only
- Connectors - limitations, e.g. Data Factory
Check every tool in your pipeline against Microsoft's list of connector limitations before you pick hierarchical keys.
Question 6: A teammate wants to change the partition key of a live container. What are the options?
We can't change it in place. The key is fixed at creation, and a key value on an item is immutable. Every option is a new container with the new key, plus a copy:
- Portal: change partition key - built on container copy jobs
- Container copy job - online or offline, server side (preview)
- Your own copy - change feed + upserts into the new container
Let's do our own copy. orders_v1 was keyed by /status, a mistake. migrate.py reads its change feed from the beginning, upserts each order into orders_v2, keyed by /customerId, then saves the continuation token. Meanwhile writes.py is the app, writing one order and updating another. We run python seed_v1.py, python migrate.py, python writes.py, python migrate.py again, and python verify.py.
from azure.cosmos import PartitionKey
from common import shop
v1 = shop.create_container_if_not_exists(
"orders_v1", partition_key=PartitionKey(path="/status")) # bad key
orders = [("o-1", "c1", "new"), ("o-2", "c2", "paid"),
("o-3", "c1", "shipped"), ("o-4", "c3", "new"),
("o-5", "c2", "shipped")]
for oid, cust, status in orders:
v1.upsert_item({"id": oid, "customerId": cust,
"status": status, "total": 12.5})
print("orders_v1:", len(orders), "orders, key",
v1.read()["partitionKey"]["paths"])import os
from azure.cosmos import PartitionKey
from common import shop, mine
v1 = shop.get_container_client("orders_v1") # key /status
v2 = shop.create_container_if_not_exists(
"orders_v2", partition_key=PartitionKey(path="/customerId"))
if os.path.exists("cf.token"): # resume
feed = v1.query_items_change_feed(
continuation=open("cf.token").read())
else: # first run
feed = v1.query_items_change_feed(start_time="Beginning")
changes = list(feed)
token = v1.client_connection.last_response_headers["etag"]
for doc in changes:
v2.upsert_item(mine(doc)) # same item, new key
open("cf.token", "w").write(token) # checkpoint after copy
print("copied", len(changes), "changes:", [d["id"] for d in changes])from common import shop
v1 = shop.get_container_client("orders_v1")
v1.upsert_item({"id": "o-6", "customerId": "c3", "status": "paid",
"total": 30.5})
o = v1.read_item("o-4", partition_key="new")
o["total"] = 99.5
v1.replace_item("o-4", o)
print("app: created o-6, updated o-4 in orders_v1")from common import shop
count = "SELECT VALUE COUNT(1) FROM c"
for name in ["orders_v1", "orders_v2"]:
c = shop.get_container_client(name)
n = list(c.query_items(count, enable_cross_partition_query=True))
print(name, "orders:", n[0], "| key",
c.read()["partitionKey"]["paths"])
o = shop.get_container_client("orders_v2").read_item(
"o-4", partition_key="c3")
print("orders_v2 point read o-4 by customer c3: total", o["total"])$ python seed_v1.py
orders_v1: 5 orders, key ['/status']
$ python migrate.py
copied 5 changes: ['o-1', 'o-2', 'o-3', 'o-4', 'o-5']
$ python writes.py
app: created o-6, updated o-4 in orders_v1
$ python migrate.py
copied 2 changes: ['o-6', 'o-4']
$ python verify.py
orders_v1 orders: 6 | key ['/status']
orders_v2 orders: 6 | key ['/customerId']
orders_v2 point read o-4 by customer c3: total 99.5The first run copies 5 changes. The app then created o-6, updated o-4. The second run resumes from the token and copies just those two: ['o-6', 'o-4']. Both containers now hold 6 orders, and orders_v2 reads o-4 by customer c3 with the new total 99.5.
A copy job does this on the server. Online mode reads the all versions and deletes change feed, so the source needs continuous backup. We stop writes on the source, let it drain, then run complete.
# Container copy job, online mode (preview, not run)
az extension add --name cosmosdb-preview
az cosmosdb copy create --resource-group rg-shop \
--job-name orders-rekey \
--src-account cosmos-shop --dest-account cosmos-shop \
--src-nosql database=shop container=orders_v1 \
--dest-nosql database=shop container=orders_v2 \
--mode Online
az cosmosdb copy show --resource-group rg-shop \
--account-name cosmos-shop --job-name orders-rekey
# processed count >= total count: stop writes, wait, then
az cosmosdb copy complete --resource-group rg-shop \
--account-name cosmos-shop --job-name orders-rekeyThe latest version feed has no deletes, so capture them with all versions and deletes mode, or use soft deletes. Stop writes for the cutover, and keep the old container until it's verified.
Scenario 7: A multi-tenant SaaS app stores data for thousands of tenants, a few of them very large. How do you design the partitioning?
A hierarchical key: tenant id, then user id.
- tenantId - level 1: thousands of values
- userId - level 2: small logical partitions
- Big tenants - spread over physical partitions
Let's load it. load_tickets.py writes 22 tenants: contoso with 40 users and 15 tickets each, fabrikam with 20 users and 10 each, and 20 small ones. Then saas.py checks the spread and runs the three access paths. We run python load_tickets.py and python saas.py.
from azure.cosmos import PartitionKey
from common import shop
tickets = shop.create_container_if_not_exists(
"tickets", partition_key=PartitionKey(
path=["/tenantId", "/userId"], kind="MultiHash"))
# tenant: (users, tickets per user)
tenants = {"contoso": (40, 15), "fabrikam": (20, 10)}
tenants.update({f"small{n:02}": (2, 3) for n in range(1, 21)})
for tenant, (users, per_user) in tenants.items():
for u in range(1, users + 1):
for t in range(1, per_user + 1):
tickets.upsert_item({
"id": f"{tenant}-u{u:02}-{t}", "tenantId": tenant,
"userId": f"u{u:02}", "status": "open"})
print("loaded", len(tenants), "tenants")from collections import Counter
from common import shop
tickets = shop.get_container_client("tickets") # /tenantId, /userId
rows = list(tickets.query_items("SELECT c.tenantId, c.userId FROM c",
enable_cross_partition_query=True))
tenants = Counter(r["tenantId"] for r in rows)
pairs = Counter((r["tenantId"], r["userId"]) for r in rows)
print("tickets:", len(rows), "| biggest tenants:",
tenants.most_common(2))
print("biggest logical partition:", max(pairs.values()), "tickets")
count = "SELECT VALUE COUNT(1) FROM c"
for prefix in (["contoso"], ["contoso", "u07"]):
n = list(tickets.query_items(count, partition_key=prefix))[0]
print("tenant dashboard" if len(prefix) == 1 else "user tickets",
prefix, n)
t = tickets.read_item("contoso-u07-3",
partition_key=["contoso", "u07"])
print("point read, full key:", t["id"], t["status"])$ python load_tickets.py
loaded 22 tenants
$ python saas.py
tickets: 920 | biggest tenants: [('contoso', 600), ('fabrikam', 200)]
biggest logical partition: 15 tickets
tenant dashboard ['contoso'] 600
user tickets ['contoso', 'u07'] 15
point read, full key: contoso-u07-3 openWe have 920 tickets, and the biggest tenants are ('contoso', 600) and ('fabrikam', 200). But the biggest logical partition is just 15 tickets: one contoso user. The tenant dashboard queries with the prefix ['contoso'] and gets 600. A user's list adds the user, ['contoso', 'u07'], and gets 15. The point read with the full key returns contoso-u07-3 open. All three stay on that tenant's partitions.
The rules for the app:
- tenantId in every query - routing and isolation
- userId alone - fans out
- Few tenants? - writes on one partition until 50 GB
I keep an eye on the largest tenants. One that needs its own throughput moves to its own container, with a copy job.
Key points to remember
- Same key value = one logical partition (max 20 GB); physical partitions: 10,000 RU/s, 50 GB
- A good key: high cardinality, even spread, in your main filters (orders: customerId)
- Hot partition: Normalized RU by PartitionKeyRangeID, then a better key
- Pass the partition key to avoid fan-out; hierarchical keys for big tenants
- No change in place: new container + copy (change feed, copy jobs in preview)
The full script
This is a Python script that writes every file and runs every command above in order against the Cosmos DB emulator: run pip install azure-cosmos first, and set COSMOS_ENDPOINT and COSMOS_KEY.
Show the full script that writes the files and runs every step
# ========================================================================
# Cosmos DB Interview Q&A #2 - Partitioning and partition keys
# TechBrothersIT. Names and sample data are invented for teaching.
# Every step below was run on the official Azure Cosmos DB Linux emulator (vNext, Docker image)
# with azure-cosmos 4.17.1; the outputs in the video and the blog post are the real outputs.
# To run it yourself:
# 1. docker run -p 8081:8081 -p 1234:1234 mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-latest
# and wait until its log says "Now listening on: http://0.0.0.0:8081".
# 2. pip install azure-cosmos
# 3. Set COSMOS_ENDPOINT=http://localhost:8081 and COSMOS_KEY to the emulator key
# (C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==),
# then run python CosmosDB_Interview_QA_02_Code.py. The scripts create their own databases and containers.
# Run it in an empty folder and on a freshly started emulator: it creates its files (and any local data
# files) there, and a second run on old data gives different output.
# Steps that restart the emulator say so in a comment: stop and start it by hand at that point.
# ========================================================================
import os, subprocess, time
def write(path, text):
if text is None:
if os.path.exists(path): os.remove(path)
return
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(text.strip("\n") + "\n")
def run(cmd, expect_error=False, allow_error=False, env=None):
print("\n$ " + cmd, flush=True)
rc = subprocess.run(cmd, shell=True, env=dict(os.environ, **(env or {}))).returncode
if rc != 0 and not (expect_error or allow_error):
raise SystemExit("stopped: this command was expected to succeed")
# Step 0: the starting files
write('common.py', 'import os\nfrom azure.cosmos import CosmosClient\n\nclient = CosmosClient(os.environ["COSMOS_ENDPOINT"],\n credential=os.environ["COSMOS_KEY"])\nshop = client.create_database_if_not_exists("shop")\n\n\ndef mine(item):\n """Your own fields only: drop _rid, _etag, _ts and friends."""\n return {k: item[k] for k in sorted(item) if not k.startswith("_")}\n')
# Step 1: An orders container keyed by /customerId: point read by id + key, wrong key, logical partitions
write('orders.py', """\
from collections import Counter
from azure.cosmos import PartitionKey
from azure.cosmos.exceptions import CosmosResourceNotFoundError
from common import shop
orders = shop.create_container_if_not_exists(
"orders", partition_key=PartitionKey(path="/customerId"))
for n, cust in enumerate(["c1", "c1", "c2", "c3", "c3", "c3"], 1):
orders.upsert_item({"id": f"o-{n}", "customerId": cust,
"total": 20 + n + 0.5})
print("partition key:", orders.read()["partitionKey"])
o = orders.read_item("o-4", partition_key="c3") # id + key
print("point read:", o["id"], o["customerId"], o["total"])
try:
orders.read_item("o-4", partition_key="c1") # wrong key
except CosmosResourceNotFoundError as e:
print("o-4 under c1:", e.status_code, "not found")
keys = orders.query_items("SELECT VALUE c.customerId FROM c",
enable_cross_partition_query=True)
print("items per logical partition:", sorted(Counter(keys).items()))
""")
run('python orders.py')
# Step 2: Candidate keys on 1,000 orders: distinct values and the biggest logical partition
write('candidates.py', """\
import random
from collections import Counter
from azure.cosmos import PartitionKey
from common import shop
random.seed(7)
STATUSES = ["new", "paid", "shipped", "shipped"] # half shipped
history = shop.create_container_if_not_exists(
"order_history", partition_key=PartitionKey(path="/customerId"))
for n in range(1000):
history.upsert_item({
"id": f"h-{n}", "customerId": f"c{random.randint(1, 400)}",
"status": random.choice(STATUSES),
"orderDate": f"2026-10-{random.randint(25, 30)}"})
docs = list(history.read_all_items())
print("orders:", len(docs))
for key in ["status", "orderDate", "customerId", "id"]:
sizes = Counter(d[key] for d in docs)
biggest = sizes.most_common(1)[0][1] / len(docs)
print(f"{key:<11} distinct {len(sizes):>4}"
f" | biggest logical partition {biggest:.1%}")
""")
run('python candidates.py')
# Step 3: A skewed key: one tenant holds 80 percent of the items
write('hot.py', """\
from collections import Counter
from azure.cosmos import PartitionKey
from common import shop
events = shop.create_container_if_not_exists(
"tenant_events", partition_key=PartitionKey(path="/tenantId"))
plan = {"contoso": 800, "fabrikam": 80, "adatum": 60,
"litware": 40, "woodgrove": 20} # events per tenant
for tenant, count in plan.items():
for n in range(count):
events.upsert_item({"id": f"{tenant}-{n}", "tenantId": tenant,
"type": "page_view"})
q = "SELECT VALUE c.tenantId FROM c"
sizes = Counter(events.query_items(
q, enable_cross_partition_query=True))
total = sum(sizes.values())
for tenant, n in sizes.most_common():
print(f"{tenant:<10} {n:>4} items {n / total:.0%}")
""")
run('python hot.py')
# Step 4: Single-partition vs cross-partition queries
write('xquery.py', """\
from common import shop
history = shop.get_container_client("order_history") # /customerId
count = "SELECT VALUE COUNT(1) FROM c"
print("c42, partition_key='c42':",
list(history.query_items(count, partition_key="c42")))
in_where = count + " WHERE c.customerId = 'c42'"
print("c42, key in the WHERE:", list(history.query_items(
in_where, enable_cross_partition_query=True)))
by_status = "SELECT c.id, c.customerId FROM c WHERE c.status = @s"
rows = list(history.query_items(
by_status, parameters=[{"name": "@s", "value": "shipped"}],
enable_cross_partition_query=True))
print("status shipped, no key:", len(rows), "orders from",
len({r["customerId"] for r in rows}), "logical partitions")
""")
run('python xquery.py')
# Step 5: Hierarchical partition key: full-key point read, prefix queries, a partial-key point read
write('hpk.py', """\
from azure.cosmos import PartitionKey
from azure.cosmos.exceptions import CosmosHttpResponseError
from common import shop
sessions = shop.create_container_if_not_exists(
"user_sessions", partition_key=PartitionKey(
path=["/tenantId", "/userId"], kind="MultiHash"))
rows = [("s1", "contoso", "u1"), ("s2", "contoso", "u1"),
("s3", "contoso", "u2"), ("s4", "adatum", "u1")]
for sid, tenant, user in rows:
sessions.upsert_item({"id": sid, "tenantId": tenant,
"userId": user})
print(sessions.read()["partitionKey"])
s = sessions.read_item("s3", partition_key=["contoso", "u2"])
print("point read, full key:", s["id"])
for prefix in (["contoso"], ["contoso", "u1"]):
found = sessions.query_items("SELECT VALUE c.id FROM c",
partition_key=prefix)
print("prefix query", prefix, "->", list(found))
try:
sessions.read_item("s3", partition_key="contoso")
except CosmosHttpResponseError as e:
print("point read, tenant only:", e.status_code,
str(e).split(".")[0])
""")
run('python hpk.py')
# Step 6: Changing the key: copy orders_v1 (/status) to orders_v2 (/customerId) with the change feed
write('seed_v1.py', """\
from azure.cosmos import PartitionKey
from common import shop
v1 = shop.create_container_if_not_exists(
"orders_v1", partition_key=PartitionKey(path="/status")) # bad key
orders = [("o-1", "c1", "new"), ("o-2", "c2", "paid"),
("o-3", "c1", "shipped"), ("o-4", "c3", "new"),
("o-5", "c2", "shipped")]
for oid, cust, status in orders:
v1.upsert_item({"id": oid, "customerId": cust,
"status": status, "total": 12.5})
print("orders_v1:", len(orders), "orders, key",
v1.read()["partitionKey"]["paths"])
""")
write('migrate.py', """\
import os
from azure.cosmos import PartitionKey
from common import shop, mine
v1 = shop.get_container_client("orders_v1") # key /status
v2 = shop.create_container_if_not_exists(
"orders_v2", partition_key=PartitionKey(path="/customerId"))
if os.path.exists("cf.token"): # resume
feed = v1.query_items_change_feed(
continuation=open("cf.token").read())
else: # first run
feed = v1.query_items_change_feed(start_time="Beginning")
changes = list(feed)
token = v1.client_connection.last_response_headers["etag"]
for doc in changes:
v2.upsert_item(mine(doc)) # same item, new key
open("cf.token", "w").write(token) # checkpoint after copy
print("copied", len(changes), "changes:", [d["id"] for d in changes])
""")
write('writes.py', """\
from common import shop
v1 = shop.get_container_client("orders_v1")
v1.upsert_item({"id": "o-6", "customerId": "c3", "status": "paid",
"total": 30.5})
o = v1.read_item("o-4", partition_key="new")
o["total"] = 99.5
v1.replace_item("o-4", o)
print("app: created o-6, updated o-4 in orders_v1")
""")
write('verify.py', """\
from common import shop
count = "SELECT VALUE COUNT(1) FROM c"
for name in ["orders_v1", "orders_v2"]:
c = shop.get_container_client(name)
n = list(c.query_items(count, enable_cross_partition_query=True))
print(name, "orders:", n[0], "| key",
c.read()["partitionKey"]["paths"])
o = shop.get_container_client("orders_v2").read_item(
"o-4", partition_key="c3")
print("orders_v2 point read o-4 by customer c3: total", o["total"])
""")
run('python seed_v1.py')
run('python migrate.py')
run('python writes.py')
run('python migrate.py')
run('python verify.py')
# Step 7: Multi-tenant SaaS: tickets keyed by /tenantId then /userId, a few large tenants
write('load_tickets.py', """\
from azure.cosmos import PartitionKey
from common import shop
tickets = shop.create_container_if_not_exists(
"tickets", partition_key=PartitionKey(
path=["/tenantId", "/userId"], kind="MultiHash"))
# tenant: (users, tickets per user)
tenants = {"contoso": (40, 15), "fabrikam": (20, 10)}
tenants.update({f"small{n:02}": (2, 3) for n in range(1, 21)})
for tenant, (users, per_user) in tenants.items():
for u in range(1, users + 1):
for t in range(1, per_user + 1):
tickets.upsert_item({
"id": f"{tenant}-u{u:02}-{t}", "tenantId": tenant,
"userId": f"u{u:02}", "status": "open"})
print("loaded", len(tenants), "tenants")
""")
write('saas.py', """\
from collections import Counter
from common import shop
tickets = shop.get_container_client("tickets") # /tenantId, /userId
rows = list(tickets.query_items("SELECT c.tenantId, c.userId FROM c",
enable_cross_partition_query=True))
tenants = Counter(r["tenantId"] for r in rows)
pairs = Counter((r["tenantId"], r["userId"]) for r in rows)
print("tickets:", len(rows), "| biggest tenants:",
tenants.most_common(2))
print("biggest logical partition:", max(pairs.values()), "tickets")
count = "SELECT VALUE COUNT(1) FROM c"
for prefix in (["contoso"], ["contoso", "u07"]):
n = list(tickets.query_items(count, partition_key=prefix))[0]
print("tenant dashboard" if len(prefix) == 1 else "user tickets",
prefix, n)
t = tickets.read_item("contoso-u07-3",
partition_key=["contoso", "u07"])
print("point read, full key:", t["id"], t["status"])
""")
run('python load_tickets.py')
run('python saas.py')
Part 3: Request Units, Throughput and Cost
The sample data on this page is invented for teaching. Every terminal output is the real output of the command above it, run on the official Azure Cosmos DB Linux emulator (vNext) with azure-cosmos 4.17.1. Features the emulator does not support are shown as code. Part of the Real Time Azure Cosmos DB Interview Questions and Answers series on TechBrothersIT. Features, limits and their status (GA or preview) are current as of October 2026; check the official Azure Cosmos DB documentation (learn.microsoft.com/azure/cosmos-db) for anything you quote in an interview.



No comments:
Post a Comment
Note: Only a member of this blog may post a comment.