DEV Community

Cover image for DynamoDB Update Expressions: SET, REMOVE, ADD, DELETE
DynoTable
DynoTable

Posted on Edited on Originally published at dynotable.com

DynamoDB Update Expressions: SET, REMOVE, ADD, DELETE

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 multi-row
UPDATE … SET … WHERE — you name a single item by its full 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 Attribute. 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.

  • SET writes or overwrites an attribute — scalars, documents, and the function idioms if_not_exists and list_append.
  • ADD does an atomic number increment or a set union, in one round trip, with no read-first.
  • REMOVE deletes an attribute outright (or a single list element by index).
  • DELETE removes 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 any order. 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:

Example Notes
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:

Example Notes
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:

Example Notes
ADD ItemCount :one

With :one = 1, this is an atomic counter. Concurrent calls serialize 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.

On on-demand in us-east-1, each ADD bills 1 WCU per KB of the item after
the write (rounded up). A 1 KB cart summary with one ADD ItemCount :one costs
1 WCU — the same row with a read-modify-write SET would cost the same WCU
but lose under concurrency. Size the item in the
item-size calculator.

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.

Edit the preset below — a SET plus an atomic ADD — and watch the
UpdateExpression rebuild as you go:

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:

Example Notes
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:

Example Notes
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:

Example Notes
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 just as atomic and race-safe as ADD: DynamoDB evaluates
OrderTotal + :price server-side against the current value, so concurrent calls
serialize on the item instead of round-tripping through your code.

Pitfalls to avoid

  • SET-ing a counter you read first. Use ADD — read-modify-write loses updates under concurrency. This is the most common cart/inventory bug.
  • list_append on a missing list. Wrap the target in if_not_exists or the first write fails.
  • Confusing REMOVE and DELETE. REMOVE drops the attribute; DELETE subtracts members from a set. Mixing them deletes more than you meant.
  • Forgetting UpdateItem is an upsert. If the key doesn't exist, it creates the item. Use a ConditionExpression (attribute_exists(CartPK)) when you mean "update only".

For modeling 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.

Top comments (0)