Skip to content
Codeloom
AWS

DynamoDB Basics for Developers

Learn DynamoDB fundamentals — tables, primary keys, queries, scans, GSIs, single-table design, and using boto3 in Python.

·3 min read · By Codeloom
Beginner 11 min read

What you'll learn

  • DynamoDB data model: tables, items, attributes, primary keys
  • Query vs Scan and when to use each
  • Global and Local Secondary Indexes
  • CRUD operations with boto3

Prerequisites

  • Basic understanding of databases
  • Python basics for the boto3 examples

DynamoDB is AWS’s fully managed NoSQL database. It handles scaling, replication, and backups automatically. You get single-digit millisecond latency at any scale — but you must design your access patterns upfront.

Core concepts

  • Table: a collection of items (like a SQL table)
  • Item: a single record (like a row), stored as JSON
  • Attribute: a field on an item (like a column)
  • Primary key: uniquely identifies each item

Primary key types

Partition key only: a single attribute that DynamoDB hashes to determine storage location.

Table: Users
PK: userId

Partition key + sort key (composite): combines two attributes. Items with the same partition key are stored together and sorted by the sort key.

Table: Orders
PK: customerId
SK: orderDate#orderId

Creating a table with boto3

import boto3

dynamodb = boto3.resource("dynamodb")

table = dynamodb.create_table(
    TableName="Users",
    KeySchema=[
        {"AttributeName": "userId", "KeyType": "HASH"},
    ],
    AttributeDefinitions=[
        {"AttributeName": "userId", "AttributeType": "S"},
    ],
    BillingMode="PAY_PER_REQUEST",
)

table.wait_until_exists()

CRUD operations

Put (create/replace)

table = dynamodb.Table("Users")

table.put_item(Item={
    "userId": "user-001",
    "name": "Alice",
    "email": "alice@example.com",
    "age": 30,
    "tags": ["admin", "active"],
})

Get

response = table.get_item(Key={"userId": "user-001"})
item = response.get("Item")

Update

table.update_item(
    Key={"userId": "user-001"},
    UpdateExpression="SET age = :age, #n = :name",
    ExpressionAttributeNames={"#n": "name"},
    ExpressionAttributeValues={":age": 31, ":name": "Alice Smith"},
)

Delete

table.delete_item(Key={"userId": "user-001"})

Query vs Scan

Query fetches items by partition key (and optionally filters by sort key). It is fast and efficient.

from boto3.dynamodb.conditions import Key

response = table.query(
    KeyConditionExpression=Key("customerId").eq("cust-001")
        & Key("orderDate").begins_with("2026-06")
)
items = response["Items"]

Scan reads every item in the table. It is slow and expensive — avoid in production.

response = table.scan(
    FilterExpression=Key("age").gt(25)
)

Global Secondary Indexes (GSI)

A GSI lets you query by a different attribute than the primary key.

table = dynamodb.create_table(
    TableName="Orders",
    KeySchema=[
        {"AttributeName": "orderId", "KeyType": "HASH"},
    ],
    AttributeDefinitions=[
        {"AttributeName": "orderId", "AttributeType": "S"},
        {"AttributeName": "customerId", "AttributeType": "S"},
    ],
    GlobalSecondaryIndexes=[{
        "IndexName": "CustomerIndex",
        "KeySchema": [
            {"AttributeName": "customerId", "KeyType": "HASH"},
        ],
        "Projection": {"ProjectionType": "ALL"},
    }],
    BillingMode="PAY_PER_REQUEST",
)

Query the GSI:

response = table.query(
    IndexName="CustomerIndex",
    KeyConditionExpression=Key("customerId").eq("cust-001")
)

Batch operations

with table.batch_writer() as batch:
    for i in range(100):
        batch.put_item(Item={
            "userId": f"user-{i:03d}",
            "name": f"User {i}",
        })

Conditional writes

table.put_item(
    Item={"userId": "user-001", "name": "Alice", "version": 1},
    ConditionExpression="attribute_not_exists(userId)",
)

This fails if the item already exists — useful for preventing overwrites.

Summary

DynamoDB gives you predictable performance at any scale, but demands that you design your keys and indexes around your access patterns. Use composite keys for one-to-many relationships, GSIs for alternate query patterns, and avoid scans in production. Start with PAY_PER_REQUEST billing and switch to provisioned capacity when your patterns stabilize.