DynamoDB Update Expressions
An update expression tells UpdateItem how to mutate a single item: which
attributes to write, increment, delete, or fold into a set. There's no UPDATE … SET … WHERE — you name the item by its key and describe the change with four
clause keywords.
How do DynamoDB update expressions work?
A DynamoDB update expression tells UpdateItem how to mutate one item using four clauses. SET writes or overwrites an . ADD atomically increments a number or unions into a set. REMOVE deletes an attribute or one list element. DELETE removes specific members from a set. One call can carry all four at once.
SETwrites or overwrites an attribute — scalars, documents, and the function idiomsif_not_existsandlist_append.ADDdoes an atomic number increment or a set union, in one round trip, with no read-first.REMOVEdeletes an attribute outright (or a single list element by index).DELETEremoves specific members from a set — and only from a set.
Coming from SQL, the trap is reaching for SET for everything. ADD and
DELETE exist because read-modify-write on a counter or a set is a race you'll
lose under concurrency.
Pick the clause by what you're changing
One UpdateItem call can carry all four clauses at once, in the order
SET … REMOVE … ADD … DELETE. Each keyword appears at most once and takes a
comma-separated list of actions.
| Clause | Works on | Use it to |
|---|---|---|
SET | Any attribute | Write/overwrite a value or document field |
ADD | Number or Set only | Atomically increment, or union into a set |
REMOVE | Any attribute or list element | Delete an attribute; drop one list index |
DELETE | Set only | Remove specific members from a set |
ADD on a string and DELETE on a scalar are validation errors, not no-ops —
DynamoDB rejects the whole call. Per the
AWS update-expression reference,
ADD is restricted to numbers and sets, and DELETE to sets.
The worked example: a shopping cart
One item per cart, keyed by CartPK = "CART#c-9f21" and CartSK = "SUMMARY".
It tracks a running OrderTotal, a LineItems list, a PromoCodes string set,
and an ItemCount.
SET — write the scalars and documents
SET overwrites whatever was there. Add a line item to the list and bump the
total in the same call:
SET OrderTotal = :total,
LineItems = list_append(LineItems, :newItem),
UpdatedAt = :now
list_append(LineItems, :newItem) appends to the tail; flip the arguments —
list_append(:newItem, LineItems) — to prepend. The order of arguments is the
order of concatenation, nothing more.
There's a footgun in that first call: if the cart is brand new, LineItems
doesn't exist yet, and list_append on a missing attribute fails. Guard it with
if_not_exists:
SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem)
if_not_exists(LineItems, :empty) returns the current list if present, else the
fallback :empty (an empty list []). That makes the first add and every later
add use the same expression — a real reason these idioms exist.
ADD — increment the count, atomically
To bump ItemCount, do not read it, add one in your code, and SET it back.
That's a lost-update race: two concurrent adds both read 3, both write 4, and
you've dropped one. ADD does the arithmetic server-side:
ADD ItemCount :one
With :one = 1, this is an atomic counter. Concurrent calls serialise on the
item, so two adds land as +2. Pass a negative number to decrement. If
ItemCount is absent, ADD treats it as 0 first — so you never need to seed
the counter.
You can build this exact expression — names, typed values, and the marshalled
request — in the
DynamoDB expression builder without
hand-escaping a single #name or :value placeholder.
REMOVE — drop an attribute or one line item
REMOVE is how you delete an attribute entirely (there's no "set it to null" —
that just writes a NULL type). Clear an applied discount and drop the third
line item in one call:
REMOVE AppliedDiscount, LineItems[2]
LineItems[2] removes the element at index 2 and shifts everything after it
down — index 3 becomes 2, and so on. If you REMOVE two indices in one
expression, both are evaluated against the original list, so removing [2]
and [3] together drops the third and fourth elements as you'd expect.
DELETE — remove set members
PromoCodes is a string set, so a customer pulling one code uses DELETE, not
REMOVE. REMOVE PromoCodes would nuke the whole set; DELETE subtracts the
named members:
DELETE PromoCodes :pulled
With :pulled = the set {"SAVE10"}, only that member goes. Two rules bite
here: a set can never be empty, so deleting the last member removes the
PromoCodes attribute outright; and the value must be a set type matching the
attribute — a bare string is a type error.
Put it together
A "add item, apply a promo, bump the count" update is one call across three clauses:
SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem),
OrderTotal = OrderTotal + :price
ADD ItemCount :one
DELETE PromoCodes :expiredCode
Note OrderTotal = OrderTotal + :price — arithmetic inside SET works on the
existing value. It's not atomic the way ADD is for race-safety, but it reads
the current total server-side rather than round-tripping it through your code.
Pitfalls to avoid
SET-ing a counter you read first. UseADD— read-modify-write loses updates under concurrency. This is the most common cart/inventory bug.list_appendon a missing list. Wrap the target inif_not_existsor the first write fails.- Confusing
REMOVEandDELETE.REMOVEdrops the attribute;DELETEsubtracts members from a set. Mixing them deletes more than you meant. - Forgetting
UpdateItemis an upsert. If the key doesn't exist, it creates the item. Use aConditionExpression(attribute_exists(CartPK)) when you mean "update only".
For modelling the keys these expressions run against, see single-table design; to decide how you'll read the cart back, see query vs scan.
Build and copy any of these in the expression builder, then try DynoTable to run them against your own tables and watch the item change live.