Back to Blog

DynamoDB Expressions Deep Dive for the AWS Developer Associate Exam (DVA-C02 & DVA-C03): Key Condition vs Filter vs Projection Expressions, Update Expressions (SET / REMOVE / ADD / DELETE), Condition Expressions & Functions, Expression Attribute Names & Values, ReturnValues, Transactions & Idempotency, and PartiQL

A practitioner's guide to the DynamoDB expression language the Developer Associate exam actually tests: the five expression parameters and which API each belongs to, why # and : placeholders exist (reserved words, dots, hyphens, names starting with digits), the key condition rules (partition key equality, six sort key operators, begins_with only), why a filter expression never saves read capacity and why a page can come back empty with a LastEvaluatedKey, projection expressions and the Select parameter, the four update clauses with their evaluation-against-the-old-item rule, if_not_exists and list_append, the condition functions and operator precedence, ReturnValues and ReturnValuesOnConditionCheckFailure, the 300-operator and 4 KB limits, TransactWriteItems cancellation reasons and the 10-minute ClientRequestToken window, PartiQL full-table-scan traps, a 24-row scenario drill and an FAQ. Framed for both DVA-C02 (last day 30 November 2026) and DVA-C03 (from 1 December 2026).

By Sailor Team , November 4, 2026

Every Developer Associate study guide tells you that DynamoDB Query is cheaper than Scan, that a conditional write prevents overwrites, and that optimistic locking uses a version number. Our own DynamoDB guide for the Developer Associate says exactly that, because that post is about keys, indexes, capacity and the service as a whole. Those sentences are enough for the question that asks “which operation should the developer use”.

They are not enough for the questions that show you an actual request. The exam, and your day job, both put a KeyConditionExpression, a FilterExpression, an UpdateExpression or a ConditionExpression in front of you and ask why it fails, why it costs what it costs, or which single change makes it correct. Why does Query reject contains() on the sort key? Why does a filtered query return zero items and a LastEvaluatedKey? Why does SET Price = Price + :p throw a ValidationException on a brand-new item while ADD Price :p does not? Why did attribute_not_exists(pk) prevent a duplicate when the client retried? Why does a transaction fail with a list of cancellation reasons instead of one exception? This is the deep dive those questions deserve.

Read it once for the model, then use the scenario drill near the end to check whether the rules stuck.

Which Exam Version You Are Taking, and Where This Sits

As of this post’s publication AWS is replacing the Developer Associate exam. The exam guide states that the last day to take DVA-C02 is 30 November 2026 and that registration for DVA-C03 opened on 1 December 2026 for delivery from that date. Both versions award the same credential and your certification keeps its original expiry.

The DVA-C03 guide lists 50 scored and 15 unscored questions, a passing scaled score of 720, and these domain weights:

DomainDVA-C02DVA-C03
1. Development with AWS Services32%30%
2. Security26%26%
3. Deployment (C03: Testing and Deployment)24%22%
4. Troubleshooting and Optimization18%22%

What matters for this post: the DVA-C03 comparison page says DVA-C02’s four separate DynamoDB skills (partition key design, consistency models, query and scan operations, keys and indexing) were consolidated into one comprehensive DynamoDB skill, Skill 1.3.4, “configure DynamoDB to support applications (for example, by selecting high-cardinality partition keys, applying read consistency models, performing query and scan operations, by defining keys, attributes, and indexes)”. Consolidation is not de-emphasis. The comparison page says so explicitly, and expressions are the mechanism behind every one of those bullets. Everything below is version-neutral because the DynamoDB API did not change when the exam code did.

The Five Expression Parameters and Where Each Lives

DynamoDB has one expression grammar but five parameters that use it. Candidates lose points by putting the right condition in the wrong parameter.

ParameterUsed byPurposeEffect on capacity
KeyConditionExpressionQuery onlySelects which items to read, by partition key (and optionally sort key)Determines what is read, so it is the only expression that lowers cost
FilterExpressionQuery, ScanDiscards items after they were read, before they are returnedNone. You pay for everything read
ProjectionExpressionGetItem, BatchGetItem, Query, Scan, TransactGetItemsChooses which attributes are returnedNone. Capacity is computed on the full item size
ConditionExpressionPutItem, UpdateItem, DeleteItem, and every action inside TransactWriteItemsDecides whether the write happens at allA failed condition still consumes write capacity
UpdateExpressionUpdateItem and the Update action of a transactionDescribes the modificationNormal write cost on the larger of old and new item size

Two older parameter families, KeyConditions/QueryFilter/ScanFilter/Expected/AttributeUpdates and the ConditionalOperator that went with them, are documented as legacy. They still work, they cannot be mixed with expressions in the same request, and they are the wrong answer whenever an option offers an expression instead. The exam has treated expressions as the modern form since DVA-C01.

Expression Attribute Names (#) and Values (:)

The single most common cause of a ValidationException in a question stem is a placeholder problem, so learn the rules before the grammar.

Values always use colon placeholders

Literal values never appear inside an expression string. Price > 500 is invalid. You write Price > :limit and supply ExpressionAttributeValues: {":limit": {"N": "500"}}. Numbers travel as strings in the low-level JSON, which is why {"N": "500"} is quoted and why SDK document clients exist to hide it. Booleans must be compared explicitly: isVisible = :v with :v set to {"BOOL": true}. You cannot write a bare isVisible as a condition.

Names use pound placeholders, and sometimes you must

An expression attribute name begins with # followed by alphanumerics or underscores. The documentation lists the situations where a placeholder is mandatory, not optional:

SituationExampleWhy it breaks without #
Attribute name is a reserved wordComment, Views, Size, Name, Status, Year, Date, Timestamp, Type, User, Count, Data, Value, Key, OrderThe parser treats it as a keyword
Name contains a dot or hyphenSafety.Warning, shirt-sizeA dot is parsed as a document path separator, so Safety.Warning looks for attribute Warning inside map Safety and returns nothing
Name begins with a number5starTokens that start with a digit are not valid identifiers
Name contains a spacefirst nameWhitespace ends the token
Same name repeated many times#pr.FiveStar, #pr.ThreeStar, #pr.OneStarNot required, but shorter and allowed

The reserved-word list has several hundred entries and the exam does not expect you to memorise it. It expects you to recognise the pattern: a plausible-looking expression such as --projection-expression "Comment" or --filter-expression "Views >= :n" fails, and the fix is #c / #v plus --expression-attribute-names. The documentation’s own Query filter example uses #v for Views for exactly this reason.

Two further rules trip people up:

  • Nested paths need one placeholder per element. Inside an expression attribute name the dot is a literal character, so you cannot alias ProductReviews.OneStar as a single #x. You define #pr = ProductReviews and #1star = OneStar and write #pr.#1star.
  • Once defined, use it everywhere in that expression. If #p is Price you cannot also write Price directly in the same expression, and you cannot drop the #.

Document paths

Nested attributes are addressed with dot notation for map keys and zero-based bracket indices for list elements: ProductReviews.FiveStar[0], Devices.FireStick.DateWatched[0]. DynamoDB supports nesting up to 32 levels deep.

The limits that become distractors

LimitValue
Any single expression string4 KB
Any single expression attribute name or value placeholder255 bytes
All substitution variables in one request combined2 MB
Operators and functions in one expression300 (the error always reports “operator count: 301”)
Operands in an IN list100
Nested attribute depth32 levels
Item size400 KB including attribute names
Partition key value / sort key value2,048 bytes / 1,024 bytes

The 300-operator count is subtle: comparators, AND/OR/NOT, BETWEEN, IN, every function, and +/- in an update expression each count as one, but the = of a SET assignment does not.

Key Condition Expressions: What Query Will and Will Not Accept

Query reads one item collection, the items that share a partition key value, in sort-key order. The key condition expression is how you tell it which collection and which slice of it. The rules are strict and every one of them is testable.

  1. The partition key must appear with equality. ForumName = :name. Not >, not begins_with, not IN. If the scenario needs items across many partition key values, the answer is many Query calls, a GSI with a different partition key, or (reluctantly) a Scan.
  2. The sort key condition is optional and limited to seven forms: =, <, <=, >, >=, BETWEEN :lo AND :hi (inclusive), and begins_with(sk, :prefix). That is the complete list.
  3. Everything else is rejected in a key condition: <>, IN, contains, attribute_exists, size, OR, NOT. A question that shows KeyConditionExpression: "pk = :p AND contains(sk, :s)" is showing you a ValidationException, and the fix is to move the contains into a FilterExpression or to redesign the sort key so begins_with works.
  4. One condition per key attribute, and only key attributes. You cannot reference a non-key attribute in a key condition, and you cannot write two conditions on the sort key (use BETWEEN instead).
  5. Attribute names in key conditions have the same reserved-word rules. Size, Year, Date and Timestamp are popular sort key names and all four are reserved, so expect #y = :yyyy in the stem.
# Items in one thread posted in September 2015, newest first, strongly consistent
aws dynamodb query \
  --table-name Reply \
  --key-condition-expression "Id = :id AND begins_with(ReplyDateTime, :dt)" \
  --expression-attribute-values '{":id":{"S":"Amazon DynamoDB#DynamoDB Thread 1"},":dt":{"S":"2015-09"}}' \
  --no-scan-index-forward \
  --consistent-read

Three Query parameters ride alongside the key condition and show up in answer options:

  • ScanIndexForward (default true) controls sort order. false returns descending order, which is how you get “the most recent N items” when the sort key is a timestamp. The name says “scan” but it is a Query parameter.
  • ConsistentRead forces a strongly consistent read on a table or a local secondary index. It is not supported on a global secondary index; a request that sets it on a GSI fails. This is the single most reliable GSI-versus-LSI discriminator on the exam.
  • Limit caps the number of items evaluated, not the number returned after filtering (next section).

Results come back sorted by sort key: numerically if the sort key is a Number, otherwise by UTF-8 bytes. Each response is capped at 1 MB of data read, measured before any filter or projection is applied, and a non-empty LastEvaluatedKey means you must call again with it as ExclusiveStartKey. The SDK pagination mechanics are covered in the SDK retries and pagination guide; here the point is that the 1 MB cap is a read cap, which is why the next section matters.

Filter Expressions: Applied After the Read, Paid For Anyway

A filter expression is evaluated after Query or Scan has finished reading its page and before the results are returned. The documentation states the consequence plainly: a Query “consumes the same amount of read capacity, regardless of whether a filter expression is present”. The same sentence appears on the Scan page.

Everything the exam asks about filters follows from that one fact:

  • Filtering does not make a Scan cheap. A Scan with a very selective filter still reads every item in the table. “Add a filter expression to reduce RCU consumption” is always a distractor. The answer that reduces RCUs is a key condition (change the access pattern, add a GSI, or use a sparse index).
  • Limit applies before the filter. With Limit: 6 and a filter, DynamoDB reads six items and returns those of the six that match, possibly none. It does not keep reading until it has six matches.
  • A page can be empty and still have more data. Because the 1 MB cap and Limit are applied to the items read, a filtered Query can return Count: 0 with a non-null LastEvaluatedKey. The documentation’s own words: “a page can return zero matching items and still include a LastEvaluatedKey”. Code that stops paginating on an empty Items array has a bug; the only end-of-results signal is the absence of LastEvaluatedKey.
  • ScannedCount vs Count. ScannedCount is the number of items that matched the key condition before the filter; Count is the number after it. A large gap between them is the exam’s way of saying “this access pattern needs an index”.

Filter expressions use the full condition grammar: all six comparators including <>, BETWEEN, IN, AND/OR/NOT, and every function (attribute_exists, attribute_not_exists, attribute_type, begins_with, contains, size). One asymmetry is testable: a Query filter expression cannot reference the partition key or sort key (those belong in the key condition), while a Scan filter may reference any attribute, including key attributes.

When is a filter the right answer? When the item collection is already small, when you genuinely need to discard a minority of the items, or when the alternative is an extra index nobody will use. The documentation’s advice is to avoid Query where you “expect to retrieve a large number of items but also need to discard most of those items”.

Projection Expressions and Select: Less Data Back, Same Capacity

A projection expression lists the attributes to return: --projection-expression "Title, Price, #pr.FiveStar[0]". It reduces response size and network transfer. It does not reduce read capacity: DynamoDB computes consumed capacity “based on the number of items and the size of those items, not on the amount of data that is returned”, so the cost is “the same whether you request all of the attributes or just some of them”. Pair this with the filter rule and you have the complete capacity picture: the only ways to pay less on a read are to read fewer or smaller items, or to read eventually consistently.

Query and Scan also take a Select parameter:

Select valueReturnsNotes
ALL_ATTRIBUTESEvery attributeDefault on a table or an LSI. On a GSI it only works if the index projects ALL; otherwise the request fails
ALL_PROJECTED_ATTRIBUTESEvery attribute projected into the indexDefault when querying an index
COUNTCount and ScannedCount onlyStill reads the items and still pays for them
SPECIFIC_ATTRIBUTESThe attributes in ProjectionExpressionImplied when you supply a projection; specifying both Select and a projection with any other value is an error

SPECIFIC_ATTRIBUTES has an IAM twist that belongs to the Security domain: the dynamodb:Attributes condition key only lets you restrict a principal to particular attributes if the policy also forces dynamodb:Select to SPECIFIC_ATTRIBUTES (and, for writes, dynamodb:ReturnValues to a value that does not leak other attributes). The partition-key-scoped dynamodb:LeadingKeys pattern is covered in the DynamoDB guide; attribute-level restriction is its companion.

Condition Expressions: Making a Write Conditional

A condition expression is evaluated against the single item identified by the request’s key, immediately before the write. If it is true the write proceeds; if it is false DynamoDB returns a ConditionalCheckFailedException (HTTP 400, message “The conditional request failed”) and writes nothing. A failed conditional write still consumes write capacity, which is a favourite “why did my WCU graph move” distractor.

The functions

FunctionTrue whenNotes
attribute_exists(path)The item has that attributeAn attribute stored as the NULL type still exists. Some SDK mappers omit null fields entirely, in which case it does not
attribute_not_exists(path)The item lacks that attributeOn a key attribute, true only when no item with that key exists, which is how it prevents overwrites
attribute_type(path, :t)The attribute’s type matches :t:t must be a value placeholder holding one of S SS N NS B BS BOOL NULL L M
begins_with(path, :prefix)String starts with the prefixThe only function also allowed in a key condition
contains(path, :x)String contains substring, or set/list contains elementPath and operand must differ; contains(a, a) is an error
size(path)Returns a number to compareString length, binary byte count, set/list/map element count. size(VideoClip) > :v

Comparators are = <> < <= > >=, plus BETWEEN :lo AND :hi (inclusive) and IN (:a, :b, ...) with up to 100 values. Logical operators are AND, OR, NOT, with parentheses for grouping. Precedence, from highest to lowest: comparators, IN, BETWEEN, the functions, parentheses, NOT, AND, OR. The practical rule is the SQL one: a OR b AND c means a OR (b AND c), so write the parentheses you mean.

The three patterns the exam loves

Idempotent create (no overwrite). PutItem replaces an existing item silently. Add ConditionExpression: "attribute_not_exists(pk)" and the put succeeds only when no item with that key exists. For a composite key, testing attribute_not_exists on either key attribute is enough, because the condition is evaluated against the item identified by both. This is also the standard answer for “a retried request created a duplicate order”: the second attempt fails harmlessly with ConditionalCheckFailedException, which the application treats as success.

Update-only (no upsert). UpdateItem creates the item if it does not exist. If the scenario says “the update must fail if the record was deleted”, the fix is ConditionExpression: "attribute_exists(pk)".

Optimistic locking. Read the item including a version number, then write with ConditionExpression: "version = :expected" and UpdateExpression: "SET ..., version = version + :one". A concurrent writer who got there first changes the version, your condition fails, and you re-read and retry. The DynamoDB guide explains why this beats a pessimistic lock; here the mechanics are what matter: the condition and the increment must be in the same request to be atomic.

Getting the old item back when the condition fails

Since 2023, PutItem, UpdateItem and DeleteItem accept ReturnValuesOnConditionCheckFailure: ALL_OLD. When the condition fails, the exception carries an Item field containing the current item, and the API reference states there is no additional cost and no read capacity consumed. This replaces the old pattern of catching the exception and issuing a GetItem to find out why, which cost an extra read and could race. Inside a transaction, each action has the same parameter, and the cancelled item appears in that action’s CancellationReason.

aws dynamodb update-item \
  --table-name ProductCatalog \
  --key '{"Id":{"N":"456"}}' \
  --update-expression "SET Price = Price - :discount" \
  --condition-expression "Price > :floor" \
  --expression-attribute-values '{":discount":{"N":"75"},":floor":{"N":"500"}}' \
  --return-values-on-condition-check-failure ALL_OLD

Run against the documentation’s sample item (Price 650) this succeeds twice (575, then 500) and fails the third time, returning the item with Price 500 inside the exception.

Update Expressions: SET, REMOVE, ADD, DELETE

UpdateItem describes a change rather than sending a whole item. The update expression is made of up to four clauses, each introduced by a keyword, in any order, each keyword at most once. Within a clause, actions are comma-separated.

ClauseWorks onDoes
SET path = valueAny typeCreates or overwrites an attribute, list element or nested map key. Supports +/- on numbers and the functions if_not_exists and list_append
REMOVE pathAny typeDeletes an attribute, or a list element by index. Removing something that does not exist is not an error
ADD path valueNumber and sets onlyNumber: arithmetic add (negative subtracts), starting from 0 if absent. Set: union, same element type required
DELETE path subsetSets onlyRemoves elements. The subset must be a set; an empty set is an error

The evaluation rule everyone gets wrong

Multiple actions in one expression are all evaluated against the item as it was before the update. They are not applied left to right. The documentation’s example: on {"a": 1, "b": 2, "c": 3}, the expression REMOVE a SET b = a, c = b yields {"b": 1, "c": 2}. b = a and c = b both read the original values, and a is removed anyway. If a question shows a chained update that “should” cascade, it will not.

A related validation rule: two actions in one expression cannot target overlapping document paths. SET Reviews = :r, Reviews.Count = :c fails because the second path lives inside the first.

SET in detail

SET Price = :p                              overwrite or create
SET Price = Price - :discount               arithmetic on an existing number
SET Price = if_not_exists(Price, :default)  create only if absent, otherwise keep
SET #ri = list_append(#ri, :newItems)       append to the end of a list
SET #ri = list_append(:newItems, #ri)       prepend (swap the operands)
SET RelatedItems[1] = :v                    replace element 1
SET #pr.#5star[1] = :r5, #pr.#3star = :r3   nested map keys and list elements

Rules the exam checks:

  • if_not_exists and list_append are SET-only. They are not valid in a condition or filter expression. list_append takes two lists; to append a single value you wrap it in a one-element list.
  • Arithmetic needs an existing number. SET Counter = Counter + :one on an item where Counter is missing raises a ValidationException. Either SET Counter = if_not_exists(Counter, :zero) + :one or ADD Counter :one (which initialises to 0) handles the first write.
  • Setting a list index past the end appends. SET RelatedItems[7] = :v on a two-element list adds the element at the end; it does not pad with nulls. Multiple elements set in one action are sorted by index.
  • A nested map’s parent must exist. SET ProductReviews.FiveStar = :x when ProductReviews is absent fails with “The document path provided in the update expression is invalid for update”. The documented fix is to initialise "ProductReviews": {"M": {}} when you create the item. You cannot create the parent and the child in the same expression because the paths overlap.

REMOVE in detail

REMOVE Brand, InStock, QuantityOnHand removes attributes. REMOVE RelatedItems[1], RelatedItems[2] removes list elements, and the remaining elements shift down so [3] becomes [1]. Both indices refer to the original list because of the evaluation rule above.

ADD and DELETE in detail

ADD QuantityOnHand :five increments a number, creating it at 0 if needed. ADD Color :set unions a string set into Color, creating the attribute if absent; the incoming set must have the same primitive type as the existing one. DELETE Color :subset removes elements from a set. Neither clause works on strings, lists or maps.

The documentation says, in a note that reads like an exam answer: “In general, we recommend using SET rather than ADD to ensure idempotent operations.” ADD count :one retried after a timeout double-counts; SET count = :computedValue with a condition on the previous value does not. When a scenario mentions duplicates from retries on a counter, that note is the answer.

Atomic counters, properly

The classic atomic counter is UpdateExpression: "SET Replies = Replies + :one" (the API reference’s own example) or ADD Replies :one. Both are atomic on the server and both are not idempotent. If the scenario demands exactly-once semantics under retries, combine the increment with a condition on a request identifier or version, or make the writer idempotent at a higher level.

ReturnValues: Reading the Item in the Same Write

ReturnValues asks the write operation to hand back the item, which saves a separate GetItem. The API reference says there is no additional cost beyond response size and that no read capacity units are consumed; the returned values are strongly consistent.

OperationValid ReturnValues
PutItemNONE (default), ALL_OLD
UpdateItemNONE, ALL_OLD, UPDATED_OLD, ALL_NEW, UPDATED_NEW
DeleteItemNONE, ALL_OLD

ALL_NEW on an UpdateItem is how you implement “increment and return the new value” in one round trip. ALL_OLD on a DeleteItem is “delete and audit what was there”. UPDATED_NEW returns only the attributes touched by the update, which is the cheap option for a counter. An option offering ALL_NEW on PutItem is a distractor: it is not a valid value there, and after a put the new item is simply what you sent.

A matching IAM condition key, dynamodb:ReturnValues, lets a policy restrict which of these a principal may request, which closes the loophole where a principal allowed to update only one attribute uses ALL_NEW to read the rest.

Capacity in One Table

The capacity rules are owned by the DynamoDB guide, but the expression questions depend on four of them, so here they are in the form the questions use:

OperationCharged onRoundingEventually consistent
GetItemThat item’s sizeUp to the next 4 KB per itemHalf
BatchGetItemEach item separatelyPer itemHalf
Query / ScanThe sum of all items read, before filter or projectionOnce, on the totalHalf
Transactional readEach item, twice (prepare + commit)Per itemNot available
WriteThe larger of old and new item sizeUp to the next 1 KBNot applicable
Transactional writeEach item, twicePer itemNot applicable

Because Query rounds once on the total, forty 100-byte items cost 1 RCU strongly consistent, whereas forty GetItem calls cost 40. That is the real reason “use Query with a key condition instead of individual gets” wins, and it is the same reason a filter cannot save you: the sum was already computed over everything read.

Transactions: TransactWriteItems and TransactGetItems

Conditions become far more interesting when several items must change together. TransactWriteItems groups up to 100 actions across one or more tables in the same account and Region, up to 4 MB aggregate, and applies them all or none. Four action types exist: Put, Update, Delete, and ConditionCheck (assert something about an item you are not writing). Rules the exam tests:

  • No item twice. You cannot ConditionCheck and Update the same item in one transaction; that is a validation error, not a conflict.
  • Not across Regions. ACID holds only in the Region where the write was made. On a global table a replica can briefly show a partially applied transaction.
  • Not through indexes, and propagation to GSIs, Streams and backups is gradual and not atomic. Stream consumers must not assume they see a transaction’s records together.
  • Double capacity. Every item costs two writes (prepare and commit), and that capacity is consumed even when the transaction is cancelled, including a cancellation caused by a failed ConditionCheck. A transaction writing three 500-byte items needs 6 WCUs, not 3.
  • Idempotency via ClientRequestToken. The token is valid for 10 minutes after the request finishes. Repeat the same request with the same token inside that window and it succeeds without re-applying (and reports read capacity rather than write capacity). Repeat it with the same token but different parameters and you get IdempotentParameterMismatchException. AWS SDKs add the token automatically; if you are not using an SDK, the best-practice list tells you to add one yourself.

How a transaction fails

A cancelled transaction raises TransactionCanceledException, and the SDKs do not retry it. The exception carries a CancellationReasons list with one entry per action, in the order of TransactItems, each with a Code:

CodeMeaning
NoneThis action did not cause the cancellation
ConditionalCheckFailedThis action’s condition expression was false (and its item is included if ReturnValuesOnConditionCheckFailure: ALL_OLD was set)
TransactionConflictAnother transaction or write was in flight on this item
ProvisionedThroughputExceededThe table or index lacked capacity
ThrottlingErrorOn-demand table throttled
ItemCollectionSizeLimitExceededAn LSI item collection would exceed 10 GB
ValidationErrorBad input, oversized item, or similar

Reading that list is the troubleshooting answer: “which action failed and why” is in the response, not in CloudWatch.

Two related exceptions complete the picture. A plain PutItem/UpdateItem/DeleteItem that collides with an in-flight transaction on the same item fails with TransactionConflictException. A client that retries a transaction (with the same token) while the original is still running sees TransactionInProgressException, which the SDKs do retry by default, consuming more read capacity each time.

Isolation, in the words the exam uses

Other operationIsolation relative to a transaction
PutItem, UpdateItem, DeleteItem, GetItemSerializable
Another transactionSerializable
BatchGetItemRead-committed as a unit (each Get serializable)
Query, ScanRead-committed
BatchWriteItemNot serializable as a unit (each write serializable)

“Read-committed” means you never see uncommitted or rolled-back data, but a Query running during a transaction can return some items from before and some from after it. If a scenario needs a consistent snapshot of several items, the answer is TransactGetItems (up to 100 items, 4 MB), not a BatchGetItem or a Query. And even after a successful transaction, an eventually consistent read may briefly return the old state; use ConsistentRead: true when the next read must see the commit.

PartiQL: SQL Syntax, Same Engine, Same Traps

PartiQL gives you SELECT/INSERT/UPDATE/DELETE syntax over DynamoDB through three APIs: ExecuteStatement (one statement), BatchExecuteStatement (up to 25 statements, no atomicity), and ExecuteTransaction (up to 100 statements, atomic). It is the same storage engine underneath, so the same rules apply, with new spellings:

  • A SELECT without a partition key equality or IN in the WHERE clause is a full table scan. WHERE OrderID = 100, WHERE OrderID = 100 OR OrderID = 200 and WHERE OrderID IN [100, 300] become key-based reads. WHERE OrderID > 1, WHERE Address = 'x', and WHERE OrderID = 100 OR Address = 'x' scan everything. Omitting WHERE returns the whole table.
  • Index names are quoted: SELECT * FROM "Orders"."GSI1".
  • ORDER BY only works on key attributes, which mirrors the sort-key-order rule of Query.
  • Parameters use ? and the same expression functions (attribute_exists, begins_with, contains, size) exist in PartiQL form.
  • IAM is separate. PartiQL requires dynamodb:PartiQLSelect, PartiQLInsert, PartiQLUpdate, PartiQLDelete. The condition key dynamodb:FullTableScan lets you deny any SELECT that would scan, and dynamodb:EnclosingOperation with the value ExecuteTransaction lets you allow or deny transactional PartiQL specifically.

The exam framing is consistent: PartiQL is a convenience for developers who think in SQL; it does not change capacity consumption, consistency, or the need for a good key design, and a policy that denies full table scans is how you stop an ad-hoc SELECT * from burning a production table’s throughput.

Writing Expressions From an SDK

The low-level JSON above is what the exam shows. In code you usually use a document client or expression builder that generates it. Two shapes you should recognise:

# boto3 resource layer: builders generate names/values placeholders for you
from boto3.dynamodb.conditions import Key, Attr

table.query(
    KeyConditionExpression=Key("pk").eq("USER#42") & Key("sk").begins_with("ORDER#2026-"),
    FilterExpression=Attr("status").ne("CANCELLED"),
    ScanIndexForward=False,
    Limit=20,
)

table.update_item(
    Key={"pk": "USER#42", "sk": "PROFILE"},
    UpdateExpression="SET #n = :name, version = version + :one",
    ConditionExpression="version = :expected",
    ExpressionAttributeNames={"#n": "name"},
    ExpressionAttributeValues={":name": "Ada", ":one": 1, ":expected": 7},
    ReturnValues="ALL_NEW",
)
// AWS SDK for JavaScript v3 DocumentClient: native JS values, same expression strings
await ddb.send(new UpdateCommand({
  TableName: "Products",
  Key: { Id: 789 },
  UpdateExpression: "SET #ri = list_append(if_not_exists(#ri, :empty), :new)",
  ExpressionAttributeNames: { "#ri": "RelatedItems" },
  ExpressionAttributeValues: { ":empty": [], ":new": ["Screwdriver"] },
  ReturnValues: "UPDATED_NEW",
}));

The second example shows the idiom for “append to a list that may not exist yet”: nest if_not_exists inside list_append. Note status and name would both need # placeholders in a raw expression; the boto3 Attr builder adds them for you, which is why hand-written strings fail where builders succeed.

Scenario Drill

Cover the right-hand column and answer.

#ScenarioAnswer
1Query with KeyConditionExpression: "pk = :p AND contains(sk, :s)" failscontains is not a key condition operator; use begins_with or move it to FilterExpression
2--projection-expression "Comment" returns a ValidationExceptionComment is reserved; use #c with ExpressionAttributeNames
3--projection-expression "Safety.Warning" returns nothing for an attribute literally named Safety.WarningThe dot is a path separator; alias the whole name with #sw
4A filtered Query returns Count: 0 but has a LastEvaluatedKeyPage cap applied before the filter; keep paginating until LastEvaluatedKey is absent
5Team adds a FilterExpression to a Scan to cut RCU costs; costs unchangedFilters run after the read; capacity is based on items read. Redesign keys or add a GSI
6Query on a GSI with ConsistentRead: true failsGSIs are eventually consistent only; use the table or an LSI
7Need the latest 10 orders for a customerQuery with ScanIndexForward: false and Limit: 10 on a timestamp sort key
8SET Counter = Counter + :one fails on a new itemArithmetic needs an existing number; use if_not_exists(Counter, :zero) + :one or ADD Counter :one
9Retried UpdateItem double-counts a tallyADD/arithmetic are not idempotent; docs recommend SET with a condition, or an idempotency key
10SET ProductReviews.FiveStar = :v fails with “document path … invalid for update”Parent map absent; initialise {"M": {}} on create, or SET ProductReviews = :map first in a separate request
11Expression REMOVE a SET b = a, c = b on {a:1,b:2,c:3}Result {b:1, c:2}; all actions read the pre-update item
12Prepend an element to a listSET l = list_append(:new, l) (operand order reversed)
13ADD Tags :t fails on a List attributeADD supports Number and sets only; use list_append
14Prevent a PutItem from overwriting an existing orderConditionExpression: "attribute_not_exists(pk)"
15UpdateItem must not create a missing recordConditionExpression: "attribute_exists(pk)"
16Two users overwrite each other’s editsOptimistic locking: condition on version, increment it in the same update
17After a ConditionalCheckFailedException the code does a GetItem to see the current stateSet ReturnValuesOnConditionCheckFailure: ALL_OLD; the item comes back in the exception at no read cost
18Increment and return the new value in one callUpdateItem with ReturnValues: ALL_NEW or UPDATED_NEW
19PutItem with ReturnValues: ALL_NEWInvalid; PutItem supports only NONE and ALL_OLD
20Transaction writing 3 × 500-byte items per second; WCUs needed6 (two writes per item)
21Transaction fails with TransactionCanceledException; which action was the problem?Read CancellationReasons; the entry with a non-None code, e.g. ConditionalCheckFailed
22Same transaction retried 30 minutes later with the same ClientRequestTokenTreated as a new request; the token is valid for 10 minutes
23Need a consistent snapshot of 5 items while transactions are writing themTransactGetItems; BatchGetItem is only read-committed as a unit
24SELECT * FROM "Orders" WHERE Total > 500 is slow and expensiveNon-key predicate means a full table scan; add a condition on the partition key or deny with dynamodb:FullTableScan

Mistakes That Cost Points

  1. Thinking a filter is an index. It is a post-read sieve. Nothing about it changes what DynamoDB reads or bills.
  2. Stopping pagination on an empty page. The end signal is a missing LastEvaluatedKey, never an empty Items list.
  3. Using a literal in the expression string. Every value goes through :placeholder. Every awkward attribute name goes through #placeholder.
  4. Expecting left-to-right evaluation in an update expression. Every action sees the pre-update item.
  5. Reaching for ADD when idempotency matters. ADD is convenient and non-idempotent, and the documentation says to prefer SET.
  6. Forgetting that cancelled transactions still bill. Capacity is consumed on prepare even when the commit never happens, and ConditionCheck failures count.
  7. Confusing TransactionCanceledException with TransactionConflictException. The first is the transaction’s own failure (with reasons); the second is what a plain write gets when it collides with a transaction.
  8. Treating PartiQL as a different engine. Same keys, same capacity, same scan trap, with its own IAM actions.

Practise Until the Expressions Read Themselves

The exam does not ask you to write an expression from scratch. It shows one and expects you to see the reserved word, the misplaced contains, the ADD that should be a SET, or the filter that cannot save capacity, in under a minute. That reflex comes from seeing dozens of flawed requests, not from re-reading the grammar.

The AWS Developer Associate mock exam bundle is built for that: eight full-length exams whose DynamoDB questions put real KeyConditionExpression, UpdateExpression and TransactWriteItems payloads in front of you, with explanations that name the exact rule each option breaks. Pair it with the DynamoDB guide for keys and capacity, the SDK retries and pagination guide for the client side, the event-driven services guide for what sits around the table, and the exam guide and study plan for scheduling your attempt on either side of the 30 November 2026 cut-over.

Frequently Asked Questions

Will DynamoDB expressions still be on the DVA-C03 exam?

Yes. The DVA-C03 guide consolidates DynamoDB into one skill, 1.3.4, that still names query and scan operations, read consistency, keys, attributes and indexes, and the comparison page states that consolidation “does not result in a decreased emphasis on essential skills”. The expression grammar is how every one of those bullets is exercised, so expect it on both DVA-C02 (until 30 November 2026) and DVA-C03 (from 1 December 2026).

What is the difference between a key condition expression and a filter expression?

A key condition decides what Query reads: partition key equality plus an optional sort key range using =, <, <=, >, >=, BETWEEN or begins_with. A filter expression discards items after they are read and before they are returned, can use any attribute and any operator, and never reduces consumed capacity.

Why does my filtered Query return no items but a LastEvaluatedKey?

Because the 1 MB page limit and Limit are applied to the items read, before the filter. A page of read items can contain zero matches. The only way to know the result set is finished is a response with no LastEvaluatedKey.

When must I use ExpressionAttributeNames?

When the attribute name is a DynamoDB reserved word, contains a dot or hyphen, begins with a digit, or contains a space. You may also use one to shorten repeated references. Values, by contrast, must always use : placeholders.

What is the difference between ADD and SET in an update expression?

SET assigns a value to any attribute type and supports +/-, if_not_exists and list_append. ADD only works on numbers (arithmetic, starting at 0 if absent) and sets (union). The documentation recommends SET over ADD for idempotent operations because a retried ADD applies twice.

Does a failed conditional write cost anything?

Yes. A write rejected by ConditionExpression still consumes write capacity, and a cancelled TransactWriteItems still consumes the prepare-phase capacity for every item it attempted. Requesting the old item via ReturnValuesOnConditionCheckFailure: ALL_OLD or ReturnValues adds no cost and consumes no read capacity.

How long is a TransactWriteItems ClientRequestToken valid?

Ten minutes after the original request finishes. A repeat with the same token inside that window returns success without re-applying the writes; a repeat with the same token but different parameters raises IdempotentParameterMismatchException; after ten minutes the same token is treated as a new request.

Can PartiQL avoid full table scans?

Only if the WHERE clause pins the partition key with equality or IN. Any statement that predicates solely on non-key attributes, or ranges over the partition key, scans the whole table. The dynamodb:FullTableScan IAM condition key lets you deny such statements outright.

Limited Time Offer: Get 80% off all Mock Exam Bundles | Sale ends in 7 days. Start learning today.

Claim Now