Visual DynamoDB Design and Documentation with DbSchema
For developers and architects who already run DynamoDB tables; partition keys, sort keys and secondary indexes are explained where the design needs them.
On this page
Open a DynamoDB table in the AWS console and you see its keys and its indexes. Which attributes the items carry, which table points at which, and why each index exists are written down nowhere, so a new teammate learns the design by reading code. DbSchema connects to DynamoDB, reads every table's primary key and key attributes, samples the items for the other attribute names, and draws it all as a diagram. On that diagram you add the relations DynamoDB does not store, query the items with PartiQL, and export the design as HTML5 documentation.
What a DynamoDB table defines, and what it leaves to the items
The examples below come from a small shop of three tables. This is the definition of the table that holds the orders, written as input for aws dynamodb create-table --cli-input-json file://orders.json:
{
"TableName": "Orders",
"AttributeDefinitions": [
{ "AttributeName": "OrderId", "AttributeType": "S" },
{ "AttributeName": "UserId", "AttributeType": "S" },
{ "AttributeName": "OrderDate", "AttributeType": "S" }
],
"KeySchema": [
{ "AttributeName": "OrderId", "KeyType": "HASH" }
],
"GlobalSecondaryIndexes": [
{
"IndexName": "UserOrders",
"KeySchema": [
{ "AttributeName": "UserId", "KeyType": "HASH" },
{ "AttributeName": "OrderDate", "KeyType": "RANGE" }
],
"Projection": { "ProjectionType": "ALL" }
}
],
"BillingMode": "PAY_PER_REQUEST"
}
OrderId is the partition key, which DynamoDB calls HASH. UserOrders is a global secondary index with UserId as its partition key and OrderDate as its sort key (RANGE). The other two tables follow the same pattern: Users has the partition key UserId, and OrderItems has a composite primary key of OrderId and ProductId, so one order holds many items.
Total, which every order carries, appears nowhere in that definition. Amazon's documentation says it directly: other than the primary key, a table is schemaless, and neither the attributes nor their types are declared beforehand. AttributeDefinitions names only the attributes that some key uses, and a key attribute can only be a string (S), a number (N) or a binary (B). Declaring a non-key attribute there is refused. DynamoDB Local answered a Users table with Name added to AttributeDefinitions with:
ValidationException: The number of attributes in key schema must match the number of attributes defined in attribute definitions.
Besides the primary key, a table can have two kinds of secondary index. They differ in what their key may be and in when you can create them:
| Key | Created with | Strongly consistent reads | Per table | |
|---|---|---|---|---|
| Primary key | partition key, optional sort key | CreateTable | yes | 1 |
| Global secondary index | any partition key, optional sort key | CreateTable or UpdateTable | no | 20 by default |
| Local secondary index | the table's partition key, another sort key | CreateTable only | yes | 5 |
The 20 is a default quota that AWS can raise. A table with a local secondary index also caps each item collection, meaning all the items that share one partition key value, at 10 GB. So unless a query needs a strongly consistent read under another sort key, a global index is the one to add, because you can add it to a live table later. This update-table input adds an index on the users' email:
{
"TableName": "Users",
"AttributeDefinitions": [
{ "AttributeName": "Email", "AttributeType": "S" }
],
"GlobalSecondaryIndexUpdates": [
{
"Create": {
"IndexName": "UsersByEmail",
"KeySchema": [
{ "AttributeName": "Email", "KeyType": "HASH" }
],
"Projection": { "ProjectionType": "KEYS_ONLY" }
}
}
]
}
An index finds items; it does not guard them. The key values of a global secondary index do not need to be unique, so UsersByEmail returns every user who shares an address.
Design the keys from the access patterns
In DynamoDB the keys decide which reads are cheap, so AWS advises listing the questions the application asks before designing the schema. For the shop, each question maps to one key or index:
| Access pattern | Served by | Condition |
|---|---|---|
| A user by id | Users primary key | UserId = ? |
| An order by id | Orders primary key | OrderId = ? |
| A user's orders, newest first | UserOrders index | UserId = ?, sorted by OrderDate |
| The items of an order | OrderItems primary key | OrderId = ? |
PartiQL, the SQL-compatible language that DynamoDB accepts, can show each one. These statements fill the tables with one user, three orders and two order items. INSERT writes one item per statement:
INSERT INTO Users VALUE {'UserId': 'u1', 'Name': 'Ana', 'Email': '[email protected]'}
INSERT INTO Orders VALUE {'OrderId': 'o100', 'UserId': 'u1', 'OrderDate': '2026-09-14', 'Total': 54.5}
INSERT INTO Orders VALUE {'OrderId': 'o101', 'UserId': 'u1', 'OrderDate': '2026-10-02', 'Total': 12}
INSERT INTO Orders VALUE {'OrderId': 'o102', 'UserId': 'u2', 'OrderDate': '2026-10-05', 'Total': 30}
INSERT INTO OrderItems VALUE {'OrderId': 'o100', 'ProductId': 'p7', 'Quantity': 2}
INSERT INTO OrderItems VALUE {'OrderId': 'o100', 'ProductId': 'p9', 'Quantity': 1}
Order o102 belongs to u2, a user that does not exist, and DynamoDB stored it without a word. The third access pattern reads the global index, which PartiQL names as "Table"."Index":
SELECT * FROM "Orders"."UserOrders" WHERE UserId = 'u1' ORDER BY OrderDate DESC
The newer order comes first:
| OrderId | UserId | OrderDate | Total |
|---|---|---|---|
| o101 | u1 | 2026-10-02 | 12 |
| o100 | u1 | 2026-09-14 | 54.5 |
The fourth reads one item collection of OrderItems, in sort key order:
SELECT ProductId, Quantity FROM OrderItems WHERE OrderId = 'o100'
| ProductId | Quantity |
|---|---|
| p7 | 2 |
| p9 | 1 |
Both statements name the partition key in WHERE, which is what keeps them cheap. Amazon's PartiQL SELECT reference says a SELECT without an equality or IN condition on the partition key becomes a full table scan. INSERT also differs from SQL in one way worth knowing: it refuses a key that already exists, where the API's PutItem replaces the item. Inserting u1 a second time failed on DynamoDB Local with:
DuplicateItem: Duplicate primary key exists in table
Should the shop be one table or three? AWS documents both designs. A single table with generic keys pays off when one request reads several kinds of item together, and multiple tables are "a good and sufficient approach" when each access pattern reads one kind. Every pattern above reads one kind of item, so three tables keep each key readable. With a single-table design, DbSchema draws one table carrying the generic keys and the attribute names of every kind of item it sampled.
Reverse-engineer the tables into DbSchema
DbSchema reaches DynamoDB through a JDBC driver that it downloads the first time you connect. The steps follow the Connect to Database page:
- Choose File → New Model, keep Connect to Database selected, and click DynamoDB.
- Choose New Connection and press Connect to DynamoDB.
- In Server Host, type the regional endpoint, such as
dynamodb.eu-west-1.amazonaws.com, or127.0.0.1with the port8000for DynamoDB Local. - For AWS, open the Network and Driver tab and type the credentials into Properties, as shown below.
- Press Connect, tick the tables in Reverse Engineer the Database, and press Import.
The DynamoDB driver reads the access key from Properties rather than from the Database User and Password fields. One line sets the key, the region, and an https:// endpoint:
accessKeyId=AKIA...;secretAccessKey=...;region=eu-west-1;endpoint=https://dynamodb.eu-west-1.amazonaws.com
That key needs permission to list, describe and scan the tables, which is all that reverse-engineering does. It changes nothing in DynamoDB.
DynamoDB Local is the safer place to try a design. It keeps a separate set of tables for each access key and region, so DbSchema, which connects to it without your application's key, sees an empty database. Start it with -sharedDb and every client sees the same tables:
docker run -p 8000:8000 amazon/dynamodb-local -jar DynamoDBLocal.jar -inMemory -sharedDb
For each table, DbSchema reads the key schema and the key attributes from DescribeTable, and the other attribute names from a scan of the first 50 items:
| In DynamoDB | In DbSchema |
|---|---|
| Partition key and sort key | Primary key, partition key first |
Key attribute of the table or of an index, type S, N, B | VARCHAR, NUMERIC, BINARY |
| Attribute names in the first 50 items | Attributes of type JSON |
Double-click a table header to open the table dialog. On an imported table, the key attribute is the primary key and every sampled attribute is JSON:
The sampled attributes are an approximation of what the items hold, not a schema that DynamoDB keeps. An attribute that only items after the first 50 carry does not show on the diagram. A PartiQL SELECT in the SQL Editor returns every attribute of the items it reads, so query the table when you need the full picture.
Draw the relations DynamoDB does not store
Orders.UserId holds the key of a user, and OrderItems.OrderId the key of an order, yet DynamoDB accepted o102 for a user it has never seen. DbSchema records those links as virtual foreign keys, lines that live in the DbSchema model file and never reach the database:
- Drag
UserIdinOrdersontoUserIdinUsers. - Check that Virtual is ticked in the dialog.
- Press OK.
Model → Find Virtual Foreign Keys proposes virtual foreign keys between attributes with matching names, which DynamoDB designs often have. The lines then work for you in the Data Explorer. Right-click Users and choose Data Explorer, click a user, then follow the virtual foreign key to Orders: the second table lists that user's orders, and it reloads each time you click another user.
The Data Explorer also edits, adds and deletes items. Each change runs in DynamoDB at once, and so does an INSERT, UPDATE or DELETE that you run in the SQL Editor: DynamoDB gives DbSchema no transaction to hold open, so Rollback has nothing to undo. Try edits on DynamoDB Local before you make them on a production table.
Create a table from DbSchema
New tables can start on the diagram. Choose Table → Create Table, type the name, and add the key attribute in the Columns tab with the Primary Key box ticked. Its type has to be STRING, NUMBER or BINARY, the three key types that DynamoDB accepts.
When DbSchema is connected, OK creates the table in DynamoDB at once. DbSchema creates it with provisioned capacity: 5 read and 5 write capacity units, unless the Options tab of the table dialog says otherwise:
READ_CAPACITY_UNITS=10 WRITE_CAPACITY_UNITS=10
Database → Offline (connect later) keeps the new table in the model only, so you can design first and synchronize the model with the database later. DbSchema creates the table with its primary key; add secondary indexes with update-table, as shown in the first section.
Document the design and keep it in Git
DynamoDB has nowhere to store a description, so the access pattern behind each key and index tends to live in someone's head. DbSchema keeps table and attribute comments in the model, and File → Export As... → HTML5/PDF Documentation writes the documentation as one HTML5 file, a PDF or a Markdown file. The HTML5 file opens in any browser with no server, shows the diagram as a vector image with the comments as mouse-over tooltips, and lists every table with its attributes and indexes.
The model itself is a .dbs file, and DbSchema has Git built in. File → Git Collaboration opens the Git dialog, where you clone a repository, commit the model, push it and pull your team's changes, and Model History lists what each commit changed. A merge changes only the file; the tables in AWS change when someone synchronizes the model with the database.
Download DbSchema, connect it to DynamoDB Local or to your AWS region, and import your tables to see their keys, indexes and sampled attributes on one diagram. Connecting, reverse-engineering, the diagram and PartiQL in the SQL Editor are in the free Community Edition. Saving the model with its virtual foreign keys, the Data Explorer that follows them, synchronizing the model with the database, and the HTML5 documentation are in DbSchema Pro, which the Git workflow also needs.