Reference for aws dynamodb — table inventory, CRUD, scans, queries, and a focused workflow for finding and deleting items by lockid.
Assumes AWS CLI v2 and credentials already configured (aws configure / SSO / env vars). For one-off scoping, append --region <r> and --profile <p> to any command.
DynamoDB is schemaless except for keys. Every table has: - A partition key (PK), required. - Optionally a sort key (SK). PK+SK together = primary key. - Zero or more secondary indexes (GSI / LSI), each with their own key schema.
This drives the three retrieval verbs:
| Verb | What it does | Cost / latency |
|---|---|---|
| get-item | Fetch one item by full primary key | Cheapest, single-digit ms |
| query | Fetch items sharing a PK (optionally narrowed by SK) — on the table or any index | Cheap, only scanned keys consume RCU |
| scan | Read the entire table / index and filter in memory | Expensive — full-table read every time |
Rule of thumb: if your access pattern (e.g. "find by lockid") isn't covered by the primary key or a GSI, you're stuck with scan until someone adds an index.
All values in expressions are encoded as DynamoDB JSON: {"S": "..."}, {"N": "123"}, {"BOOL": true}, etc. The newer aws dynamodb execute-statement (PartiQL) sidesteps the type tags entirely — see §7.
| Command | Purpose |
|---|---|
aws dynamodb list-tables |
All tables in current region |
aws dynamodb list-tables --max-items 50 --starting-token <t> |
Paginate |
aws dynamodb describe-table --table-name T |
Schema, keys, indexes, item count (item count is stale — refreshed ~every 6h) |
aws dynamodb describe-table --table-name T --query 'Table.KeySchema' |
Just the PK / SK |
aws dynamodb describe-table --table-name T --query 'Table.GlobalSecondaryIndexes[].{Name:IndexName,Keys:KeySchema}' |
GSIs and their key schemas — check this first when chasing a non-key attribute like lockid |
aws dynamodb describe-table --table-name T --query 'Table.AttributeDefinitions' |
Typed attributes referenced by keys/indexes |
aws dynamodb describe-time-to-live --table-name T |
TTL attribute name + status |
Examples use a table Locks with PK lockid (S). Adjust the JSON to match your table.
put-itemaws dynamodb put-item \
--table-name Locks \
--item '{
"lockid": {"S": "abc-123"},
"owner": {"S": "alice"},
"acquired":{"N": "1716240000"},
"ttl": {"N": "1716243600"}
}'
Conditional put (don't clobber):
aws dynamodb put-item \
--table-name Locks \
--item '{"lockid":{"S":"abc-123"},"owner":{"S":"alice"}}' \
--condition-expression "attribute_not_exists(lockid)"
get-itemaws dynamodb get-item \
--table-name Locks \
--key '{"lockid":{"S":"abc-123"}}'
Strongly-consistent read (default is eventually consistent):
aws dynamodb get-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--consistent-read
Project only specific attributes:
aws dynamodb get-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--projection-expression "lockid, owner"
update-itemaws dynamodb update-item \
--table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--update-expression "SET #o = :o, acquired = :t" \
--expression-attribute-names '{"#o":"owner"}' \
--expression-attribute-values '{":o":{"S":"bob"},":t":{"N":"1716240900"}}' \
--return-values ALL_NEW
Increment a counter:
aws dynamodb update-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--update-expression "ADD attempts :inc" \
--expression-attribute-values '{":inc":{"N":"1"}}'
Remove an attribute:
aws dynamodb update-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--update-expression "REMOVE owner"
delete-itemaws dynamodb delete-item \
--table-name Locks \
--key '{"lockid":{"S":"abc-123"}}'
Conditional delete (only if owned by alice — avoids deleting a re-acquired lock):
aws dynamodb delete-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--condition-expression "#o = :o" \
--expression-attribute-names '{"#o":"owner"}' \
--expression-attribute-values '{":o":{"S":"alice"}}' \
--return-values ALL_OLD
--return-values ALL_OLD echoes the deleted item so you can confirm or pipe it onward.
aws dynamodb batch-get-item --request-items '{
"Locks": {
"Keys": [
{"lockid":{"S":"abc-123"}},
{"lockid":{"S":"abc-124"}}
]
}
}'
aws dynamodb batch-write-item --request-items '{
"Locks": [
{"DeleteRequest":{"Key":{"lockid":{"S":"abc-123"}}}},
{"DeleteRequest":{"Key":{"lockid":{"S":"abc-124"}}}},
{"PutRequest":{"Item":{"lockid":{"S":"abc-125"},"owner":{"S":"alice"}}}}
]
}'
Check the response for UnprocessedItems and retry those — batch-write does not retry partial failures on its own.
transact-write-items (up to 100 items, all-or-nothing)aws dynamodb transact-write-items --transact-items '[
{"Delete":{"TableName":"Locks","Key":{"lockid":{"S":"abc-123"}},
"ConditionExpression":"#o = :o",
"ExpressionAttributeNames":{"#o":"owner"},
"ExpressionAttributeValues":{":o":{"S":"alice"}}}},
{"Put":{"TableName":"AuditLog",
"Item":{"event_id":{"S":"e-1"},"deleted_lock":{"S":"abc-123"}}}}
]'
aws dynamodb query \
--table-name Locks \
--key-condition-expression "lockid = :id" \
--expression-attribute-values '{":id":{"S":"abc-123"}}'
With a sort-key range (table has PK tenant, SK acquired):
aws dynamodb query --table-name Locks \
--key-condition-expression "tenant = :t AND acquired BETWEEN :lo AND :hi" \
--expression-attribute-values '{
":t":{"S":"acme"},":lo":{"N":"1716000000"},":hi":{"N":"1716240000"}
}'
aws dynamodb query --table-name Locks \
--index-name owner-acquired-index \
--key-condition-expression "#o = :o" \
--expression-attribute-names '{"#o":"owner"}' \
--expression-attribute-values '{":o":{"S":"alice"}}'
| Flag | Purpose |
|---|---|
--filter-expression |
Server-side filter applied after the key match — saves bandwidth, not RCU |
--projection-expression |
Pick attributes to return |
--limit N |
Max items per page |
--scan-index-forward false |
Descending sort-key order (newest first) |
--consistent-read |
Strongly consistent (table only, not GSI) |
--select COUNT |
Return only Count, no items |
--starting-token <t> / --no-paginate |
Manual pagination in CLI v2 |
aws dynamodb scan --table-name Locks
With a filter (still reads the whole table — only the network response shrinks):
aws dynamodb scan --table-name Locks \
--filter-expression "owner = :o" \
--expression-attribute-values '{":o":{"S":"alice"}}'
Parallel scan (splits work across N workers, run each in a separate shell):
aws dynamodb scan --table-name Locks --total-segments 4 --segment 0
aws dynamodb scan --table-name Locks --total-segments 4 --segment 1
# ...etc
How you find/delete by lockid depends on what lockid is in your table. Run describe-table first (§1) and pick the matching path.
lockid IS the partition key — single-item opsEasy case. Use get-item / delete-item directly:
# Find
aws dynamodb get-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}'
# Delete
aws dynamodb delete-item --table-name Locks \
--key '{"lockid":{"S":"abc-123"}}' \
--return-values ALL_OLD
If lockid is the PK and there's a sort key, you may have multiple items per lockid. Use query to enumerate, then delete each by full key (see 6c).
lockid is a GSI key — query the index, delete via the base table keydelete-item only accepts the base table's primary key, not the index key. So the flow is: query the GSI to discover base keys → loop → delete-item against the base table.
# 1. Query the index — project just the base PK (assume base PK is "id")
aws dynamodb query --table-name Locks \
--index-name lockid-index \
--key-condition-expression "lockid = :lid" \
--expression-attribute-values '{":lid":{"S":"abc-123"}}' \
--projection-expression "id" \
--output json > /tmp/lockid-hits.json
# 2. Inspect first
jq '.Count, .Items' /tmp/lockid-hits.json
# 3. Delete each match
jq -r '.Items[].id.S' /tmp/lockid-hits.json | while read -r id; do
aws dynamodb delete-item --table-name Locks \
--key "{\"id\":{\"S\":\"$id\"}}" \
--condition-expression "lockid = :lid" \
--expression-attribute-values '{":lid":{"S":"abc-123"}}' \
--return-values ALL_OLD
done
The --condition-expression on the delete guards against a race where the item's lockid changed between the query and the delete (GSIs are eventually consistent, so this matters).
lockid is a non-key attribute — scan, then deleteSlowest path; use sparingly on large tables. Same general shape:
# 1. Scan with a filter (uses full-table RCU — be careful on big tables)
aws dynamodb scan --table-name Locks \
--filter-expression "lockid = :lid" \
--expression-attribute-values '{":lid":{"S":"abc-123"}}' \
--projection-expression "id" \
--output json > /tmp/lockid-hits.json
# 2. Sanity check
jq '.Count' /tmp/lockid-hits.json
# 3. Delete loop (single-item)
jq -r '.Items[].id.S' /tmp/lockid-hits.json | while read -r id; do
aws dynamodb delete-item --table-name Locks \
--key "{\"id\":{\"S\":\"$id\"}}"
done
If the table has a composite key (PK id + SK version), project both attributes and build the key JSON accordingly:
aws dynamodb scan --table-name Locks \
--filter-expression "lockid = :lid" \
--expression-attribute-values '{":lid":{"S":"abc-123"}}' \
--projection-expression "id, version" \
--output json \
| jq -r '.Items[] | "\(.id.S)\t\(.version.N)"' \
| while IFS=$'\t' read -r id version; do
aws dynamodb delete-item --table-name Locks \
--key "{\"id\":{\"S\":\"$id\"},\"version\":{\"N\":\"$version\"}}"
done
batch-write-item (25 at a time)For many matches, batch the deletes — far fewer API calls and lower latency.
# Build batches of 25 DeleteRequests and POST them
jq -c '.Items | _nwise(25) |
{"Locks": [.[] | {"DeleteRequest":{"Key":{"id": .id}}}]}' \
/tmp/lockid-hits.json \
| while read -r batch; do
aws dynamodb batch-write-item --request-items "$batch"
# NOTE: inspect UnprocessedItems and retry — batch-write doesn't retry for you
done
_nwise(25) chunks into groups of ≤25, the max per batch-write-item call.
--select COUNT (or jq '.Count') to see how many items match before deleting anything.--projection-expression to get the full item, or --return-values ALL_OLD on each delete, so you can reverse course.lockid is mutable or eventual consistency could lie to you.sleep 0.1 between deletes or watch CloudWatch ConsumedWriteCapacityUnits. Each delete-item costs 1 WCU per KB of the item; large items + tight loops will throttle.aws dynamodb create-backup --table-name Locks --backup-name pre-lockid-purge-$(date +%Y%m%d).PartiQL (execute-statement) is convenient for ad-hoc CLI queries because you can use plain SQL literals instead of {"S":"..."}.
# Find by lockid (works for PK, GSI key, or non-key attribute — DynamoDB picks the cheapest plan)
aws dynamodb execute-statement \
--statement "SELECT id, lockid, owner FROM Locks WHERE lockid = 'abc-123'"
# Delete by PK
aws dynamodb execute-statement \
--statement "DELETE FROM Locks WHERE id = 'abc-123'"
# Update
aws dynamodb execute-statement \
--statement "UPDATE Locks SET owner = 'bob' WHERE id = 'abc-123'"
Caveats:
- DELETE / UPDATE only work on a full primary key — same constraint as the low-level API.
- If your WHERE clause doesn't match a PK or index, PartiQL silently does a full scan. Run EXPLAIN (in the console) or check costs before unleashing on a large table.
- Use execute-transaction for transactional PartiQL.
- Pagination uses NextToken like other commands.
If items have a TTL attribute, you may not need to delete by lockid at all — set the TTL and DynamoDB removes them within ~48h.
| Command | Purpose |
|---|---|
aws dynamodb describe-time-to-live --table-name T |
Is TTL enabled? On what attribute? |
aws dynamodb update-time-to-live --table-name T --time-to-live-specification "Enabled=true,AttributeName=ttl" |
Enable TTL |
(per-item) --update-expression "SET ttl = :t" with epoch seconds |
Mark item for expiry |
TTL is asynchronous, low priority, and unconditional once triggered. It does not consume write capacity. Don't use it for "delete in 5 minutes" SLAs.
| Command | Purpose |
|---|---|
aws dynamodb describe-table --table-name T --query 'Table.StreamSpecification' |
Is a stream enabled? |
aws dynamodb update-table --table-name T --stream-specification StreamEnabled=true,StreamViewType=NEW_AND_OLD_IMAGES |
Enable a stream |
aws dynamodb list-streams --table-name T |
Stream ARNs |
aws dynamodb describe-continuous-backups --table-name T |
PITR status |
aws dynamodb update-continuous-backups --table-name T --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true |
Enable PITR |
aws dynamodb create-backup --table-name T --backup-name name |
On-demand backup |
aws dynamodb restore-table-from-backup --target-table-name T2 --backup-arn ... |
Restore |
aws dynamodb export-table-to-point-in-time --table-arn ... --s3-bucket b --export-format DYNAMODB_JSON |
Snapshot to S3 (requires PITR) |
T=Locks
aws dynamodb describe-table --table-name "$T" \
--query '{PK:Table.KeySchema, GSIs:Table.GlobalSecondaryIndexes[].{Name:IndexName,Keys:KeySchema}}' \
--output yaml
Look for lockid in the PK or any GSI's key schema. If absent → §6c (scan).
aws dynamodb query --table-name Locks \
--index-name lockid-index \
--key-condition-expression "lockid = :lid" \
--expression-attribute-values '{":lid":{"S":"abc-123"}}' \
--select COUNT
Swap query for scan (with --filter-expression) when lockid isn't indexed.
LID=abc-123
T=Locks
IDX=lockid-index
PK=id
aws dynamodb query --table-name "$T" --index-name "$IDX" \
--key-condition-expression "lockid = :lid" \
--expression-attribute-values "{\":lid\":{\"S\":\"$LID\"}}" \
--projection-expression "$PK" --output json \
| jq -r ".Items[].$PK.S" \
| xargs -I{} -n1 aws dynamodb delete-item --table-name "$T" \
--key "{\"$PK\":{\"S\":\"{}\"}}" --return-values ALL_OLD
There's no "restore one item." Either restore the whole backup to a side table (restore-table-from-backup) and copy the row across, or pull the item from a PITR S3 export and re-put-item.
scan is read-the-world. Even with a FilterExpression, you pay RCU for every item examined. Add a GSI on hot lookup attributes (lockid) if you're querying them regularly.--consistent-read. That flag is silently ignored — and yes, this has bitten people.describe-table item counts are stale (~6h refresh). Use scan --select COUNT for an exact (expensive) count, or a query --select COUNT if you can target a PK.return-values options: NONE (default), ALL_OLD (delete/update only), ALL_NEW / UPDATED_OLD / UPDATED_NEW (update only). Picking the right one saves a follow-up get-item.name, owner, status, size, ...) must use #alias placeholders via --expression-attribute-names. Easiest habit: always alias attribute names.--no-cli-pager and/or --no-paginate if you want to control it yourself or feed output to scripts.ProvisionedThroughputExceededException.--endpoint-url http://localhost:8000. Same commands, same syntax.