4. Reads and writes
DynamoDB · 4. Reads and writesThe API is small. Six operations do almost everything, and knowing exactly what each one costs is most of what it takes to use DynamoDB well.
READ WRITE GetItem one item PutItem replace an item Query one UpdateItem modify attributes partition DeleteItem remove an item Scan everything BatchWriteItem up to 25 BatchGetItem up to 100 TransactWriteItem up to 100GetItem
Section titled “GetItem”aws dynamodb get-item --table-name AppData \ --key '{"PK":{"S":"CUSTOMER#42"},"SK":{"S":"PROFILE"}}' • requires the FULL primary key — both PK and SK when the table has a composite key • returns one item or nothing • cost: 1 RCU per 4 KB, eventually consistent 2 RCU per 4 KB, strongly consistent • ProjectionExpression fetches fewer attributes but does NOT reduce the read cost — the whole item is read on the serverThat last line surprises people: projections save network bytes, not capacity units.
The operation the whole database is built around.
aws dynamodb query --table-name AppData \ --key-condition-expression 'PK = :pk AND begins_with(SK, :prefix)' \ --expression-attribute-values '{":pk":{"S":"CUSTOMER#42"}, ":prefix":{"S":"ORDER#2026-"}}' \ --scan-index-forward false --limit 20 KEY CONDITION (mandatory) PK = :value ← always equality AND SK = | < | <= | > | >= | BETWEEN | begins_with
OPTIONS ScanIndexForward=false newest first Limit items per page FilterExpression applied AFTER the read ExclusiveStartKey pagination cursor COST: the total size of items READ, before filtering, at 4 KB per RCU. A query that reads 1 MB and filters down to one item costs 1 MB of reads. SCAN reads EVERY item in the table or index.
LEGITIMATE NOT LEGITIMATE one-off migrations anything a user waits for exports and backfills "search" small config tables a report you run hourly parallel batch jobs because there is no index# if you must scan large tables, parallelise itaws dynamodb scan --table-name AppData --segment 0 --total-segments 8A scan on a large table is expensive, slow, and consumes the capacity your application needs. The right response to “I need a scan” is almost always a new index (chapter 7) or a different key design (chapter 8).
Consistency, per read
Section titled “Consistency, per read” EVENTUALLY CONSISTENT (default) • may read a replica that has not caught up • typically milliseconds behind, occasionally more after a failover • 0.5 RCU per 4 KB — HALF price
STRONGLY CONSISTENT (ConsistentRead=true) • reads the leader; sees every acknowledged write • 1 RCU per 4 KB • not available on global secondary indexes (ch 7) • not available across regions in global tables (ch 11) WHEN YOU NEED STRONG CONSISTENCY • read-modify-write without a conditional check • "did my write land?" immediately after writing • anything where a stale read causes a wrong decision (balances, inventory, permissions)
WHEN EVENTUAL IS FINE — most reads • feeds, profiles, catalogues, dashboards • anything already served behind a cacheChoosing eventual consistency where it is safe is the single cheapest performance and cost win available (chapter 6).
Writes
Section titled “Writes”# PutItem REPLACES the whole itemaws dynamodb put-item --table-name AppData \ --item '{"PK":{"S":"CUSTOMER#42"},"SK":{"S":"PROFILE"},"name":{"S":"Alice"}}' PutItem replaces the item entirely. attributes not supplied are GONE. UpdateItem modifies named attributes, leaves the rest alone, creates the item if absent. DeleteItem removes it.
COST: 1 WCU per 1 KB (rounded up), for all three. a 0.5 KB update costs the same as a 1 KB one.UpdateItem is usually what you want, and its expression language does real work:
aws dynamodb update-item --table-name AppData \ --key '{"PK":{"S":"CUSTOMER#42"},"SK":{"S":"PROFILE"}}' \ --update-expression 'SET #n = :n, updated = :t ADD loginCount :one' \ --expression-attribute-names '{"#n":"name"}' \ --expression-attribute-values '{":n":{"S":"Alice"},":t":{"S":"2026-09-23"}, ":one":{"N":"1"}}' SET assign, including list_append and if_not_exists(path, default) REMOVE delete an attribute ADD atomic numeric increment, or add to a set DELETE remove elements from a setADD is a genuine atomic counter — no read, no race, no transaction — and it is the right tool for view counts, quotas and rate counters.
Expression attribute names
Section titled “Expression attribute names” #n is required whenever an attribute name is a RESERVED WORD — and the reserved list is enormous (name, status, size, timestamp, count, year, …).
Symptom: ValidationException "Invalid …expression: Attribute name is a reserved keyword". Fix: alias every attribute name. some teams alias all of them, always, and never think about it again.Conditional writes
Section titled “Conditional writes”This is how DynamoDB does concurrency control, and it is the feature that makes single-item correctness possible without transactions.
# create only if it does not already exist--condition-expression 'attribute_not_exists(PK)'
# optimistic locking--condition-expression 'version = :expected'--update-expression 'SET version = :next, status = :s'
# business rule enforced in the database--condition-expression 'balance >= :amount' A failed condition returns ConditionalCheckFailedException …and still costs WRITE capacity. Conditions are cheap, not free.Chapter 9 builds transactions on top of these; most applications need conditions far more often than they need transactions.
Batch operations
Section titled “Batch operations” BatchGetItem up to 100 items, across tables BatchWriteItem up to 25 PutItem/DeleteItem (no UpdateItem, no conditions)
• NOT atomic — individual items can fail • failures return in UnprocessedItems, and you MUST retry them with backoff • the win is FEWER ROUND TRIPS, not lower capacity cost — each item is billed as usual# the loop everyone forgets to writewhile request: resp = client.batch_write_item(RequestItems=request) request = resp.get("UnprocessedItems") or None if request: time.sleep(backoff())Pagination and Limit, precisely
Section titled “Pagination and Limit, precisely” Limit = 50 means "read at most 50 ITEMS from storage" It does NOT mean "return 50 results after filtering"# what a page actually tells youresp = table.query(KeyConditionExpression=Key("PK").eq(pk), FilterExpression=Attr("status").eq("open"), Limit=50)
resp["Count"] # items returned AFTER the filterresp["ScannedCount"] # items READ — what you paid forresp["LastEvaluatedKey"] # present ⇒ there may be moreThe three rules
Section titled “The three rules” ① loop until LastEvaluatedKey is ABSENT, never until a page is empty ② treat LastEvaluatedKey as an opaque cursor; hand it to clients encoded, never construct one ③ there is no OFFSET. "page 7" requires reading pages 1–6. design for cursors, or precompute.A page can legitimately contain zero items and still have more data behind it — the filter removed everything in that 1 MB. Code that stops there loses data silently (chapter 14).
Return values, and getting work for free
Section titled “Return values, and getting work for free” ReturnValues on writes NONE (default) ALL_OLD the item as it was before UPDATED_OLD just the changed attributes, before ALL_NEW the item after the change UPDATED_NEW just the changed attributes, afterresp = table.update_item(Key=key, UpdateExpression="ADD stock :n", ExpressionAttributeValues={":n": -1}, ConditionExpression="stock > :zero", ReturnValues="ALL_NEW")
remaining = resp["Attributes"]["stock"] # no extra readThat is a read-modify-write with no read: the condition enforces the rule and ALL_NEW returns the result, in one billed write. Chapter 9 leans on this pattern heavily.
ReturnConsumedCapacity NONE | TOTAL | INDEXES ReturnItemCollectionMetrics SIZE → tells you how big the item collection is getting, which is the early warning for the 10 GB LSI limit (ch 7)PartiQL, briefly
Section titled “PartiQL, briefly”SELECT * FROM AppData WHERE PK = 'CUSTOMER#42' AND begins_with(SK, 'ORDER#');PartiQL gives a SQL-like syntax over the same operations. It is convenient in the console and in ad hoc work, and it is a genuine hazard in application code: a statement that looks like SQL can silently be a full table scan. The underlying rules do not change, so prefer the explicit API where cost matters.
What to take from this chapter
Section titled “What to take from this chapter”- Six operations cover almost everything: GetItem, Query, Scan for reads; PutItem, UpdateItem, DeleteItem for writes, plus batch and transactional forms.
GetItemneeds the full primary key;ProjectionExpressionsaves network bytes, not capacity.Queryreads one partition by exact PK with an optional sort-key range — and costs the bytes read before filtering.FilterExpressionis not an index, and it can return empty pages; always paginate untilLastEvaluatedKeyis absent.Scanreads everything and belongs in migrations and batch jobs, never on a request path.- Consistency is chosen per read: eventual is the default and half price; strong reads the leader and is unavailable on GSIs and across regions.
PutItemreplaces,UpdateItemmerges. Writes cost 1 WCU per KB, andADDgives a true atomic counter.- Alias attribute names with
#— the reserved-word list is long enough that aliasing everything is a reasonable habit. - Conditional expressions are DynamoDB’s concurrency control: existence checks, optimistic locking and business rules, enforced server-side, billed even when they fail.
- Batches are not atomic; you must retry
UnprocessedItemswith backoff.
Next: a request traced — from the SDK call through the request router to the storage node and back, with the milliseconds accounted for.