2. Tables, keys and items
DynamoDB · 2. Tables, keys and itemsThe 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.
Tables and items
Section titled “Tables and items” 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.
Attribute types
Section titled “Attribute types” 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 12The primary key
Section titled “The primary key”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.
Item collections
Section titled “Item collections”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 profileBecause 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.
The limits that shape designs
Section titled “The limits that shape designs” 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 onlyThe 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.
Naming keys generically
Section titled “Naming keys generically” 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 typesGeneric 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.
Prefixing and key formatting
Section titled “Prefixing and key formatting” 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.
Working with items in code
Section titled “Working with items in code”The wire format is not what you write. Every SDK offers a higher-level client that marshals native types.
The document client
Section titled “The document client”import boto3from 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.Type surprises worth knowing
Section titled “Type surprises worth knowing” 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 onlyThe null versus missing distinction catches people writing conditional expressions: an attribute explicitly set to null still exists, so attribute_not_exists will not fire.
Designing the item shape
Section titled “Designing the item shape” 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)Reading the model back
Section titled “Reading the model back”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.
What to take from this chapter
Section titled “What to take from this chapter”- 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#0007sorts beforeITEM#0060. ItemCountis 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.