When NOT to Use Single-Table Design in DynamoDB
Single-table design is the default advice for DynamoDB: one Query hands back a
parent and its children, with no joins and no N+1. It is also a trade, buying read
speed with a rigid, opaque schema, and three kinds of workload pay more than they
get.
When should you not use single-table design in DynamoDB?
Avoid single-table design when your workload is heavy OLAP analytics, plain CRUD over a handful of unrelated entities, or entities that scale and fail independently. In those cases multiple tables read better and stay flexible, and they cost the same per request as long as you don't read their entities together.
- Heavy analytics? Don't single-table. Overloaded keys are OLAP-hostile, so export the table to S3 and query it with Athena.
- Plain CRUD with a handful of access patterns? One table per entity is fine, readable, and costs you nothing in performance.
- Entities that scale or fail independently? Separate tables let you back up, encrypt and scale each one on its own.
- Single-table still wins when your patterns are known, related, and high-volume: that's the case it was built for.
Know what single-table actually costs
Single-table design moves cost off the read path and onto legibility and
flexibility. A table holding five entity types behind PK/SK is hard to read and
hard to change.
A new access pattern can mean a backfill across every item type in the table: a
throttled parallel Scan that writes the new index attributes, covered in
DynamoDB migrations. When your access patterns don't
justify that rigidity, reach for multiple tables.
Per request, the price is identical in any layout: each GetItem bills 0.5 read
units per 4 KB eventually consistent, whichever table it hits. Multiple tables cost
more only when a screen reads entities from several of them, because each request
rounds up on its own.
AWS's own example: one query for two items under 4 KB is 0.5 read units, two queries are 1 (data-modeling foundations). The single-table design cost table works the numbers.
Don't single-table an analytics workload
DynamoDB is built for OLTP: small, known, point-and-range reads. Analytics is
OLAP: GROUP BY, big aggregates, ad-hoc slicing across the whole dataset. The
two pull in opposite directions.
Single-table design makes OLAP worse. Overloaded keys and mixed entity types mean an analytics job must first untangle which item is which before it can sum anything.
Coming from SQL, the reflex is to write the aggregate against the live table.
In DynamoDB that's a full Scan: you pay for and read every item, which is the
Scan footgun at full volume.
Move analytics to a different engine. AWS's export to S3 "enables you to perform analytics and complex queries on your data using other AWS services such as Athena" (DynamoDB export to S3). Two paths, depending on how fresh the numbers must be:
- Batch or one-off: turn on point-in-time recovery
(
aws dynamodb update-continuous-backups --table-name T --point-in-time-recovery-specification PointInTimeRecoveryEnabled=true), runaws dynamodb export-table-to-point-in-time, and query the DynamoDB JSON files with Athena. Convert them to Parquet with AWS Glue if you want columnar scans. The export consumes no read capacity. - Continuous: the zero-ETL integration with Amazon Redshift replicates updates every 15 to 30 minutes and also needs point-in-time recovery.
Don't single-table simple CRUD
If your app reads and writes a few unrelated entities by their own id, and you have maybe three access patterns, single-table buys you nothing.
Take a Tenants table and an ApiKeys table for a small B2B tool:
| TenantId | Name | PlanTier |
|---|---|---|
| TNT-8842 | Northwind | pro |
| KeyId | TenantId | Scope |
|---|---|---|
| KEY-01H9... | TNT-8842 | read-write |
Each query is a GetItem by id, or a Query on a GSI keyed by TenantId.
There's no parent-and-children fetch to optimize, so there's nothing for an
overloaded partition to win. Two clear tables read better than one murky one.
The trap is cargo-culting single-table because it's "best practice." The standard approach is to start from access patterns, and if the patterns are trivial, the simple shape is the right shape.
Split tables that scale or fail independently
A single table shares one backup, one encryption key, one stream and one pool of capacity. When two entities have different write rates or durability needs, that shared fate becomes a liability.
Picture a fleet-tracking system. Vehicles change rarely; their telemetry pours in every second:
| VehicleId | Make | Model | Region |
|---|---|---|---|
| VEH-204 | Volvo | FH16 | eu-west |
| VehicleId | DeviceTs | SpeedKph | Fuel |
|---|---|---|---|
| VEH-204 | 2026-06-23T10:00:01Z | 88 | 0.61 |
Telemetry is keyed by vehicle with the timestamp as sort key, so "the last hour for
VEH-204" is one Query and two vehicles reporting in the same second never
collide.
Two tables let you back up the vehicle catalog without the telemetry history and give each its own stream consumer. AWS lists separate backups, encryption keys and streams as advantages of multiple-table design (data-modeling foundations).
Capacity splits too, so a telemetry write burst can't use up the catalog's provisioned writes. AWS counts the other side of that as a cost: each table needs its own alarms and is scaled independently.
TTL is not on AWS's advantages list: it works per item, so in one table only the telemetry items that carry the expiry attribute expire.
Map the decision before you commit
Walk the workload through one gate: are the entities related, and are the patterns known and high-volume? If not, multiple tables.
Read it as a flow: start at the top and follow the first branch that matches.
Only the No / Yes / Yes path ends at single-table design.
Single vs multiple, head to head
| Factor | Single-table | Multiple tables |
|---|---|---|
| Related reads | One Query, no joins | Client-side join or extra round-trips |
| Readability | Opaque overloaded keys | One entity per table, self-documenting |
| New access pattern | Backfill can touch every item type | Backfill touches one entity's table |
| Analytics / OLAP | Untangle entity types before aggregating | Still export, but cleaner per-entity |
| Independent scaling | Shared backup, key, stream and capacity | Back up, encrypt and scale separately |
| Best when | Known, related, high-volume patterns | Unrelated, evolving, or analytical |
Pitfalls + next steps
The mirror-image mistake is over-correcting: splitting related, high-volume entities into a table-per-entity and rebuilding SQL-style joins in your app. That's the N+1 trap single-table removes. Pick the shape the access patterns ask for.
When you do model relationships, lean on the right index type: see
GSI vs LSI before you add one. When you write a query
against a multi-table schema, sketch the KeyConditionExpression in the
DynamoDB Expression Builder first: if you
can't write a key condition for it, the query is a Scan.
Then try DynoTable to browse both shapes against your own tables and see which one your access patterns actually want.