DynamoDB basics for DynoStudio

View .md

New to DynamoDB? Start here before the PartiQL dialect pages. This is the short mental model DynoStudio expects: items live in tables, keys decide what can be read cheaply, indexes add alternate read paths, a Scan is a warning sign until you know you want one — and whether you use one table or many is a design choice, not a given.

You'll learn
  • Build the beginner mental model — tables, items, partition keys, and sort keys
  • Tell a Query (targeted, cheap) apart from a Scan (reads — and bills for — everything)
  • See how a GSI adds an alternate read path to the same table
  • Understand why DynamoDB modeling starts from access patterns, not entities
  • Know when to keep entities in separate tables, and when to reach for single-table design

The library picture

Imagine a huge library. The table is the library. An item is one book. The partition key tells you which shelf to visit. The sort key tells you where on that shelf to look.

A Query goes straight to the right shelf. A Scan walks every shelf and checks every book. A GSI is another set of shelf labels, so you can find the same books by email, status, region, or whatever your app needs.

That is the whole beginner picture. The rest of DynamoDB is mostly learning how to design good shelf labels before your app has millions of books.

Here is that library. Click the ways of reading it and watch which shelves get walked — and what a GSI really is (the same books, new shelf labels):

The library, clickable Toy data
CUST#1 delivered CUST#2 shipped CUST#3 processing CUST#4 cancelled CUST#5 refunded CUST#6 returned
54 orders on 6 shelves — the table. Each shelf is one customer (the partition key); a customer's orders sit in sort-key order.

The shape of DynamoDB data

A DynamoDB table stores items. An item is a bag of attributes: strings, numbers, booleans, lists, maps, sets, and nulls. The table only requires the key attributes; everything else can vary from item to item. That is why DynamoDB feels flexible, but also why your access patterns matter early.

Here is one item from Bean & Bark, the coffee roaster you'll meet in the tutorial. The simplest way to lay this out is the way you would in SQL: a table per thing. Below is one row of a Customers table, keyed by CustomerId — shown as the table you already think in, or, one click away, as the JSON item DynamoDB actually stores:

table Customers
CustomerId PK partition key — one customer Name Email Region
c-014Dana Okafordana@okaforcoffee.coEast
{  "CustomerId": "c-014",  // partition key — one customer  "Name":       "Dana Okafor",  "Email":      "dana@okaforcoffee.co",  "Region":     "East"}

That one key, CustomerId, is this table's partition key — give DynamoDB a CustomerId and it goes straight to that customer. Orders are different: a customer has many, so an Orders table uses a composite key — a partition key that groups, plus a sort key that orders within the group:

table Orders
CustomerId PK partition key — whose orders OrderDate SK sort key — a full timestamp, unique within the customer and still date-sortable OrderId Total a number, not "$540" (store integer cents in production) — so filters like Total > 100 compare numerically Status
c-0142026-06-19T14:03:00ZOrder#8842540delivered
{  "CustomerId": "c-014",  // partition key — whose orders  "OrderDate":  "2026-06-19T14:03:00Z",  // sort key — a full timestamp, unique within the customer and still date-sortable  "OrderId":    "Order#8842",  "Total":      540,  // a number, not "$540" (store integer cents in production) — so filters like Total > 100 compare numerically  "Status":     "delivered"}

CustomerId (the partition key) keeps all of one customer's orders in the same place; OrderDate (the sort key) lines them up in time, so you can ask for all of a customer's orders or just the ones since June. A sort key has to be unique within its partition, which is why it's a full timestamp and not a bare 2026-06-19 — otherwise a customer's second order that day would overwrite the first (if two can share an instant, append the order id: OrderDate#OrderId). Two tables, one shape each — this is multi-table design, and it maps closely to how you'd model the same store in a relational database. Hold onto it; there's an alternative worth meeting later.

Query is the happy path

sql
-- Targeted: you know the partition key, so DynamoDB goes to one customer.SELECT * FROM "Orders"WHERE CustomerId = 'c-014'-- Broad: Region isn't a key, so DynamoDB reads the whole table, then filters.SELECT * FROM "Customers"WHERE Region = 'West'

A Query starts with a partition-key value — a CustomerId you already know. It can optionally narrow within that partition using the sort key. That is DynamoDB at its best: predictable, direct, and cheap enough to reason about.

A Scan reads — and bills you for — every item in a table or index, then filters out the rows you did not want. "Everyone in the West region" has no key to start from, so it reads every customer and keeps the matches. The filtered-away rows are still read and still paid for, so "the query only returned three rows" tells you nothing about the cost. DynoStudio warns when a statement becomes a Scan because that is usually the first place a beginner accidentally turns a small test query into an expensive production habit.

That doesn't make a Scan forbidden — reading a small lookup table, exporting once, or running a deliberate full-table job are all fine reasons to read everything. The warning is about doing it by accident, at scale.

Feel the rule before you memorise it. Flip the predicates below and watch the statement, the "Executes as" strip, and the bill respond — the partition-key equality is the only toggle that changes what DynamoDB reads:

Shape the query, watch the billSimulated
SELECT CustomerId, OrderDate, Status FROM "Orders"
WHERE CustomerId = 'c-014'
Executes asQuery · Orders
Query KeyConditionExpression: CustomerId = 'c-014'
One-partition key read — this is the cheap side of the cost rule.
Items read — billed1,204
Rows returned1,204
151 RCU · 1 partition · table holds 432,926 items
Simulated: one fixed, illustrative table — only the toggles change the numbers. The real strip classifies live at every keystroke in DynoStudio.

Indexes are alternate doors

sql
-- Base table: get one customer's orders by CustomerId.SELECT * FROM "Orders"WHERE CustomerId = 'c-014'-- A GSI reads the SAME orders by a different key — here by Status, so-- "every order still processing" is a Query, not a Scan.SELECT * FROM "Orders"."ByStatus"WHERE Status = 'processing'-- ...but a GSI obeys the same rule: filter it on a non-key attribute,-- and it is still a Scan, just of the index.SELECT * FROM "Orders"."ByStatus"WHERE Total > 100

A global secondary index, or GSI, is another key shape for the same table. If the Orders table is keyed by CustomerId, a GSI can let you read the same orders by Status — so "every order still processing", across all customers, becomes a Query instead of a Scan. DynoStudio shows indexes as explicit targets so you can see whether a statement reads the base table or an alternate access path.

A GSI obeys the same Query/Scan rule as the base table — an equality on the GSI's partition key is a Query, but filtering a GSI on a non-key attribute is still a Scan, just of the index. An index gives you a new cheap key to read by, not a free pass: the way to fix a Scan is usually a GSI whose partition key is the attribute you're filtering on.

Access patterns come first

Relational modeling often starts with normalized entities, then writes queries later. DynamoDB modeling usually starts with the questions the app must answer: "get a customer by id", "list a customer's orders", "find every order still processing". The table and index keys are designed so those questions become Queries instead of Scans.

Those questions drive a bigger decision, too — not just how to key each table, but how many tables to use at all.

One table, or many?

Everything so far used a table per entity — a Customers table, an Orders table. That is multi-table design, and it is the most familiar: it looks like SQL, each table has one clear shape, and it is easy to reason about. The tradeoff is that answering "this customer and all her orders" means reading two tables and stitching the results together.

DynamoDB offers something with no SQL equivalent: put different kinds of item in the same table, keyed so related ones sit together. That is single-table design. A shared pk/sk does the work, with a prefix marking what each item is — flip to the table view and you can see it, the profile row and the order rows filling different columns in the one table:

table BeanAndBark
pk PK sk SK Name Total
CUSTOMER#c-014PROFILEDana Okafor
CUSTOMER#c-014ORDER#8842540
CUSTOMER#c-014ORDER#9013360
[  { "pk": "CUSTOMER#c-014", "sk": "PROFILE", "Name": "Dana Okafor" },  { "pk": "CUSTOMER#c-014", "sk": "ORDER#8842", "Total": 540 },  { "pk": "CUSTOMER#c-014", "sk": "ORDER#9013", "Total": 360 }]

Now a single Query on pk = 'CUSTOMER#c-014' returns the customer and every one of her orders together — one query path, not a two-table stitch — because they share a partition. That is the payoff: fewer round-trips for data you always read together. (A Query still pages at 1 MB, so a very large history comes back over several pages; it's the same key, not a new access pattern.) The price is the modeling — you design those # key shapes up front, around your access patterns, and the table no longer has one tidy schema.

There is also a second reason to reach for many tables, and it has nothing to do with access patterns — it is about who owns the data. When a system is split into services, each service usually keeps its own table: a login service owns a users table, an orders service owns an orders table, so the two can deploy, scale, and be secured on their own. Sharing one table across services would tie them back together. (Where the orders service needs a buyer's name, it references them by id or keeps a small synced copy — the buyer's home stays with the login service.)

So the honest answer is often both: single-table within a service where items are read together, and separate tables across services that own different data. DynoStudio doesn't push either — it reads, queries, and prices whichever you chose. Modeling across services goes deeper on that choice.

The Bean & Bark tutorial is single-table end to end because it is one store — one service's data — so co-locating a customer with her orders (the CUSTOMER#/ORDER# shape above) is exactly right, and reading her history starts from one cheap Query path. Grow it into a platform with separate identity and orders services, and you'd spread across several tables instead.

Where DynoStudio fits

DynoStudio does not replace DynamoDB concepts. It makes them visible while you work: the "Executes as" strip shows the native request, key-aware statements tint as targeted reads, Scan-shaped statements warn before they run, and schema-derived functions make common access patterns repeatable. When you're connected, the Scan warning is even quantified against the table's live item count — it estimates how many items the read will bill, not just that it's a Scan.

Once those ideas make sense, move into the PartiQL dialect overview. First queries and Targeted reads stay beginner-friendly; the joins, aggregates, and transaction pages are references you can return to when your data model needs them.

AWS reading path
Recap
  • A table holds items; the partition key picks the shelf, the sort key orders it.
  • A Query goes straight to one partition; a Scan walks — and bills for — everything, even rows a filter throws away. DynoStudio flags Scans before they run.
  • A GSI is an alternate key shape for the same table, shown as an explicit target — and it's Query-or-Scan by the same rule.
  • Design keys around the questions your app asks, so those questions become Queries, not Scans.
  • Multi-table design (a table per entity) is the familiar start; single-table design (many entity types in one table, keyed by prefix) trades modeling effort for fewer requests on related data.
Check yourself4
A table holds three million items. SELECT * FROM "Customers" WHERE Region = 'West' returns 3 rows. How many items does DynamoDB bill you for reading?
What turns a read into a cheap, targeted Query instead of a Scan?
You keep filtering Orders by Status, which isn't a key. What's the right fix?
You constantly need a customer and all their orders together. Which design makes that a single Query, rather than stitching two tables?
Was this page helpful?