Amazon DynamoDB for beginners: one table, a few items and the keys that find them
DynamoDB is a serverless key-value and document database. Today: what the partition and sort keys do, when to skip it, and a CloudShell lab that builds an on-demand table, reads it two ways and deletes it again.
Amazon DynamoDB, in plain words
Amazon DynamoDB is a fully managed NoSQL database. You create a table, put items in it (each item is a small bundle of named attributes, a bit like a JSON object up to 400 KB), and AWS runs the servers, storage and replication for you. There is no instance size to pick and nothing to patch.
Every table has a primary key that you choose when you create it and can't change later. The partition key decides which bucket of storage an item lives in, and an optional sort key orders the items that share a partition key. Pick them around the questions your app will ask: with UserId as partition key and CreatedAt as sort key, "give me this user's notes from October, newest first" is a single cheap Query.
In on-demand mode, which the docs call the default and recommended choice, you pay per read and write request and nothing for throughput when the table sits idle. Reads are eventually consistent unless you ask for a strongly consistent read, which costs twice as much and always returns the latest write. You'll see both in today's lab.
Reach for it when
- Your app looks up and updates records by a known ID, such as a user profile, a shopping cart, a session or a device's latest reading, and needs fast answers at any scale.
- You're building on Lambda or other serverless pieces and want a database that has no servers, no connection pool to manage and no cost when nobody is using it.
- You can list your access patterns up front ("orders for a customer by date", "latest status for a device") and design keys around them.
Skip it when
- You need ad hoc questions across the whole dataset, joins between many tables or flexible reporting. A relational database such as Aurora or RDS, or an analytics tool such as Athena, fits better.
- Your access patterns are still unknown and change weekly. DynamoDB rewards key design done early; without it you end up running expensive
Scans. - You're storing large files such as images, PDFs or videos. Items max out at 400 KB, so put the file in S3 and keep only its key and metadata in DynamoDB.
The AI angle
AI apps need a fast place to keep state that isn't the model itself: chat history per user, an agent's session notes, which documents a user uploaded, rate limits and usage counters. A table keyed by user (partition key) and timestamp (sort key) answers "the last 20 turns of this conversation" with one Query, which is the same pattern today's lab practices. DynamoDB doesn't do vector search on its own; pair it with a vector store for retrieval and keep the per-user records here.
Hands-on lab: Build a small notes table, query it two ways, then delete it
From AWS CloudShell, create an on-demand DynamoDB table with a partition key and a sort key, write four items, compare an eventually consistent read with a strongly consistent one, run a key-based query, try a write that a condition blocks, then delete the table and confirm it's gone.
Before you start
- An AWS account that you own and are allowed to experiment in. Don't use a work account without permission.
- Sign-in as an IAM user or role that can create, read, write and delete DynamoDB tables (an administrator role in a personal sandbox account covers this).
- The console set to the Region you want to use. CloudShell runs commands in the console's current Region.
Open CloudShell and create the table
Choose the CloudShell icon in the console's top bar. The table uses
UserIdas partition key (HASH) andCreatedAtas sort key (RANGE), both strings.PAY_PER_REQUESTmeans on-demand. Deletion protection is off by default, which keeps cleanup simple. Thewaitcommand returns once the table isACTIVE. If CloudShell restarts during the lab, setTABLEagain.TABLE=sandbox-notes echo "Region: $AWS_REGION" aws dynamodb create-table --table-name "$TABLE" \ --attribute-definitions AttributeName=UserId,AttributeType=S AttributeName=CreatedAt,AttributeType=S \ --key-schema AttributeName=UserId,KeyType=HASH AttributeName=CreatedAt,KeyType=RANGE \ --billing-mode PAY_PER_REQUEST \ --tags Key=purpose,Value=sandbox-lab \ --query 'TableDescription.TableStatus' --output text aws dynamodb wait table-exists --table-name "$TABLE" echo readyWrite four items
Each value is wrapped in a type marker:
Sfor string,Nfor number (sent as a string). Only the two key attributes are required; every item may carry different extra attributes.user-aanduser-bare made-up IDs.--return-consumed-capacity TOTALshows that each small write costs 1 write unit.put() { aws dynamodb put-item --table-name "$TABLE" --item "$1" \ --return-consumed-capacity TOTAL --query 'ConsumedCapacity.CapacityUnits'; } put '{"UserId":{"S":"user-a"},"CreatedAt":{"S":"2026-09-30T08:00Z"},"Text":{"S":"Buy coffee"}}' put '{"UserId":{"S":"user-a"},"CreatedAt":{"S":"2026-10-02T09:15Z"},"Text":{"S":"Read the DynamoDB guide"}}' put '{"UserId":{"S":"user-a"},"CreatedAt":{"S":"2026-10-20T17:40Z"},"Text":{"S":"Try a Query"},"Views":{"N":"0"}}' put '{"UserId":{"S":"user-b"},"CreatedAt":{"S":"2026-10-05T12:00Z"},"Text":{"S":"Someone else's note"}}'Read one item two ways
get-itemneeds the full primary key: both the partition key and the sort key. The first read is eventually consistent (the default) and should report0.5read units; the second adds--consistent-readand should report1.0. Same item, twice the price, guaranteed latest value.KEY='{"UserId":{"S":"user-a"},"CreatedAt":{"S":"2026-10-02T09:15Z"}}' aws dynamodb get-item --table-name "$TABLE" --key "$KEY" \ --return-consumed-capacity TOTAL --query '[Item.Text.S, ConsumedCapacity.CapacityUnits]' aws dynamodb get-item --table-name "$TABLE" --key "$KEY" --consistent-read \ --return-consumed-capacity TOTAL --query '[Item.Text.S, ConsumedCapacity.CapacityUnits]'Query one user's October notes, newest first
A
Querymust name one partition key value with=, and may narrow the sort key, here withbegins_with.--no-scan-index-forwardflips the order to descending. You should get two notes foruser-a(the September note anduser-b's note are left out), and DynamoDB never touched the other items to find them.aws dynamodb query --table-name "$TABLE" \ --key-condition-expression 'UserId = :u AND begins_with(CreatedAt, :m)' \ --expression-attribute-values '{":u":{"S":"user-a"},":m":{"S":"2026-10"}}' \ --no-scan-index-forward \ --query 'Items[].[CreatedAt.S, Text.S]' --output tableBlock an accidental overwrite, then bump a counter
By default
put-itemsilently replaces an item with the same key. The conditionattribute_not_exists(UserId)makes the write succeed only if no such item exists yet, so this one fails withConditionalCheckFailedExceptionand the original text survives. Thenupdate-itemwithADD(via the stand-in name#v, a good habit because DynamoDB reserves many common words) increments theViewsnumber in place and returns the new value,1.aws dynamodb put-item --table-name "$TABLE" \ --item '{"UserId":{"S":"user-a"},"CreatedAt":{"S":"2026-10-20T17:40Z"},"Text":{"S":"oops"}}' \ --condition-expression 'attribute_not_exists(UserId)' aws dynamodb update-item --table-name "$TABLE" \ --key '{"UserId":{"S":"user-a"},"CreatedAt":{"S":"2026-10-20T17:40Z"}}' \ --update-expression 'ADD #v :one' \ --expression-attribute-names '{"#v":"Views"}' \ --expression-attribute-values '{":one":{"N":"1"}}' \ --return-values ALL_NEW --query '[Attributes.Text.S, Attributes.Views.N]'
Check that it worked
Step 1 prints CREATING and then ready. Each write in step 2 prints 1.0. Step 3 prints Read the DynamoDB guide with 0.5, then again with 1.0. Step 4 shows a two-row table with the 2026-10-20 note above the 2026-10-02 note. In step 5 the put-item ends with ConditionalCheckFailedException, and the update prints Try a Query and 1.
Clean up
Deleting the table removes it and every item in it; there is nothing else to clean up because the lab created only this table. The wait command polls until DynamoDB reports the table gone (it can take up to a few minutes). The last two commands should end with a ResourceNotFoundException and print an empty list [].
aws dynamodb delete-table --table-name "$TABLE" --query 'TableDescription.TableStatus' --output text
aws dynamodb wait table-not-exists --table-name "$TABLE" && echo deleted
aws dynamodb describe-table --table-name "$TABLE"
aws dynamodb list-tables --query "TableNames[?starts_with(@, 'sandbox-notes')]"Gotchas
- The primary key is fixed once the table exists. Changing it means creating a new table and copying the data, so sketch your access patterns before you create it.
Scanreads the whole table and you pay for everything it reads, even items a filter then throws away. If you find yourself scanning often, the keys (or a secondary index) don't match your questions yet.- Global secondary indexes are always eventually consistent;
--consistent-readworks only on the table itself and on local secondary indexes. - A write whose condition fails still uses write capacity, and a read of an item that doesn't exist still uses read capacity. Both show up on an on-demand bill.
- News: since October 15, 2026, DynamoDB Streams consumers can start reading from a chosen point in time with the
AT_TIMESTAMPshard iterator type, and since October 20, 2026, a global table replica can be detached and kept as a standalone table.