# Bean & Bark: your first real query

Fourth day on the job. The whole store — 240,000 orders — lives in one DynamoDB table called `BeanAndBark`. There is no "revenue" button and no "find Dana" button. There is a query box, a founder who needs an answer, and eighteen minutes. This is where you learn the one distinction DynamoDB is built around: a **Query** that touches three items, versus a **Scan** that reads — and bills you for — all 240,000.

**You'll learn**

- Tell a cheap **Query** from a full-table **Scan** — and read which one you got before you run it
- See why **what you read** (and pay for) is not the same as **what you asked for**
- Pull every order for one customer in a single read, using its **partition key**

> **8:52am — the ticket:** **Nadia** (founder): *"morning! Dana — our biggest wholesale account — says her last order never arrived and she's not happy. can you pull her orders before standup? 🙏"* Standup is at 9:10. Go.

## Your first instinct is a trap

You know Dana's name, so you ask for it by name. It reads like plain English and it returns exactly the right orders — which is precisely why it's dangerous. `CustomerName` isn't a key, so DynamoDB has no door to walk through: it reads **every item in the table** and throws almost all of them away. That's a **Scan**.

```sql
-- Your instinct: you know her name, so ask for it.
SELECT OrderId, Placed, Status, Total
FROM "BeanAndBark"
WHERE CustomerName = 'Dana Okafor'   -- a plain attribute → Scan
```

Before you run it — a guess. Of the 240,000 items, how many does DynamoDB *read* to hand back Dana's three orders? Flip between the two variants below and watch the **"Executes as"** strip: the first asks by name, the second asks by key. Same three orders come back. The bill does not.

**Try it · find Dana's orders**

_By name — Scan_

```sql
SELECT OrderId, Placed, Status, Total
FROM "BeanAndBark"
WHERE CustomerName = 'Dana Okafor'
```

_Executes as:_ Scan · BeanAndBark — No key in the WHERE — so every one of the ~240,000 items is read and billed, then filtered down to three. What you pay for is the read, not the three rows you wanted. Flip to "By key" to ask the same question for the price of one partition.

_By key — Query_

```sql
SELECT OrderId, Placed, Status, Total
FROM "BeanAndBark"
WHERE pk = 'CUSTOMER#c-014' AND begins_with(sk, 'ORDER#')
```

_Executes as:_ Query · BeanAndBark — The pk equality is the key condition — DynamoDB goes straight to Dana's partition and reads only her orders. Same three rows. Items read drops from ~240,000 to 3, and the strip turns green before anything runs.

_Run it live in DynoStudio:_ https://www.kanject.com/dynostudio/

Read the two footers. Same three orders, same answer for Nadia — but the Scan read ~240,000 items to find them (`< 0.01%` of what it touched was useful) and the Query read exactly 3 (`100%`). **What DynamoDB reads is what you pay for; what it returns is only what you asked for.** The whole craft is closing that gap — and a single **partition key** equality closes it completely.

> **On the sample? Your numbers will be smaller:** If you imported the ~5,900-item sample rather than a full ~240,000-order table, this Scan reads a few thousand items, not ~240,000 — but the shape holds exactly: the Query still reads **3**, the Scan still reads **everything there is**, and the studio’s "Executes as" strip shows your real counts live. The lesson is the *ratio*, not the absolute.

> **🎯 That keyed read is par:** One partition, zero waste — you can't do this question more cheaply. You sent Dana her tracking at 9:04, six minutes to spare, and you did it for **1 read capacity unit** instead of hundreds. The by-name Scan would have given the exact same answer and quietly billed you for the whole table. Getting the answer is easy. Getting it *for par* is the job.

**Recap**

- A **partition key** equality is the one thing that turns a read into a cheap **Query**; a plain attribute like `CustomerName` forces a full **Scan**.
- **Read ≠ returned.** You pay for every item DynamoDB reads, not the rows it hands back — the "Executes as" strip shows the gap before you run.
- One partition key gives you a customer's whole collection of orders in a single, cheap read.

**Before standup — check yourself**

**1. You need one customer's orders from `BeanAndBark` (keys `pk`, `sk`). Which `WHERE` is a cheap **Query**?**

- `WHERE pk = 'CUSTOMER#c-014' AND begins_with(sk, 'ORDER#')` ✓ — the `pk` equality is the key condition — DynamoDB reads only that customer's partition, and `begins_with(sk, …)` narrows it to orders.
- `WHERE CustomerName = 'Dana Okafor'` — `CustomerName` isn't a key, so there's no key condition — this Scans the whole table and filters, billing you for every item read.
- `WHERE Total > 300` — a filter on a non-key attribute — the classic Scan. It reads all 240,000 items before keeping the ones over $300.

**2. The by-name Scan read ~240,000 items to return 3 rows. How many items do you pay for?**

- ~240,000 — everything it read ✓ — a Scan is billed on items **read**, not returned. The filter runs *after* the read, so the 3 matching rows cost you all 240,000.
- 3 — only the rows that came back — that's what you *asked for*, not what DynamoDB *read*. The filter discards the rest **after** they're read and billed.
- 0 — reads are free, only writes cost — reads are metered too, in read capacity units. A wide Scan is one of the easiest ways to run up a bill.

**Try it yourself**

**1. Turn the Scan into a Query**

This reads the whole table: `SELECT * FROM "BeanAndBark" WHERE CustomerName = 'Dana Okafor'`. Rewrite it to read only Dana's partition.

_Hint:_ You looked her account up once — her partition key is `CUSTOMER#c-014`. Swap the name filter for a `pk` equality.

_Solution:_ Ask by key: `pk = 'CUSTOMER#c-014'` is the key condition (a Query), and `begins_with(sk, 'ORDER#')` keeps it to orders — one partition, nothing wasted.

```sql
SELECT OrderId, Placed, Status, Total
FROM "BeanAndBark"
WHERE pk = 'CUSTOMER#c-014' AND begins_with(sk, 'ORDER#')
```

**2. Just the unshipped ones**

Nadia only cares about orders that haven't shipped. Keep it a Query, but return only Dana's orders whose `Status` is still `pending`.

_Hint:_ The `pk` equality stays (it's what keeps this a Query). Add `Status` as a filter *within* that partition — it's cheap here because you're only reading Dana's items.

_Solution:_ Keep the key condition and add a filter: `Status = 'pending'` runs against the handful of items in Dana's partition, not the whole table.

```sql
SELECT OrderId, Placed, Status, Total
FROM "BeanAndBark"
WHERE pk = 'CUSTOMER#c-014' AND begins_with(sk, 'ORDER#')
  AND Status = 'pending'
```

> **Where next?:** You found one customer, cheaply. [Targeted reads](https://www.kanject.com/docs/dynostudio-partiql-reads/) is the full field guide to the Query/Scan line — every predicate, which ones narrow the read and which merely filter, and exactly what each one costs. Then you can go back to Nadia and answer the *harder* question: not "where's Dana's order," but "why did a whole region go quiet last month."

---
_Source: https://www.kanject.com/docs/dynostudio-bean-bark-first-query/ · Kanject Docs_
