Skip to content

2. Tables, keys and items

The model is small enough to learn in an afternoon and constrained enough that the constraints are the interesting part. This chapter is the vocabulary; every later chapter assumes it.

TABLE a namespace with a primary key schema
ITEM one record. up to 400 KB, including
attribute names.
ATTRIBUTE a typed name/value pair. every item may
have entirely different attributes,
EXCEPT the key attributes, which are
required and typed.
{
"PK": {"S": "CUSTOMER#42"},
"SK": {"S": "ORDER#2026-03-11#A917"},
"total": {"N": "48.20"},
"items": {"L": [{"S": "SKU-1"}, {"S": "SKU-7"}]},
"paid": {"BOOL": true},
"meta": {"M": {"source": {"S": "web"}}}
}

That is the wire format. Most SDKs offer a document client that maps it to native types, and you should use it — hand-writing {"N": "48.20"} is how numbers become strings in production.

SCALAR S string (UTF-8, non-empty for keys)
N number — 38 digits of precision,
transmitted as a STRING
B binary (base64 on the wire)
BOOL
NULL
DOCUMENT M map (nested, arbitrary depth)
L list (heterogeneous)
SET SS string set
NS number set (no duplicates,
BS binary set no ordering,
cannot be empty)
THINGS THAT CATCH PEOPLE
• numbers are strings on the wire, and N has 38
digits of precision — more than a double. use
the string form in the SDK for money.
• sets cannot be empty; removing the last element
removes the attribute.
• an empty string IS allowed for non-key attributes
(this changed in 2020), but NOT for key attributes.
• there is no date type. use ISO-8601 strings, which
sort correctly, or epoch numbers for TTL. → ch 12

Two shapes, and the choice is permanent:

SIMPLE KEY partition key only
PK = "USER#42"
→ one item per key. GetItem and PutItem only.
COMPOSITE KEY partition key + sort key
PK = "USER#42", SK = "ORDER#2026-03-11"
→ many items per partition key, SORTED by SK.
→ Query can read a RANGE of them.
PARTITION KEY hashed → decides the partition, the
machine, and the throughput ceiling
for that key (ch 3)
Queried ONLY by equality.
SORT KEY stored in sorted order within the
partition. Queried by =, <, >,
BETWEEN, begins_with.

Everything DynamoDB can do efficiently is a consequence of those two sentences: hash to find the partition, then binary-search a sorted range inside it.

All items sharing a partition key form an item collection, and it is the unit that most of DynamoDB’s interesting behaviour operates on.

PK = CUSTOMER#42 ← one item collection
┌─────────────────────────────────────────────┐
│ SK = ADDRESS#home {city, zip} │
│ SK = ORDER#2026-01-02 {total, status} │ sorted
│ SK = ORDER#2026-03-11 {total, status} │ by SK
│ SK = PROFILE {name, email} │
└─────────────────────────────────────────────┘
ONE Query returns any contiguous slice:
SK begins_with "ORDER#" → all orders
SK between "ORDER#2026-01" and "ORDER#2026-04"
SK = "PROFILE" → just the profile

Because sort keys are compared as strings (or numbers), the key design decides which slices are cheap. That is why practitioners obsess over key formatting — ORDER#2026-03-11 sorts usefully, Order 11/3/2026 does not.

ITEM SIZE 400 KB, including attribute
names. Bigger → store in S3
and keep a pointer. (ch 13)
QUERY/SCAN RESULT 1 MB per request, before
filters. Then you paginate.
PARTITION KEY VALUE up to 2048 bytes
SORT KEY VALUE up to 1024 bytes
ITEM COLLECTION unlimited — UNLESS the table
has a local secondary index,
in which case 10 GB. (ch 7)
BATCH 25 items (write), 100 (read)
TRANSACTION 100 items (ch 9)
GSIs per table 20 (soft)
LSIs per table 5, at table creation only

The 1 MB limit deserves emphasis because it surprises people: filters are applied after the 1 MB is read, so a filtered query can return zero items and still consume the capacity for a megabyte of reads.

ENTITY-SPECIFIC GENERIC
customerId / orderDate PK / SK
works fine for one required for single-table
entity per table design (ch 8), because one
table holds many entity
types

Generic names look unhelpful in the console and are the right choice whenever a table holds more than one kind of item. The item’s actual type lives in a separate attribute:

{"PK":"CUSTOMER#42", "SK":"ORDER#A917", "_type":"Order", …}

That _type attribute is not decoration — it is what makes stream processing (chapter 10) and sparse indexes (chapter 7) work.

CONVENTION WHY
CUSTOMER#42 the prefix namespaces the
ORDER#A917 key space so different
PRODUCT#SKU-1 entity types never collide
ORDER#2026-03-11#A917 ISO dates sort correctly
as strings; a uniqueness
suffix prevents collisions
# as separator rare in real data, sorts
low, easy to split on
ZERO-PADDING NUMBERS
SK = "ITEM#7" sorts AFTER "ITEM#60"
SK = "ITEM#0007" sorts BEFORE "ITEM#0060" ✓

That second box has caused more quiet bugs than any other item in this chapter.

The wire format is not what you write. Every SDK offers a higher-level client that marshals native types.

import boto3
from decimal import Decimal
table = boto3.resource("dynamodb").Table("AppData")
table.put_item(Item={
"PK": "CUSTOMER#42", "SK": "ORDER#A917",
"total": Decimal("48.20"), # NOT float
"tags": {"priority", "gift"}, # a set
"lines": [{"sku": "S1", "qty": 2}],
})
THE DECIMAL RULE
DynamoDB numbers have 38 digits of precision.
Python floats do not. Passing a float raises, or
silently loses precision in other languages.
Use Decimal / BigDecimal / string-backed numbers
for anything that is money.
empty string allowed for non-key attributes,
rejected for key attributes
empty set not allowed — remove the
attribute instead
null vs missing {"NULL": true} is an attribute
that EXISTS with a null value.
attribute_not_exists() is false
for it. (ch 4)
nested depth 32 levels
booleans in sets no such thing — sets are S, N
or B only

The null versus missing distinction catches people writing conditional expressions: an attribute explicitly set to null still exists, so attribute_not_exists will not fire.

ONE ITEM PER… ask: what do I read together,
and what do I write together?
TOGETHER → one item a profile's name, email and
preferences, read on every page
APART → separate a user's 10 KB of settings,
items, same PK read once a month, versus their
200-byte session state, written
constantly
WHY IT MATTERS
reads round to 4 KB — a fat item costs more on
EVERY read
writes round to 1 KB and rewrite the WHOLE item
on PutItem — a 10 KB item costs 10 WCU to
change one field
UpdateItem still bills for the resulting item
size, so splitting hot fields out is a
real saving (ch 13)
Terminal window
aws dynamodb describe-table --table-name AppData \
--query 'Table.{keys:KeySchema, attrs:AttributeDefinitions, size:TableSizeBytes, count:ItemCount}'
NOTE: ItemCount and TableSizeBytes update roughly
every 6 hours. They are for capacity planning, not
for application logic — and never for a count your
users see.

If you need a live count of anything, you maintain it yourself as you write (chapter 9’s atomic counters) or compute it downstream from a stream (chapter 10). There is no cheap SELECT count(*), by design.

  • A table holds items (max 400 KB each, including attribute names) of typed attributes, with only the key attributes required.
  • Types are scalars (S, N, B, BOOL, NULL), documents (M, L) and sets (SS, NS, BS). Numbers travel as strings, sets cannot be empty, and there is no date type — use ISO-8601 strings.
  • The primary key is partition key alone or partition key plus sort key. The partition key is queried only by equality; the sort key supports ranges and begins_with.
  • The key schema is permanent. Changing it means a new table and a migration.
  • Items sharing a partition key form an item collection, stored sorted by sort key — so key formatting decides which slices are cheap.
  • Know the limits: 1 MB per query page (filters apply after), 25/100 items per batch, 100 per transaction, 10 GB item collections when an LSI exists.
  • Use generic key names (PK/SK) plus an explicit type attribute whenever one table holds several entity types.
  • Format keys deliberately: prefixes to namespace, ISO dates to sort, and zero-padded numbers so ITEM#0007 sorts before ITEM#0060.
  • ItemCount is stale by up to six hours — never use it in application logic.

Next: partitions — how the key becomes a machine, how partitions split, and the throughput ceiling that lives behind every hot-key story.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source