AWS DynamoDB Tables
Create and operate DynamoDB tables: the key schema you cannot undo, attribute definitions, deletion protection, PITR and on-demand backups.
DynamoDB Tables
Creating a DynamoDB table takes one command. Getting it right matters more than usual because the key schema can never be changed, no migration, no ALTER TABLE. Changing a partition key means creating a new table and copying every item across.
Everything else, capacity mode, indexes, streams, TTL, backups, can be changed later. So the decision worth slowing down for is the keys, and that decision comes from your access patterns rather than from the shape of your data.
Creating a table
aws dynamodb create-table --table-name Orders \
--attribute-definitions \
AttributeName=customerId,AttributeType=S \
AttributeName=orderDate,AttributeType=S \
--key-schema \
AttributeName=customerId,KeyType=HASH \
AttributeName=orderDate,KeyType=RANGE \
--billing-mode PAY_PER_REQUEST \
--deletion-protection-enabled \
--tags Key=Environment,Value=production
aws dynamodb wait table-exists --table-name Orders
aws dynamodb describe-table --table-name Orders \
--query 'Table.[TableStatus,ItemCount,TableSizeBytes,BillingModeSummary.BillingMode]'
[ "ACTIVE", 0, 0, "PAY_PER_REQUEST" ]
ItemCount and TableSizeBytes update roughly every six hours, so a freshly loaded table reporting zero items is normal rather than a failed import.
Attribute definitions only describe keys
This trips up nearly everyone coming from SQL. --attribute-definitions is not a schema. It declares the types of attributes used in keys and indexes, and nothing else. Every other attribute is arbitrary and per-item:
aws dynamodb put-item --table-name Orders --item '{
"customerId": {"S": "C-4821"},
"orderDate": {"S": "2026-09-16"},
"amount": {"N": "149.99"},
"items": {"L": [{"S": "SKU-1"}, {"S": "SKU-2"}]},
"giftNote": {"S": "Happy birthday"}
}'
Nothing declared amount, items or giftNote, and the next item need not have them. The corollary is that declaring an attribute you do not use in a key is an error:
ValidationException: One or more parameter values were invalid:
Number of attributes in KeySchema does not exactly match number of
attributes defined in AttributeDefinitions
Only three key types exist: S (string), N (number) and B (binary).
Turn on deletion protection
aws dynamodb update-table --table-name Orders --deletion-protection-enabled
A delete-table call is immediate and takes every item with it. Deletion protection costs nothing, and it is the cheapest insurance in DynamoDB. Set it on every table that is not disposable, including in your Terraform or CloudFormation so a destroy in the wrong workspace fails loudly.
Backups: two different things
Point-in-time recovery gives continuous backup with second-level granularity for the last 35 days:
aws dynamodb update-continuous-backups --table-name Orders \
--point-in-time-recovery-specification PointInTimeRecoveryEnabled=true
aws dynamodb restore-table-to-point-in-time \
--source-table-name Orders \
--target-table-name Orders-restored \
--restore-date-time 2026-09-16T09:00:00Z
On-demand backups are explicit snapshots that live until you delete them, which is what you want before a risky migration or for retention beyond 35 days:
aws dynamodb create-backup --table-name Orders --backup-name pre-migration-2026-09-16
aws dynamodb list-backups --table-name Orders --query 'BackupSummaries[].[BackupName,BackupStatus]'
The constraint that surprises people: a restore always creates a new table. You cannot restore in place. Recovery therefore means restoring alongside, verifying, then repointing the application, so practise it before you need it, and note that indexes, TTL, streams and capacity settings are not all carried over automatically.
Table classes
aws dynamodb update-table --table-name AuditLog \
--table-class STANDARD_INFREQUENT_ACCESS
STANDARD_INFREQUENT_ACCESS cuts storage cost substantially and raises read and write cost. It pays off when storage dominates the bill. Audit logs, historical events, anything large and rarely read. For a table serving live traffic it usually costs more. Compare TableSizeBytes against consumed capacity before switching.
Global tables
Multi-region, multi-writer replication:
aws dynamodb update-table --table-name Orders \
--replica-updates '[{"Create": {"RegionName": "us-east-1"}}]'
Replication is asynchronous and conflicts resolve last writer wins, by the timestamp DynamoDB records. That is fine for data owned by one region at a time and wrong for anything where two regions might update the same item concurrently, a counter, an inventory level, because one update is simply discarded. Streams must be enabled, and the same key schema applies to every replica.
Loading and exporting in bulk
aws dynamodb import-table \
--s3-bucket-source S3Bucket=my-imports,S3KeyPrefix=orders/ \
--input-format DYNAMODB_JSON \
--table-creation-parameters '{
"TableName":"OrdersImported",
"AttributeDefinitions":[{"AttributeName":"customerId","AttributeType":"S"}],
"KeySchema":[{"AttributeName":"customerId","KeyType":"HASH"}],
"BillingMode":"PAY_PER_REQUEST"
}'
Import from S3 consumes no write capacity and is dramatically cheaper than looping put-item, but it can only create a new table, not add to an existing one. Export to S3 similarly consumes no read capacity and is the right way to feed Athena or a data pipeline:
aws dynamodb export-table-to-point-in-time \
--table-arn "$TABLE_ARN" --s3-bucket my-exports --export-format DYNAMODB_JSON
Export requires PITR to be enabled.
Common errors
ResourceInUseException: Table already exists: Orders
create-table is not idempotent. Check with describe-table first.
ValidationException: Number of attributes in KeySchema does not exactly match
number of attributes defined in AttributeDefinitions
You declared an attribute that is not part of a key or index, or omitted one that is.
ResourceNotFoundException: Requested resource not found
Right table name, wrong region. The most common cause by some distance. DynamoDB tables are regional.
ValidationException: Table is protected against deletion
Deletion protection is on. That is it working.
Quick reference
| Task | Command |
|---|---|
| Create | aws dynamodb create-table … |
| Wait until ready | aws dynamodb wait table-exists --table-name <t> |
| Inspect | aws dynamodb describe-table --table-name <t> |
| List | aws dynamodb list-tables |
| Protect | --deletion-protection-enabled |
| Enable PITR | aws dynamodb update-continuous-backups … |
| Snapshot | aws dynamodb create-backup --backup-name <n> |
| Restore | aws dynamodb restore-table-to-point-in-time … |
| Add a replica | --replica-updates '[{"Create":{"RegionName":"…"}}]' |
| Bulk load | aws dynamodb import-table … |
Key takeaways
- The key schema is permanent. Changing it means a new table and a full copy.
- Attribute definitions describe keys only: every other attribute is arbitrary per item.
- Declaring an unused attribute fails the call. Only key and index attributes belong there.
- Enable deletion protection everywhere. It is free and
delete-tableis instant. - PITR covers 35 days; on-demand backups cover longer. They are different features.
- Every restore creates a new table. Practise the repoint before you need it.
- Import and export via S3 consume no capacity and are far cheaper than item-by-item loops.
- Global tables resolve conflicts last-writer-wins, which silently discards concurrent updates.
Check your understanding
A create-table call that keeps failing
A create-table command declares attribute definitions for customerId, orderDate and amount, with customerId as HASH and orderDate as RANGE. It fails validation. Why?
Recovering from a bad write
A faulty deployment corrupted records over two hours. The table has point-in-time recovery enabled. What does recovery actually involve?
Frequently Asked Questions
Can I change a DynamoDB table's partition key after creating it?
No. The key schema is fixed for the life of the table, and there is no equivalent of ALTER TABLE. Changing it means creating a new table with the new schema and copying every item across, usually by exporting to S3 and importing, which consumes no capacity, or by a Lambda reading the old table's stream during a dual-write migration. This is why enumerating access patterns before creating the table matters so much more here than with a relational database.
What is the difference between attribute definitions and a schema?
AttributeDefinitions declares the data types of attributes that participate in the key schema or an index, and nothing more. Everything else is schemaless: two items in the same table can carry completely different attributes, and nothing needs declaring in advance. That is also why declaring an attribute you do not use in a key causes a validation error. The definitions list and the key schema must correspond exactly.
What is the difference between point-in-time recovery and on-demand backups?
PITR is continuous: once enabled, you can restore to any second within the last 35 days, which is what you want for recovering from a bad deployment. On-demand backups are explicit snapshots that persist until you delete them, so they cover longer retention and deliberate checkpoints such as "before this migration". They are billed differently and serve different purposes; most production tables want PITR always on, plus on-demand backups before risky changes.
Why does describe-table show ItemCount as 0 after I loaded data?
Because ItemCount and TableSizeBytes are updated roughly every six hours rather than in real time. A freshly loaded table legitimately reports zero, and that is not a sign the import failed. For an accurate current count you would have to scan the table with --select COUNT, which consumes read capacity proportional to the table size, usually not worth it just to confirm a load succeeded.
Should I use a DynamoDB global table for multi-region?
Only if you understand the conflict behaviour. Global tables replicate asynchronously and resolve conflicts last-writer-wins, so if two regions update the same item at nearly the same moment, one update is silently discarded. That is fine when each item is effectively owned by one region, user data pinned to a home region, for example, and wrong for counters, inventory levels or anything where a lost update matters. Where it fits, it gives genuine multi-region writes with very little work.
What is the DynamoDB Standard-IA table class for?
Reducing storage cost for data that is rarely read, at the price of higher per-request read and write cost. It suits audit logs, historical events and archives. Tables that are large relative to their traffic. For a table serving live application traffic the extra request cost usually outweighs the storage saving, so compare TableSizeBytes against your consumed capacity before switching. The change is a single update-table call and is reversible.