Domain 2 of 4 · Chapter 1 of 12

Connect to and query Azure Cosmos DB for NoSQL with the SDK

From account URL to a single item

Six lines of Python stand between an Azure Cosmos DB for NoSQL account and a document in your hands.

Listing 1: the shortest complete read against a Cosmos DB for NoSQL account

from azure.cosmos import CosmosClient
from azure.identity import DefaultAzureCredential

client = CosmosClient("https://my-account.documents.azure.com:443/", DefaultAzureCredential())
database = client.get_database_client("catalog")
container = database.get_container_client("products")
item = container.read_item(item="sku-4417", partition_key="outdoor")

DefaultAzureCredential is the object that finds whatever Azure identity the code is running as, and the next section is where it earns its place; take it on trust for one more paragraph. Every other term in that listing is a resource level or the object that stands for it. Microsoft frames the model the same way: your code interacts with the account, which is the unique top-level namespace for your data, with databases that organize containers, with containers that hold a set of items, and with items, each of which is a JSON document in your container[1]. The figure below walks those four levels down the left and names the Python object that addresses each one on the right.

The three objects, and which of them talks to the service

CosmosClient is the only one of the three you construct. It takes the account URL and a credential, and it is the object that holds connections and caches. DatabaseProxy and ContainerProxy are obtained from it by name, and they are deliberately cheap: the SDK reference calls a DatabaseProxy an interface to a database that could, or couldn't, exist in the service yet[1], and says the class shouldn't be instantiated directly. Read that sentence as a warning as much as a definition. get_container_client("prodcuts") with a typo hands back a perfectly valid ContainerProxy; the mistake only becomes visible when read_item comes back as a 404. A proxy is a name you have written down, not a name the service has confirmed.

The cost of that arrangement is that the client is not cheap. Microsoft's Python performance guidance is that each client instance is thread-safe and performs efficient connection management and address caching, and therefore recommends a single instance of the client for the lifetime of the application[2]. The SDK reference adds that initialization is a heavy operation and that you should not construct clients as a way of validating credentials or network connectivity. Building a client inside a request handler is the single most common way to make a fast database feel slow.

What the partition key is doing in that last line

read_item took two arguments, and only one of them is the item's name. The other, partition_key, is the value that tells Cosmos DB which slice of the container owns the document. A container is created with a partition key path, /category in this example, and each item's value at that path decides which logical partition[3] the item belongs to: a logical partition is simply the set of items that share one partition key value. Scoping a query with the partition key below takes that apart properly. That value is the single most important input on this page: it is what turns a search across a whole container into a lookup inside one part of it, and later sections come back to it for queries as well as for reads.

Where this page stops

The rest of the pages in this domain assume the connection you just made. This one owns getting a client, proving who you are, and getting data back out with the right amount of work. The very next page, tuning request units, indexing, and consistency, owns what those operations cost and how you change that cost: throughput modes, the indexing policy, time to live, the five consistency levels and the session token. Storing and searching embeddings belongs to vector search in Cosmos DB, and reading items as they change belongs to the change feed processor. Request units, Cosmos DB's single normalized measure of what an operation costs in processing, memory and I/O[4], appear here only as a way to compare two ways of reading the same data, never as a tuning exercise.

Four levels, four things in your codeIn the accountIn your Python codeAccountthe account URLCosmosClientbuilt once, from URL plus credentialDatabasea name you chooseDatabaseProxyget_database_client(name)Containerhas a partition key pathContainerProxyget_container_client(name)Itemone JSON documentItem dictionaryread_item(...) or query_items(...)
The four resource levels of an account, and the Python object that names each one.

Two ways to prove who you are

Two kinds of credential carry essentially all real applications, and the choice between them changes far more than one constructor argument. (A third form exists: the Python client also accepts a dictionary of resource tokens, which are narrow, time-limited grants minted per user, and they are out of scope here.) Decide between them before writing any code, because only one of the two needs a second piece of configuration that has nothing to do with your application.

The first approach is the account key. You pass the key string where the credential goes, and that is the whole story: the key is a secret that already carries access to the data in the account, so possession is the authorization. The Python CosmosClient accepts it directly, and its reference notes the credential can be the account key, or a dictionary of resource tokens[5]. The cost is that a key names nobody. It cannot be scoped to one container, it cannot be traced to a person or a workload, and rotating it means touching every consumer.

The second approach is Microsoft Entra ID, Microsoft's identity service (formerly Azure Active Directory). You pass a credential object, usually DefaultAzureCredential, and the SDK obtains a token for the signed-in user, service principal, or managed identity. Microsoft's own guidance maps the principal type to where the code runs: a user identity or service principal for local development, a managed identity for code hosted on Azure, and a service principal for servers or clients outside of Azure[1].

The grant that catches people out

Authenticating is not authorizing, and in Cosmos DB the two live in different systems. Azure role-based access control (Azure RBAC) governs the control plane, which Microsoft defines as the ability to manage resources for an Azure service without managing data[6]. Reading and writing items is the data plane, defined on the same page as the ability to read and write data without the ability to manage resources in the account. Cosmos DB runs its own native role system for that second plane, and the reference is explicit that its role definitions are distinct from Azure role-based access control role definitions[7].

The practical consequence is the one the figure below traces. A principal holding Owner or Contributor on the account has ample control-plane power and still cannot read an item, because no Azure RBAC role contains Cosmos DB data actions. What it needs is a Cosmos DB data-plane role assignment, either a custom definition or one of the two built-ins: Cosmos DB Built-in Data Reader and Cosmos DB Built-in Data Contributor. The same page that shows DefaultAzureCredential in Python says so directly, pointing readers at the section on creating roles and assigning them to a principal ID.

Two details about those data actions worth knowing

The built-in reader carries four actions: readMetadata, items/read, executeQuery, and readChangeFeed. Two of them are less obvious than they look.

First, Microsoft.DocumentDB/databaseAccounts/readMetadata is not optional garnish. The SDKs issue read-only metadata requests during initialization and to serve specific data requests, fetching things like the account's regions, a container's partition key, and the addresses of its physical partitions (the machines the container's data actually sits on, which Cosmos DB manages for you), and the reference states this action must be allowed in every situation where your Azure Cosmos DB account is accessed through one of the Azure Cosmos DB SDKs[7]. A hand-built role definition that grants only items/read produces a client that fails before it reads anything.

Second, running a query needs two actions rather than one. The same reference notes that to perform NoSQL queries using the SDKs you must have both the executeQuery and the readChangeFeed permissions. That pairing looks like a documentation slip and is not one; it is a real requirement of the query path, and a custom read-only role that omits readChangeFeed will authenticate, point-read, and then fail on the first query_items.

One caveat on scope. Cosmos DB scopes a data-plane assignment to the account, a database, or a container, so / as the scope grants access to the entire account. That is convenient and it is also the widest grant available; prefer the narrowest scope your application can live with.

An item read under Microsoft Entra IDDefaultAzureCredentialobtains an Entra IDaccess tokenCosmos DB accountchecks the nativedata-plane roledataActionsitems/read, executeQuery,readMetadataItem returnedwith its requestunit chargeno data-plane role403 ForbiddenOwner and Contributordo not help here
Authentication gets the token; a separate Cosmos DB data-plane role assignment gets the item.

Point read or query: address versus search

Every read you write is one of two things: an address lookup, where you already know exactly which item you want, or a search, where you describe what you want and let the service find it. Cosmos DB prices those two very differently, and it names the first one a point read.

A point read is read_item(item=<id>, partition_key=<value>). It fetches one item from the partition that owns it without engaging the query engine. Microsoft states its cost plainly: reading a single item by its ID and partition key uses one request unit[4] for an item of about 1 KB, and the cost page adds that the only factor affecting the charge of a point read, besides the consistency level in force, is the size of the item retrieved. A 100 KB item costs 10 request units. Request units are Cosmos DB's normalized currency for the cost of an operation, and this page uses them only for comparison; how you provision and tune them belongs to the request units page.

A search is query_items(query=..., parameters=...). It compiles and runs SQL against an index. Microsoft orders read operations from most to least efficient as point reads, then a query with a filter clause within a single partition key, then a query without an equality or range filter, then a query without filters, and gives the design advice that follows from that ordering: make sure your item ID has a meaningful value so you can fetch your items with a point read (instead of a query) when possible[8]. Those middle two rungs have names worth learning, because the rest of this page uses them: a query that supplies a partition key value is an in-partition query, and one that does not is a cross-partition query. The figure below draws all three rungs against a container of four physical partitions, shading the ones each path has to touch.

The trap: a query that looks like a point read is not one

SELECT * FROM c WHERE c.id = @id with the partition key supplied returns the same single item as read_item, and readers reasonably assume Cosmos DB recognizes the shape and charges accordingly. It does not. The cost page is explicit that in the API for NoSQL, point reads can only be made using the REST API or SDKs, and that queries which filter on one item's ID and partition key aren't considered a point read. If you have both values, call read_item. Writing the equivalent SELECT is not a stylistic choice, it is a more expensive one.

So the decision is not stylistic. If both halves of the item's address are in hand, address it; reach for the query engine only when a predicate is genuinely all you have.

How much of the container each read path touchesThe read pathThe container, four physical partitionsPoint readid plus partition key value, no query engineP1P2P3P4In-partition querypartition key value supplied, one index readP1P2P3P4Cross-partition queryno partition key value, one query per partitionP1P2P3P4index readuntouched
The read ladder: a point read touches one item, a scoped query one partition's index, a fan-out every partition's index.

Writing the query: parameters, not concatenation

Once you have decided that a search is what you need, there is exactly one safe way to get a runtime value into it. When the value in a predicate comes from outside your code, it goes in the parameters list rather than into the query string.

Listing 2: a category-and-price filter, with both runtime values parameterized

# The value never touches the query text, so nothing the caller sends
# can change the shape of the query.
items = container.query_items(
    query="SELECT * FROM c WHERE c.category = @category AND c.price < @limit",
    parameters=[
        {"name": "@category", "value": user_category},
        {"name": "@limit", "value": 50},
    ],
    partition_key=user_category,
)
# ... iterate the result, see the paging section below

The parameters argument is a list of dictionaries with name and value keys, and the name must carry its leading @ to match the placeholder in the query text. Microsoft's description of the mechanism is that parameterized SQL provides robust handling and escaping of user input, and prevents accidental exposure of data through SQL injection[9]. Values can be any valid JSON, including arrays and nested objects, and because Cosmos DB is schemaless they are not type-validated at query time, which is worth knowing before you rely on a parameter to reject bad input for you.

There is a second, quieter reason to parameterize, and it is easy to overstate. In the Java SDK, the query plan for a single-partition query is cached on the client, keyed by the SQL query string, so an unparameterized query whose text changes on every call misses that cache. Microsoft documents this as a client-side cache, enabled by default for Java SDK version 4.20.0 and above[10], not as a service-side plan store, and the Python guidance on the same page offers a different lever for the same goal, covered in the next section. Parameterize for safety first; treat plan caching as a Java and .NET benefit rather than a universal one.

Scoping a query with the partition key

The difference between the second and third rungs of the read ladder in Point read or query above is one argument, and understanding exactly what makes a query land in one row rather than the other is the highest-value thing on this page.

Start with the vocabulary. A partition key has two components. The partition key path is the property location fixed when the container is created, written with a leading slash, for example /userId. The partition key value is what a given item holds at that path, for example Andrew. Microsoft describes the pair as exactly those two components[3], and notes that all items sharing a partition key value form one logical partition. Physical partitions are the machines underneath; they are an internal system implementation that Cosmos DB fully manages, and one or more logical partitions map to a single physical partition.

With that in place, the routing rule is short. When a query carries a filter on the partition key, Cosmos DB routes it to the physical partitions corresponding to the partition key values specified in the filter[11]. Without such a filter the query must fan out to every physical partition, and because each physical partition has its own index, you are effectively running one query per physical partition, whose results Cosmos DB then aggregates. There is no default global index in Cosmos DB, which is why the fan-out is unavoidable rather than a missed optimization.

The equality rule, and the range filter that quietly breaks it

A filter on the partition key is not automatically a scoping filter. Microsoft states the requirement directly: to be an in-partition query, the query must have an equality filter that includes the partition key[11]. WHERE c.DeviceId = 'XMS-0001' scopes. WHERE c.DeviceId > 'XMS-0001' does not, and it fans out exactly like a query with no partition key filter at all, despite mentioning the partition key by name. Adding an unrelated equality filter alongside a scoping one changes nothing about the routing; it only narrows the result.

In Python you have a second way to say the same thing, and it is the one Microsoft's query performance guidance recommends. Pass the value as the partition_key argument rather than relying on the query text.

Listing 3: the two ways to scope, and the one that also skips a round trip

# Scoped by the query text alone.
items = container.query_items(
    query="SELECT * FROM c WHERE c.city = 'Seattle' AND c.state = 'Washington'")

# Scoped by the argument: the SDK can skip fetching a query plan.
items = container.query_items(
    query="SELECT * FROM c WHERE c.city = 'Seattle'",
    partition_key="Washington")

To run a query the service needs a query plan, and building one costs a network request to the Cosmos DB gateway, the account's HTTPS front end, that adds latency. (The same word names the Python SDK's only connection mode; the gateway is the server, Gateway mode is the choice to route data requests through it.). The Python guidance says there is a way to remove that request for single-partition queries, and it is to specify the partition key value and pass it as the partition_key argument[10]. The partition_key argument in the SDK reference is described as the partition key at which the query request is targeted, and notes that if it is set to None the call performs a cross-partition query.

A word about enable_cross_partition_query

Older Cosmos DB SDKs required you to opt into fan-out explicitly by passing enable_cross_partition_query=True, and a great deal of sample code still carries it. The parameter still exists on query_items in the current Python SDK, described as allowing more than one request to execute the query, so seeing it is not evidence that the code is broken or ancient. It is simply no longer the thing that decides scope. What decides scope today is whether a partition key value reaches the service, through the argument or through an equality filter. Treat the flag as noise, and read the partition_key argument instead.

When the fan-out is fine

Avoiding cross-partition queries is worth effort only at size. You are charged a minimum of about 2.5 request units each time a physical partition's index is checked, even if no items in that partition match[11], so on a container with one or two physical partitions the fan-out costs very little. Microsoft's threshold for caring is concrete: try to avoid cross-partition queries if you plan to have over 30,000 request units provisioned or to store over 100 GB of data. Below that, the guidance is that having some cross-partition queries is inevitable, and that this is okay.

So scope deliberately when you can and stop worrying when you cannot: the partition key value in the argument or in an equality filter is the whole lever, and below the thresholds above the fan-out is not the problem worth your afternoon.

Paging: what a page actually promises

query_items returns before any results exist. What comes back is a pageable object, and each page you pull is a separate query execution on the service, with its own cost and its own end. The loop in the figure below is the shape all correct query code takes, whether you write it out or let a for statement hide it.

The knob for page size is max_item_count. Microsoft's description of it is careful, and the care is the point: it specifies the maximum number of items returned by a query, and it tells the query engine to return that number of items or fewer[12]. It is a ceiling, never a quota. Setting max_item_count=1000 does not promise pages of 1,000 items, and code that treats a short page as the end of the result set is wrong.

Several things can shorten a page besides simply running out of matches. The pagination page lists them: the container was throttled and there weren't available request units to return more results, the response was too large, the execution took too long, or it was simply more efficient for the query engine to return results in extra executions. It adds two consequences that surprise people. Running the same query twice may produce a different number of pages, and in some cases a query can return an empty page of results. An empty page is not an empty result set.

The rule that falls out of all this is the one Microsoft states: to ensure accurate query results, progress through all pages, and continue executing until there are no extra pages.

Continuation tokens, and the Python-specific limits

A continuation token is the bookmark that lets a later call resume where an earlier one stopped. Query executions are stateless on the server side and can be resumed at any time using the token, and as long as you stay on the same SDK version, tokens never expire. If a query returns a continuation token, there are more results; when it stops returning one, you are done.

Two restrictions bind. The general one is that continuation tokens cannot be used for queries with GROUP BY or DISTINCT, because those would require storing a significant amount of state, although a DISTINCT query can use them if you add an ORDER BY. The Python-specific one is narrower and easy to miss: continuation tokens for cross-partition queries are supported for streamable queries such as SELECT * FROM c WHERE ..., while aggregate cross-partition queries, meaning sorting, counting and distinct, do not support continuation tokens[12]. If your resume-from-a-token design depends on an ORDER BY across partitions in Python, it will not work, and the fix is architectural rather than a flag.

Reading the cost of each page

Every response carries its own charge, and in Python you read it from the connection rather than from the returned item.

Listing 4: pulling the request charge after an operation

existing_item = container.read_item(
    item="aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
    partition_key="61dba35b-4f02-45c5-b648-c6badc0cbd79",
)
# last_response_headers reflects the LAST operation on this connection,
# so read the charge immediately after the call you care about.
request_charge = container.client_connection.last_response_headers["x-ms-request-charge"]

The ContainerProxy exposes a last_response_headers dictionary that maps all the headers returned by the underlying HTTP API for the last operation executed, and the charge lives under the x-ms-request-charge key[13]. The name of that dictionary is the warning label: it holds the last operation's headers, so capture the value before issuing another call. For a multi-page query, each page is charged for the computation performed for that page, and summing across the pages gives the cost of the whole query.

Treat a page, then, as one execution's worth of answer and nothing more: it is capped but not filled, it costs what it costs, and only the absence of a continuation token tells you the query is finished.

One query, several executionsquery_items(...)one query, no results yetService executesreturns up to max_item_countPage deliveredplus a continuation tokenNo token leftthe result is completeToken present: execute again
Each page is its own execution; the continuation token is what says whether another one is owed.

The exceptions the SDK raises

Reads fail in ways you can branch on, and writes fail in one way you have to design for. Both are handled by the same habit: let the exception type carry the meaning instead of parsing a status code out of a message.

The Python SDK raises service-specific exceptions from azure.cosmos.exceptions. Three matter on this page. CosmosResourceNotFoundError is an HTTP error response with status code 404[14], raised when the item, container, or database you addressed is not there. CosmosResourceExistsError is the 409 you get when a create collides with something already present. CosmosAccessConditionFailedError is the 412 that the next part of this section is about. All three derive from CosmosHttpResponseError, which is the general failure and the right thing to catch when you have no specific recovery in mind.

Catching by type is what makes idempotent setup code readable. create_container raises CosmosResourceExistsError if a container with the same name already exists, while create_container_if_not_exists does not throw in that case, which Microsoft describes as useful for avoiding errors if you run the same code multiple times[15]. The convenience method absorbs the 409; it does not check or update existing settings or throughput if they differ from what you passed, so it is a create-or-attach, not a create-or-reconcile.

The habit to carry out of this is small and pays every time: branch on the exception type, never on a status code you parsed out of a message. One of those types, the 412, is a whole design problem rather than an error to log, and it is what the next section is about.

Preventing a lost update with _etag

Two writers, one item, and one of them is about to lose. This section is the design that stops it.

Here are the parties and the moment that matters. Two writers both read the same item, both change a different field in their own copy, and both send a replace. Without help, the second replace overwrites the first writer's change and nobody learns anything: the classic lost update. The condition for it is not concurrency in the abstract, it is that at least one writer acted on a copy that had already gone stale.

Cosmos DB solves this with optimistic concurrency control, which Microsoft defines as the mechanism that allows you to prevent lost updates and deletes[16]. The machinery is one system property. Every item carries an _etag whose value is generated and updated by the server every time the item is updated. You send the _etag you read back with your replace, as the if-match request header. If it matches the server's current value the item is updated; if it is no longer current, the server rejects the operation with an HTTP 412 Precondition failure, and the client can refetch the item to acquire the current version.

Call it what it is: this is not locking. Nothing is held between your read and your write, and no component decides who wins in advance. The server simply refuses a write whose stated assumption about the item's version has already been falsified, which is why the retry is your job rather than the SDK's.

The figure below traces the whole cycle, including the branch that matters: the 412 path back to a fresh read. In the current Python SDK the two pieces are the etag and match_condition keyword arguments on replace_item and upsert_item. The etag argument is an ETag value or the wildcard, used to check whether the resource has changed, and match_condition says what to do about it: MatchConditions.IfNotModified is the one that produces an if-match check.

Listing 5: the read, change, conditional replace, retry loop

from azure.core import MatchConditions
from azure.cosmos import exceptions

for attempt in range(5):
    item = container.read_item(item=item_id, partition_key=pk)
    item["stock"] = item["stock"] - 1          # your change, on your copy
    try:
        container.replace_item(
            item=item_id,
            body=item,
            etag=item["_etag"],                # the version you actually read
            match_condition=MatchConditions.IfNotModified,
        )
        break
    except exceptions.CosmosAccessConditionFailedError:
        continue                                # 412: re-read and try again
# ... give up or escalate after the loop

Two boundaries on that pattern. It protects a single item, and a logical partition defines the scope of a database transaction, so this is item-level protection in one region rather than anything broader. It is also distinct from conflict resolution: on an account configured for multiple write regions, a conflict resolution policy decides how independently committed versions are merged, which is a different mechanism from the _etag check you are making here. And note that multiple write regions and strong consistency do not combine at all, because accounts with multiple write regions can't use strong consistency[17].

A conditional replace, and the retry it forcesread_itemkeeps the _etagChange the copyin your processreplace_itemetag plus IfNotModifiedSavedthe etag was currentetag no longer currentHTTP 412someone else wrote firstre-read and try again
Optimistic concurrency: the server refuses a write whose _etag assumption has gone stale, and the retry is yours to write.

Exam-pattern recognition

Questions on this objective are usually a scenario plus four plausible calls, and they resolve on one of a small number of distinctions. These are the ones worth having ready.

A scenario that hands you an id and a partition key value. The answer is read_item, not a SELECT that filters on both. The distractor exists precisely because the SELECT returns the same item, and the reason it is wrong is documented: queries filtering on one item's ID and partition key aren't considered a point read.

A scenario about a client that authenticates but then fails on data. Look for Owner, Contributor, or a control-plane role in the setup. Azure RBAC governs managing the account; item access needs a Cosmos DB native data-plane role assignment such as Cosmos DB Built-in Data Reader. Adding another Azure RBAC role is always the wrong fix.

A scenario about a slow or expensive query with a well-chosen partition key. Check whether the partition key value actually reaches the service. A range comparison on the partition key does not scope, an equality comparison does, and passing partition_key= scopes and also lets the SDK skip the query plan round trip.

A scenario about missing results. If the code takes the first page and stops, that is the defect. max_item_count is a maximum, pages can be short or empty, and correct code drains the pageable until no continuation token comes back.

A scenario with concurrent updaters and a lost change. The answer is a conditional replace with the item's _etag and MatchConditions.IfNotModified, plus a retry on 412. Answers that reach for a stored procedure, a lock, or a stronger consistency level are solving a different problem: consistency levels govern what a reader sees across replicas, not whether two writers overwrite each other.

A scenario about connection mode or latency from Python. Direct mode is currently only supported on .NET and Java SDK platforms[18], and the Python client's connection_mode keyword currently supports only Gateway. Any answer that tunes Python to direct mode is wrong. Real Python-side levers are collocating the client in the same region as the account and reusing one client.

A scenario that asks you to join two entity types. A JOIN in Cosmos DB unwinds an array inside a single item; joins are scoped to that item and can't occur across multiple items and containers. The right answer is a data-modeling change, and Microsoft's own advice is to rework the model rather than attempt the join.

One habit is worth more than any of these individually: after any change to a query, read x-ms-request-charge and compare. Cosmos DB guarantees that the same query on the same data always costs the same number of request units, so the number is a stable measurement rather than a noisy one, and it settles arguments that reasoning alone will not.

The three ways to get items out of a container, ordered by how much of the container they touch

ConsiderationPoint read (address lookup)In-partition query (scoped search)Cross-partition query (fan-out search)
What you must already knowThe item id and its partition key valueThe partition key value, plus a predicate for everything elseOnly a predicate
The Python callcontainer.read_item(item=id, partition_key=value)container.query_items(query=..., parameters=..., partition_key=value)container.query_items(query=..., parameters=...) with no partition_key
How the service executes itFetches one item straight from the partition that owns it, without running the query engineRoutes the query to the physical partitions holding those partition key values and uses their indexRuns one query per physical partition against each partition's own index, then aggregates the results
What it returnsExactly one item, or a 404 exceptionA page-by-page iterable of matching itemsA page-by-page iterable of matching items
Relative costThe cheapest read: about 1 request unit for a 1 KB itemMore than a point read for the same item, because the query engine runsThe most expensive: a minimum of about 2.5 request units per physical partition checked, even when that partition matches nothing
Where it is the wrong choiceYou do not know the partition key value, or you need more than one itemThe filter on the partition key is a range rather than an equality comparison, which makes it a fan-out after allThe container is large (over 30,000 provisioned request units or over 100 GB) and this is a hot path

Decision tree

Choosing how to read from a containerDo you know the item idand its partition key value?yesPoint readread_item(item=id, partition_key=value)noCan you supply a partition key value,by argument or an equality filter?yesIn-partition queryquery_items(..., partition_key=value)noIs the container over 30,000 RU/sor over 100 GB?noCross-partition queryabout 2.5 RU per partition checkedyesRe-model the containersynthetic or hierarchical partition key

Sharp facts the exam loves — give these one last read before exam day.

Cheat sheet

Sharp facts the exam loves — scan these before test day.

CosmosClient is the SDK entry point, built from the account endpoint plus a credential and reused for the app lifetime

The azure-cosmos CosmosClient is constructed from the account URI and a credential — either the account key/resource token or a Microsoft Entra token via DefaultAzureCredential. A single CosmosClient is thread-safe and should be created once and reused, because per-request clients waste connections and warm-up cost.

Trap Constructing a fresh CosmosClient per request instead of reusing one for the app's lifetime.

5 questions test this
Item operations with Microsoft Entra ID require a Cosmos DB data-plane RBAC role, not just a management role

Passing DefaultAzureCredential authenticates with Entra ID, but reading or writing items also requires a Cosmos DB data-plane role assignment (for example Cosmos DB Built-in Data Reader or Data Contributor) scoped to the account. Azure RBAC management roles such as Contributor grant control-plane access only and do not authorize data operations.

Trap Owner/Contributor on the account does not grant data-plane read/write; you must assign a Cosmos DB built-in data role.

4 questions test this
Cosmos SDKs connect in Gateway (HTTPS) or Direct (TCP) mode; the Python SDK uses Gateway mode

Gateway mode routes every request through the account's HTTPS gateway on port 443, which is firewall-friendly, while Direct mode opens TCP connections straight to backend replicas for lower latency and higher throughput. The .NET and Java SDKs support both modes; the Python and JavaScript SDKs operate in Gateway mode.

Trap Expecting to switch the Python SDK into Direct mode for lower latency.

3 questions test this
Enabling multi-region (multi-master) writes on CosmosClient

Multi-region writes are enabled at the ACCOUNT level, and the client must then opt in as well. Microsoft's Python instruction is to set multiple_write_locations=True in the client initialization AND set preferred_locations to the regions the data is replicated into, ordered by preference: CosmosClient(url, credential, multiple_write_locations=True, preferred_locations=['West US','East US']). enable_endpoint_discovery defaults to True, so the SDK routes to the first available region in that list and falls back across regions on a regional event. You order the list yourself, shortest distance or best latency first; it is .NET's ApplicationRegion that auto-populates preferred locations by geo-proximity, not the Python argument.

Trap Enabling multiple write regions rules out Strong consistency (a multi-write account supports at most Bounded Staleness), and the conflict-resolution policy reconciles concurrent writes but does NOT do the region routing.

3 questions test this
The SDK models the account → database → container hierarchy as client-side proxy objects

From a CosmosClient you obtain a DatabaseProxy via get_database_client(db_id) and a ContainerProxy via get_container_client(container_id); these proxies are lightweight client-side references to a named resource. Obtaining one does not confirm the resource is there: Microsoft describes a DatabaseProxy as an interface to a database that could, or couldn't, exist in the service yet, so a mistyped database or container name surfaces only when you call a real operation on the proxy.

Trap Treating a returned ContainerProxy as proof the container actually exists.

3 questions test this
Each container has a partition key path whose value routes point operations and scopes queries

A container is created with a partition key path (for example /customerId), and each item's value at that path determines its logical partition. The SDK uses the partition key to route point reads and writes to the owning partition and to confine a query to a single physical partition when supplied.

6 questions test this
The SDK raises typed exceptions such as CosmosResourceNotFoundError (404) and CosmosResourceExistsError (409)

Resource errors surface as typed exceptions — CosmosResourceNotFoundError for a missing item/container (HTTP 404) and CosmosResourceExistsError for a create conflict (HTTP 409) — so code branches on the exception type instead of parsing status codes. Convenience calls like create_container_if_not_exists absorb the 409.

Optimistic concurrency with _etag and If-Match

Cosmos DB items carry a system _etag that changes on every write; to prevent a lost update, read the item, then issue a conditional replace passing that etag (the if-match request header, expressed in the current Python SDK as the etag keyword argument plus match_condition=MatchConditions.IfNotModified on replace_item or upsert_item). If another writer changed the item first, the server rejects the replace with HTTP 412 Precondition Failed and the app re-reads and retries. This is single-region item-level concurrency control, separate from the multi-region conflict-resolution policy.

A point read (read_item by id and partition key) is the cheapest, lowest-latency single-item fetch

read_item(item=id, partition_key=pk) performs a point read that retrieves one item directly from its partition, typically costing about 1 RU for a 1-KB document. When you know both the id and the partition key, a point read is always preferable to a query.

Trap SELECT * FROM c WHERE c.id=@id runs the query engine and costs more RUs than an equivalent point read.

6 questions test this
query_items runs a SQL query through the query engine and returns a paged iterator

container.query_items(query=..., parameters=...) executes a SQL query and returns a paged iterable of matching items. Queries engage the indexing and query engine and are billed per RU based on the work performed, which exceeds a point read when fetching a single known item.

Trap Reaching for query_items to fetch one item whose id and partition key are already known.

8 questions test this
Passing partition_key confines a query to one partition; omitting it fans out cross-partition

Supplying partition_key=value to query_items scopes execution to a single logical partition, which is cheaper and faster. Without it (or with enable_cross_partition_query=True in older SDKs) the query fans out to every physical partition, raising RU cost and latency.

Trap Omitting partition_key and still expecting the query to touch a single partition.

2 questions test this
Parameterized SQL passes values via a parameters list, preventing SQL injection

You author queries with @-prefixed placeholders and pass a parameters list of {"name": "@id", "value": v} entries to query_items, with the leading @ included in the name. Parameterization avoids string concatenation and is what provides robust handling and escaping of user input, preventing accidental exposure of data through SQL injection. Query plans are cached on the CLIENT, not by the service, keyed on the SQL query string, which is why an unparameterized query misses that cache; Microsoft documents the cache as enabled by default for the Java SDK 4.20.0+ and Spring Data 3.13.0+, and the documented Python lever for skipping the query-plan call is passing partition_key instead.

Trap Believing the service caches query plans, so a concatenated SQL string costs no more than a parameterized one.

4 questions test this
Cross-partition queries execute per-partition and merge into pages; in Python only streamable ones resume by continuation token

The SDK runs a cross-partition query as parallel sub-queries per physical partition and merges results into pages; a continuation token resumes the next page after the client stops iterating. ORDER BY and aggregates across partitions require the SDK to gather and merge partial results, and in the Python SDK those are exactly the queries continuation tokens do NOT cover: cross-partition tokens are supported for streamable queries such as SELECT * FROM c WHERE ..., while aggregate cross-partition queries (sorting, counting, distinct) do not support continuation tokens. Separately, no SDK supports continuation tokens for GROUP BY, or for DISTINCT without an ORDER BY.

Trap Expecting a continuation token to resume a cross-partition ORDER BY or aggregate query in Python.

4 questions test this
max_item_count bounds page size and each page reports its cost in x-ms-request-charge

max_item_count on query_items is an upper bound on the items returned per page: the engine returns that number of items or fewer, never more. A page can therefore come back short, or even empty, when throttling, response size, execution time, or the engine's own efficiency choices split the results further, so correct code drains every page instead of trusting one round trip. The RU cost of each page is reported in the x-ms-request-charge response header (read in Python from container.client_connection.last_response_headers) so query cost can be measured and tuned.

3 questions test this
Cosmos SQL JOINs are intra-document self-joins that unwind arrays within a single item

A JOIN in Cosmos DB SQL unwinds arrays inside one document (intra-document), not rows across documents, and queries can project shaped results and call built-in functions such as STARTSWITH, ARRAY_CONTAINS, and VectorDistance.

References

  1. Get started using Python - Azure Cosmos DB
  2. Performance tips for the Azure Cosmos DB Python SDK
  3. Partitioning and horizontal scaling in Azure Cosmos DB
  4. Request Units as a throughput and performance currency in Azure Cosmos DB
  5. azure.cosmos.CosmosClient class (azure-cosmos Python SDK reference)
  6. Connect using role-based access control and Microsoft Entra ID - Azure Cosmos DB for NoSQL
  7. Data plane security reference - Azure Cosmos DB for NoSQL
  8. Optimize request cost in Azure Cosmos DB
  9. Parameterized queries - query language for Cosmos DB
  10. Query performance tips for Azure Cosmos DB SDKs
  11. Query a container in Azure Cosmos DB
  12. Pagination - query language for Cosmos DB
  13. Find the request unit charge for a SQL query in Azure Cosmos DB
  14. azure.cosmos.exceptions module (azure-cosmos Python SDK reference)
  15. Create a container in Azure Cosmos DB for NoSQL using Python
  16. Database transactions and optimistic concurrency control in Azure Cosmos DB
  17. Consistency level choices in Azure Cosmos DB
  18. SQL SDK connectivity modes in Azure Cosmos DB