DynamoDB basics: keys, reads and writes

Amazon Web Services · Practical · 16 min

DynamoDB is the default serverless database on AWS, with consistent single-digit-millisecond performance at any scale if keys are right.

DynamoDB is a fully managed key-value and document database. There are no servers or connections to manage: you create a table and call an HTTPS API.

Every table has a primary key:

  • A partition key alone (each item is found by its exact key), or
  • A partition key plus a sort key: items sharing a partition key form an item collection, sorted by the sort key, which you can query by range (begins_with, between, >).

Items are schemaless apart from the key attributes and can be up to 400 KB.

Operations, cheapest first: GetItem (one item by full key), Query (items of one partition key, optionally filtered by sort-key condition, up to 1 MB per page), and Scan (reads the entire table, slow and expensive; avoid in request paths). FilterExpression is applied after reading, so it doesn't reduce cost. Writes are PutItem, UpdateItem (with update expressions such as SET total = total + :n) and DeleteItem, and all can be conditional.

Reads are eventually consistent by default; pass ConsistentRead=True for strongly consistent reads on the table (at double the cost).

Capacity: on-demand mode bills per request and scales automatically, the right default; provisioned mode with auto scaling is cheaper for steady, predictable load. TTL deletes expired items for free.

In boto3 the Table resource converts Python values to DynamoDB's typed format for you; numbers come back as Decimal. Names such as name, status and date are reserved words, so use ExpressionAttributeNames placeholders.

Example

import boto3
from boto3.dynamodb.conditions import Key

ddb = boto3.resource("dynamodb", region_name="eu-west-1")
table = ddb.create_table(
    TableName="orders", BillingMode="PAY_PER_REQUEST",
    KeySchema=[{"AttributeName": "customer_id", "KeyType": "HASH"},
               {"AttributeName": "order_date", "KeyType": "RANGE"}],
    AttributeDefinitions=[{"AttributeName": "customer_id", "AttributeType": "S"},
                          {"AttributeName": "order_date", "AttributeType": "S"}])
table.wait_until_exists()

for day, total in [("2026-09-30", 20), ("2026-10-02", 35), ("2026-10-15", 12)]:
    table.put_item(Item={"customer_id": "c42", "order_date": day, "total": total})

resp = table.query(KeyConditionExpression=Key("customer_id").eq("c42")
                   & Key("order_date").begins_with("2026-10"))
print([(i["order_date"], i["total"]) for i in resp["Items"]])
# [('2026-10-02', Decimal('35')), ('2026-10-15', Decimal('12'))]

item = table.get_item(Key={"customer_id": "c42", "order_date": "2026-09-30"})["Item"]
print(item["total"])   # 20

Remember

  • Primary key = partition key (+ sort key); design keys around queries
  • GetItem and Query are efficient; Scan reads everything
  • FilterExpression doesn't reduce read cost
  • On-demand capacity is the default; numbers come back as Decimal

Watch out for

  • Using Scan with a FilterExpression in an API endpoint, which reads and bills the whole table.

Check yourself

  1. You need all orders for customer c42 in October 2026. Which call is efficient?
  2. A Query uses FilterExpression total > 100 and returns 3 of 500 items in the partition. What is billed?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html