AWS · lesson 11 of 19
Beginner
4Intermediate
13- 5 VPC and Networking Basics
- 6 RDS Basics
- 7 AWS Lambda Fundamentals
- 8 CloudFormation Basics
- 9 CloudWatch Monitoring Basics
- 11 DynamoDB: Tables and Keys
- 12 DynamoDB: Scan, Reserved Words, and Global Secondary Indexes
- 13 DynamoDB: Conditional Writes, Atomic Counters, and Streams
- 14 SQS: Visibility Timeout and Dead-Letter Queues
- 15 SQS: FIFO Queues, Message Groups, and Deduplication
- 16 SNS: Fan-Out to SQS and Filter Policies
- 17 ELB and Auto Scaling: Wiring an ASG Behind a Load Balancer
- 18 ECS: Task Definitions and Services
Also part of AWS
8- + Linting AWS IAM Policies Before Deploy with Parliament from Cloud Security
- + Auditing the AWS IAM Credential Report with Python from Cloud Security
- + Hunting in AWS CloudTrail Logs with Python from Cloud Security
- + Dead-Letter Queues: What Happens When Retries Keep Failing from Event-Driven Architecture
- + Idempotent Consumers: Surviving a Duplicate Delivery from Event-Driven Architecture
- + Load Balancing: Round Robin vs Least Connections from System Design
- + Kubernetes Fundamentals from Kubernetes
- + Horizontal Pod Autoscaling: Real Replicas, Real CPU Numbers from Kubernetes
DynamoDB: Tables and Keys
Create a DynamoDB table with a partition key and sort key, put two items under the same partition, and query them with begins_with — all against the real DynamoDB Local emulator, no AWS account needed.
By SkillByExample · Updated
DynamoDB tables don’t have a fixed schema, but every table has a primary key, and that key shapes everything about how data is stored and retrieved. This lesson uses a composite primary key — a partition key plus a sort key — and runs against DynamoDB Local, AWS’s own Docker-based emulator, so there’s no AWS account or billing involved anywhere in this lesson.
Creating a Table With a Partition Key and Sort Key
CustomerId is the partition key (which customer), OrderId is the sort key (which order,
within that customer) — together they’re the only way to address one specific item. The aws()
function below runs the real AWS CLI from its own Docker image, since no local install is needed.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb create-table \
--table-name Orders \
--attribute-definitions \
AttributeName=CustomerId,AttributeType=S \
AttributeName=OrderId,AttributeType=S \
--key-schema \
AttributeName=CustomerId,KeyType=HASH \
AttributeName=OrderId,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--endpoint-url http://aws11-ddb:8000 \
--query 'TableDescription.{TableName:TableName,TableStatus:TableStatus,KeySchema:KeySchema}'
$ docker network create aws11-lab
275064355a5179ade5ee35db5b35bb0645c5be72b3ca398df7f1cbeb1354c9d2
$ docker run -d --name aws11-ddb --network aws11-lab amazon/dynamodb-local:3.3.2
b0abda3b6983a5ff4b8e88721f1e218eef5a9b6ccfa7a4342bfe73fb51e336fc
$ until docker run --rm --network aws11-lab curlimages/curl:8.11.1 -s -o /dev/null http://aws11-ddb:8000; do sleep 0.3; done
$ bash create_table.sh
{
"TableName": "Orders",
"TableStatus": "ACTIVE",
"KeySchema": [
{
"AttributeName": "CustomerId",
"KeyType": "HASH"
},
{
"AttributeName": "OrderId",
"KeyType": "RANGE"
}
]
}
The table is ACTIVE immediately — DynamoDB Local has no provisioning delay the way a real table
briefly does. PAY_PER_REQUEST billing means there’s no fixed read/write capacity to size ahead
of time; every request is metered individually, which is what the ConsumedCapacity numbers below
actually measure.
Putting Two Items Under One Partition Key
Both orders share CustomerId: alice, but have different OrderId values — exactly what a sort
key is for.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb put-item --table-name Orders \
--item '{"CustomerId":{"S":"alice"},"OrderId":{"S":"o-001"},"total":{"N":"30"}}' \
--return-consumed-capacity TOTAL \
--endpoint-url http://aws11-ddb:8000
aws dynamodb put-item --table-name Orders \
--item '{"CustomerId":{"S":"alice"},"OrderId":{"S":"o-002"},"total":{"N":"55"}}' \
--return-consumed-capacity TOTAL \
--endpoint-url http://aws11-ddb:8000
$ bash put_orders.sh
{
"ConsumedCapacity": {
"TableName": "Orders",
"CapacityUnits": 1.0
}
}
{
"ConsumedCapacity": {
"TableName": "Orders",
"CapacityUnits": 1.0
}
}
put-item with no --return-consumed-capacity wouldn’t show this at all; each write costs 1.0
write capacity unit here because both items are well under a single write unit’s per-item size
cap, as the FAQ below details.
Querying by Partition Key, Filtered by Sort-Key Prefix
Query needs the exact partition key, plus (optionally) a condition on the sort key —
begins_with is one of several sort-key conditions it supports.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb query --table-name Orders \
--key-condition-expression "CustomerId = :c AND begins_with(OrderId, :p)" \
--expression-attribute-values '{":c":{"S":"alice"},":p":{"S":"o-"}}' \
--return-consumed-capacity TOTAL \
--endpoint-url http://aws11-ddb:8000 \
--query '{Count:Count,ScannedCount:ScannedCount,ConsumedCapacity:ConsumedCapacity,OrderIds:Items[].OrderId.S}'
$ bash query_orders.sh
{
"Count": 2,
"ScannedCount": 2,
"ConsumedCapacity": {
"TableName": "Orders",
"CapacityUnits": 0.5
},
"OrderIds": [
"o-001",
"o-002"
]
}
Both orders come back — begins_with(OrderId, "o-") matches both o-001 and o-002 — and the
query costs only 0.5 read capacity units for the pair: Query defaults to an eventually-consistent
read, which is priced at half of a strongly-consistent read’s unit cost, and both tiny items
together fit well under one unit either way. Count and ScannedCount match here because nothing
was filtered out after the key condition narrowed the scan to exactly these two items; the next
lesson shows a case where they diverge.
A Point Read by the Full Composite Key
GetItem needs both halves of the key, not a range — for one small item like this, it costs the
same as a Query that happens to return one item (both are a single eventually-consistent read
unit); the real advantage is precision, not price: GetItem only works when you already know
exactly which one item you want.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb get-item --table-name Orders \
--key '{"CustomerId":{"S":"alice"},"OrderId":{"S":"o-001"}}' \
--endpoint-url http://aws11-ddb:8000
$ bash get_order.sh
{
"Item": {
"OrderId": {
"S": "o-001"
},
"total": {
"N": "30"
},
"CustomerId": {
"S": "alice"
}
}
}
GetItem returns one item directly, with no Count/ScannedCount — there’s nothing to scan, it
goes straight to the one partition holding that exact key.
Practice
1. Put a third order for a different customer, then confirm alice's query from above still returns only her two orders.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb put-item --table-name Orders \
--item '{"CustomerId":{"S":"bob"},"OrderId":{"S":"o-010"},"total":{"N":"18"}}' \
--endpoint-url http://aws11-ddb:8000
aws dynamodb query --table-name Orders \
--key-condition-expression "CustomerId = :c" \
--expression-attribute-values '{":c":{"S":"alice"}}' \
--endpoint-url http://aws11-ddb:8000 \
--query '{Count:Count,OrderIds:Items[].OrderId.S}'
$ bash practice1.sh
{
"Count": 2,
"OrderIds": [
"o-001",
"o-002"
]
}
Still just alice’s two orders — a Query without a sort-key condition still requires the exact
partition key, so bob’s order under a different partition key never enters the result.
2. Try GetItem with only the partition key on this table and read the real error.
#!/usr/bin/env bash
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb get-item --table-name Orders \
--key '{"CustomerId":{"S":"alice"}}' \
--endpoint-url http://aws11-ddb:8000
$ bash practice2.sh
aws: [ERROR]: An error occurred (ValidationException) when calling the GetItem operation: The number of conditions on the keys is invalid
A ValidationException, not a “not found” — half of a composite key isn’t a valid key at all, so
DynamoDB rejects the request before it ever looks for an item.
3. Query alice's orders with a sort-key prefix that matches nothing, and check Count/ScannedCount/ConsumedCapacity.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb query --table-name Orders \
--key-condition-expression "CustomerId = :c AND begins_with(OrderId, :p)" \
--expression-attribute-values '{":c":{"S":"alice"},":p":{"S":"zzz"}}' \
--return-consumed-capacity TOTAL \
--endpoint-url http://aws11-ddb:8000
$ bash practice3.sh
{
"Items": [],
"Count": 0,
"ScannedCount": 0,
"ConsumedCapacity": {
"TableName": "Orders",
"CapacityUnits": 0.0
}
}
Items is empty and Count/ScannedCount are both 0 — DynamoDB knows from the partition key
alone that there’s nothing under this prefix, so it scans zero items, not “two items that didn’t
match.”
4. Overwrite o-001's total with a second put-item for the same key, and confirm it replaced rather than added a new item.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb put-item --table-name Orders \
--item '{"CustomerId":{"S":"alice"},"OrderId":{"S":"o-001"},"total":{"N":"999"}}' \
--endpoint-url http://aws11-ddb:8000
aws dynamodb query --table-name Orders \
--key-condition-expression "CustomerId = :c" \
--expression-attribute-values '{":c":{"S":"alice"}}' \
--endpoint-url http://aws11-ddb:8000 \
--query '{Count:Count,Orders:Items[].{OrderId:OrderId.S,total:total.N}}'
$ bash practice4.sh
{
"Count": 2,
"Orders": [
{
"OrderId": "o-001",
"total": "999"
},
{
"OrderId": "o-002",
"total": "55"
}
]
}
Still only 2 items under alice’s partition key — put-item with the same full key (CustomerId +
OrderId) replaces the existing item’s attributes wholesale, it never creates a duplicate.
5. Delete one of alice's orders and confirm the query result shrinks by one.
#!/usr/bin/env bash
set -e
aws() { MSYS_NO_PATHCONV=1 docker run --rm --network aws11-lab -e AWS_ACCESS_KEY_ID=test -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 amazon/aws-cli:2.39.5 "$@"; }
aws dynamodb delete-item --table-name Orders \
--key '{"CustomerId":{"S":"alice"},"OrderId":{"S":"o-002"}}' \
--endpoint-url http://aws11-ddb:8000
aws dynamodb query --table-name Orders \
--key-condition-expression "CustomerId = :c" \
--expression-attribute-values '{":c":{"S":"alice"}}' \
--endpoint-url http://aws11-ddb:8000 \
--query '{Count:Count,OrderIds:Items[].OrderId.S}'
$ bash practice5.sh
{
"Count": 1,
"OrderIds": [
"o-001"
]
}
Count drops to 1 and only o-001 remains — DeleteItem needs the same full composite key as
GetItem, for the same reason: neither half alone addresses one item.
Cleanup
$ docker rm -f aws11-ddb
aws11-ddb
$ docker network rm aws11-lab
aws11-lab
Next: scanning with a filter, the total reserved-word error, and adding a global secondary index
to this same table.