GetItemQueryScanPutItemUpdateItemCondition ExpressionsConsistency

Reading & Writing Data

The operations available in DynamoDB, their cost implications, consistency models, and the condition expressions that make writes safe.

45 min read9 sections
01

Reading: GetItem & BatchGetItem

GetItem is the fastest and cheapest read operation — a single item lookup by its complete primary key. If you know the exact key, always use GetItem over Query.

GetItem

GetItem Characteristics

  • Fetch a single item by its complete primary key (PK + SK if composite)
  • Strongly consistent or eventually consistent (ConsistentRead parameter)
  • ProjectionExpression: return only specific attributes — reduces bandwidth, NOT RCU
  • RCU cost: ceil(itemSize / 4 KB) for strong, half for eventual
  • Returns empty if item doesn't exist (no error thrown)
get-item-example.tstypescript
// GetItem — exact primary key lookup
const result = await dynamodb.getItem({
  TableName: "Orders",
  Key: {
    PK: { S: "USER#123" },
    SK: { S: "ORDER#2024-01-15#abc" }
  },
  ConsistentRead: true,  // strongly consistent (2× RCU)
  ProjectionExpression: "orderId, #s, total",  // only these attributes
  ExpressionAttributeNames: { "#s": "status" } // 'status' is reserved word
});

BatchGetItem

BatchGetItem Characteristics

  • Fetch up to 100 items or 16 MB in one API call
  • Items can be from multiple tables
  • Internally parallel — DynamoDB fetches in parallel
  • Not a transaction — each GetItem is independent
  • Partial failure: UnprocessedKeys returned — retry with exponential backoff
02

Reading: Query

Query fetches items sharing a partition key, optionally filtered by sort key conditions. It is the primary tool for fetching collections of related items.

query-example.tstypescript
// Query — fetch all orders for a user in January 2024
const result = await dynamodb.query({
  TableName: "Orders",
  KeyConditionExpression: "PK = :pk AND SK BETWEEN :start AND :end",
  ExpressionAttributeValues: {
    ":pk": { S: "USER#123" },
    ":start": { S: "ORDER#2024-01-01" },
    ":end": { S: "ORDER#2024-01-31" }
  },
  ScanIndexForward: false,  // reverse sort (newest first)
  Limit: 10,               // max 10 items to evaluate
  FilterExpression: "#s = :status",  // applied AFTER fetch
  ExpressionAttributeNames: { "#s": "status" },
  ExpressionAttributeValues: { ...prev, ":status": { S: "DELIVERED" } }
});

FilterExpression Does NOT Reduce Cost

FilterExpression is applied AFTER items are read from the partition. You pay RCU for ALL items that match the KeyConditionExpression, even if FilterExpression discards 99% of them. If you're filtering heavily, your key design is wrong.

Query Mechanics

Query Mechanics

  • KeyConditionExpression: PK = value [AND SK condition] — required
  • SK conditions: =, <, <=, >, >=, BETWEEN, begins_with
  • Returns items sorted by sort key ascending by default
  • ScanIndexForward: false — reverse sort order
  • Limit: max items to EVALUATE (not items to return — confusing!)
  • Pagination: LastEvaluatedKey → pass as ExclusiveStartKey in next call
  • Can query base table or any GSI/LSI
1

Key Condition

DynamoDB finds the partition, applies sort key condition to narrow items

2

Read Items

All matching items read from partition (RCU charged here)

3

Filter Expression

Non-key filters applied — items removed from response but already charged

4

Limit Check

If Limit reached during evaluation, stop and return LastEvaluatedKey

5

Return

Filtered items returned to client with pagination token if more exist

03

Reading: Scan

Scan reads every item in a table or index. No key condition — it reads everything. This is almost always the wrong operation for production code paths.

AspectQueryScan
Key conditionRequired (PK + optional SK)None — reads everything
CostOnly items in partitionEntire table charged
PerformancePredictable, fastUnpredictable, slow at scale
Use caseProduction queriesMigrations, analytics, exports
ParallelismNot neededParallel scan (Segment/TotalSegments)

When Scan is Acceptable

When Scan is Acceptable

  • One-time data migrations (not recurring)
  • Small tables (< 1000 items) where cost is negligible
  • Background analytics jobs (off-peak, rate-limited)
  • Table exports to S3 (use DynamoDB Export instead)
  • Development/debugging (never in production hot paths)

Parallel Scan

Parallel scan divides the table into N segments, scanning each in parallel. Faster but consumes N× the RCU simultaneously. Use for large one-time migrations, never for user-facing requests.

04

Consistency Models

ModelBehaviorRCU CostAvailability
Eventually Consistent (default)May return stale data (usually < 1 second old)Half cost (0.5 RCU per 4 KB)Base table + GSI + Global Tables
Strongly ConsistentAlways returns latest committed dataFull cost (1 RCU per 4 KB)Base table + LSI only
Transactional ReadSnapshot isolation across multiple items2× cost (2 RCU per 4 KB)Base table only, same region
📰

The News Wire Analogy

Eventually consistent reads are like checking a news aggregator — the story might be 30 seconds behind the wire service. Strongly consistent reads are like calling the reporter directly — you always get the latest. Transactional reads are like getting a synchronized snapshot of multiple reporters at the exact same moment. Each level costs more but gives stronger guarantees.

When Strong Consistency Matters

Use strongly consistent reads for: financial balances, inventory counts, anything where stale data means incorrect behavior. Use eventually consistent (default) for: user profiles, product catalogs, anything where a 1-second delay is acceptable.

05

Writing: PutItem & UpdateItem

PutItem — Create or Full Replace

PutItem Characteristics

  • Writes an item — creates new or FULLY REPLACES existing
  • Entire item replaced if key exists — not a merge, not a partial update
  • ConditionExpression: only write if condition is true (optimistic concurrency)
  • ReturnValues: ALL_OLD returns the previous item before replacement
  • WCU cost: ceil(max(oldItemSize, newItemSize) / 1 KB)

UpdateItem — Partial Modification

update-item-example.tstypescript
// UpdateItem — atomic increment + set attribute
const result = await dynamodb.updateItem({
  TableName: "Products",
  Key: { PK: { S: "PRODUCT#123" }, SK: { S: "PRODUCT#123" } },
  UpdateExpression: "SET #views = #views + :one, #name = :newName REMOVE #deprecated",
  ConditionExpression: "attribute_exists(PK)",  // only update existing items
  ExpressionAttributeNames: {
    "#views": "viewCount",
    "#name": "name",
    "#deprecated": "oldField"
  },
  ExpressionAttributeValues: {
    ":one": { N: "1" },
    ":newName": { S: "Updated Product" }
  },
  ReturnValues: "UPDATED_NEW"  // return new values of updated attributes
});

Update Expression Actions

ActionPurposeExample
SETAdd or overwrite attributesSET #count = #count + :val
REMOVEDelete attributes from itemREMOVE #oldField, #temp
ADDAdd to number or add elements to setADD #tags :newTags
DELETERemove elements from a setDELETE #tags :removeTags

Upsert Behavior

UpdateItem creates the item if it doesn't exist (upsert). To prevent this, add ConditionExpression: attribute_exists(PK). To ensure create-only, use PutItem with ConditionExpression: attribute_not_exists(PK).

06

Writing: DeleteItem & BatchWriteItem

DeleteItem

DeleteItem Characteristics

  • Remove a single item by its complete primary key
  • ConditionExpression: only delete if condition met
  • ReturnValues: ALL_OLD returns the deleted item
  • No error if item doesn't exist (unless condition fails)
  • WCU cost: ceil(itemSize / 1 KB) — charged even for non-existent items with conditions

BatchWriteItem

BatchWriteItem Characteristics

  • Write or delete up to 25 items or 16 MB in one API call
  • Mix of PutItem and DeleteItem operations (no UpdateItem!)
  • Items can span multiple tables
  • NOT atomic — partial success possible, UnprocessedItems returned
  • No condition expressions in batch — only unconditional puts/deletes
  • Use for: bulk loading, bulk deletion, data migrations

No UpdateItem in Batch

BatchWriteItem only supports PutItem and DeleteItem. If you need batch updates, you must use individual UpdateItem calls or TransactWriteItems (limited to 25 items, but supports updates).

07

Condition Expressions

Condition expressions make any write operation conditional — atomic compare-and-swap without transactions. They are the foundation of safe concurrent writes in DynamoDB.

condition-expression-patterns.txttext
Common Condition Expression Patterns:
═══════════════════════════════════════════════════════════════
Pattern                              | Use Case
═══════════════════════════════════════════════════════════════
attribute_not_exists(PK)             | Only create, fail if exists
attribute_exists(PK)                 | Only update existing items
#version = :expected_version         | Optimistic locking
#status = :active                    | Only process active items
#balance >= :amount                  | Sufficient funds check
size(#items) < :maxItems             | Bounded collections
═══════════════════════════════════════════════════════════════

Failed conditionConditionalCheckFailedException
This is NOT an errorit's expected flow. Handle in application.

Why Conditions Matter

Without Conditions

  • Two users update same item simultaneously
  • Last write wins silently
  • First user's changes lost without notice
  • No way to detect the conflict

With Conditions

  • Both users include version condition
  • First write succeeds, increments version
  • Second write fails (version mismatch)
  • Application retries with fresh data
08

Interview Questions

Q:What is the difference between Query and Scan?

A: Query requires a partition key and optionally filters by sort key — it reads only items in one partition. Scan reads every item in the entire table. Query cost is proportional to items in the partition; Scan cost is proportional to the entire table size. Use Query for production, Scan only for migrations/exports.

Q:Why does FilterExpression not reduce RCU cost?

A: FilterExpression is applied AFTER items are read from the partition. DynamoDB first reads all items matching the KeyConditionExpression (charging RCU), then filters in memory. If you Query 1000 items and FilterExpression keeps 10, you still pay for 1000 items of RCU. The solution is better key design, not more filters.

Q:What is the difference between PutItem and UpdateItem?

A: PutItem creates or FULLY REPLACES an item — all existing attributes are overwritten. UpdateItem modifies specific attributes while preserving others. UpdateItem also supports atomic operations (increment counters, append to lists) and creates the item if it doesn't exist (upsert). Use PutItem for full writes, UpdateItem for partial modifications.

Q:How does optimistic locking work in DynamoDB?

A: Add a 'version' attribute to items. On update, include ConditionExpression: #version = :expectedVersion and increment version in the UpdateExpression. If another writer modified the item (version changed), the condition fails with ConditionalCheckFailedException. The application retries: read fresh item, apply changes, attempt update again.

Q:What happens when a Query returns LastEvaluatedKey?

A: It means more items exist that match the query but weren't returned (either due to 1 MB response limit or Limit parameter). Pass LastEvaluatedKey as ExclusiveStartKey in the next Query call to continue pagination. When LastEvaluatedKey is absent from the response, all matching items have been returned.

09

Common Mistakes

🔍

Using Scan where Query would work

Scanning the entire table then filtering in application code. If you're filtering by a known attribute, create a GSI with that attribute as the partition key. A Query on a GSI is orders of magnitude cheaper than a Scan with FilterExpression.

Create a GSI with the filter attribute as partition key and use Query instead of Scan.

💸

Assuming FilterExpression saves money

Adding FilterExpression to reduce results without realizing you still pay for all items read. If your Query returns 10,000 items and FilterExpression keeps 50, you pay for 10,000 items. Redesign your sort key or add a GSI instead.

Redesign your sort key to include the filter attribute, or create a GSI that supports the access pattern directly.

✏️

Using PutItem when UpdateItem is needed

PutItem replaces the entire item. If you only want to update one attribute but use PutItem without including all other attributes, you'll lose data. Use UpdateItem for partial modifications.

Use UpdateItem with SET expressions for partial modifications to preserve existing attributes.

⚠️

Not handling ConditionalCheckFailedException

Condition expressions are expected to fail in concurrent systems. Not catching and retrying ConditionalCheckFailedException means your application silently drops writes. Implement retry logic with exponential backoff.

Catch ConditionalCheckFailedException, re-read the item, and retry the operation with exponential backoff and jitter.

📄

Confusing Limit with result count

Limit controls how many items DynamoDB EVALUATES, not how many it returns. With Limit=10 and a FilterExpression that matches 20% of items, you might get only 2 results. You must paginate until LastEvaluatedKey is absent to get all matching items.

Always paginate using LastEvaluatedKey until it is absent from the response to retrieve all matching items.