aws-sandbox.orgOne AWS service and one hands-on lab a day. Independent; not affiliated with AWS.
Thursday, October 29, 2026BeginnerDatabases

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.

TIMEAbout 15 minutes
COSTA dozen or so on-demand requests and a few hundred bytes of storage for a few minutes: a tiny fraction of one US cent. In US East (N. Virginia) the on-demand prices are listed at $0.625 per million writes and $0.125 per million reads for the Standard table class; check the page for your Region.
FREE TIER / CREDITSThe DynamoDB free tier's 25 GB of monthly storage covers this lab's data, but its free 25 read and 25 write capacity units apply to provisioned capacity, not on-demand requests, so the handful of requests here are billed (fractions of a cent). Accounts created since July 15, 2025 get Free Tier credits that apply to eligible usage; check your plan's terms.

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.
  1. Open CloudShell and create the table

    Choose the CloudShell icon in the console's top bar. The table uses UserId as partition key (HASH) and CreatedAt as sort key (RANGE), both strings. PAY_PER_REQUEST means on-demand. Deletion protection is off by default, which keeps cleanup simple. The wait command returns once the table is ACTIVE. If CloudShell restarts during the lab, set TABLE again.

    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 ready
  2. Write four items

    Each value is wrapped in a type marker: S for string, N for number (sent as a string). Only the two key attributes are required; every item may carry different extra attributes. user-a and user-b are made-up IDs. --return-consumed-capacity TOTAL shows 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"}}'
  3. Read one item two ways

    get-item needs the full primary key: both the partition key and the sort key. The first read is eventually consistent (the default) and should report 0.5 read units; the second adds --consistent-read and should report 1.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]'
  4. Query one user's October notes, newest first

    A Query must name one partition key value with =, and may narrow the sort key, here with begins_with. --no-scan-index-forward flips the order to descending. You should get two notes for user-a (the September note and user-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 table
  5. Block an accidental overwrite, then bump a counter

    By default put-item silently replaces an item with the same key. The condition attribute_not_exists(UserId) makes the write succeed only if no such item exists yet, so this one fails with ConditionalCheckFailedException and the original text survives. Then update-item with ADD (via the stand-in name #v, a good habit because DynamoDB reserves many common words) increments the Views number 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.
  • Scan reads 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-read works 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_TIMESTAMP shard iterator type, and since October 20, 2026, a global table replica can be detached and kept as a standalone table.