[KongchangAI]
· 4 min read· 2,275 words

DynamoDB in Practice: From Creating a Table via CLI to Understanding the Cost of Every Read

DynamoDB in Practice: From Creating a Table via CLI to Understanding the Cost of Every Read

A CLI lab showing how DynamoDB's partition key determines both write placement and read cost.

Using a note-taking app as its backdrop, the article runs a complete DynamoDB lab via the AWS CLI: creating a table, writing, reading, and deleting. Its core argument is that the partition key chosen at table creation determines both which record each write lands on and how much work each read requires. It demonstrates typed JSON writes, silent overwrites from duplicate-key puts, and compares three read paths—GetItem, Scan, and Query. Key conclusions: GetItem goes straight to a single record by key; Scan reads the whole table and filter expressions don't reduce the amount read, with ScannedCount vs Count being the direct signal of an inefficient read; on-demand capacity bills per request, and strongly consistent reads cost double.

DynamoDB in Practice: From Creating a Table via CLI to Understanding the Cost of Every Read

A note-taking app stores data on the server's local disk, and the moment the machine is replaced, the data is gone. So you decide to persist data in a managed table—and immediately run into two questions: what's the first command to type? And how do you tell whether a read is cheap or expensive?

This hands-on lab answers both questions using Amazon DynamoDB. It is accessed entirely through APIs, so every operation can be done from the terminal. The core argument running through this article is a single sentence: the partition key you choose when creating the table determines both which record each write lands on and how much work each read requires. Reading by key goes straight to a single record; a Scan reads through every record—and that difference shows up directly in the response the command returns.

DynamoDB lab overview

Environment Setup: One Identity, One Region, One Billing Mode

You need an AWS account (using a non-root day-to-day admin identity), version 2 of the AWS CLI, and you must be logged in as that identity. The lab uses the Asia Pacific (Mumbai) region ap-south-1, which you can set with:

aws configure set region ap-south-1

Confirm your current identity with aws sts get-caller-identity; the Account field in the output is your 12-digit account ID. The CLI documentation notes that this command requires no permissions, so it will run under any identity.

For billing mode, choose on-demand capacity: you pay per read and write request and don't need to plan capacity in advance. The official documentation calls it the default and recommended mode, and explicitly states that if a table has no traffic, no throughput charges apply in on-demand mode, while storage is billed separately. As of 2026-10-02, the pricing page lists a free storage allowance of 25 GB per region, plus 25 free read capacity units and 25 free write capacity units—but the latter two apply only to provisioned mode, not to on-demand requests. The few dozen requests in this lab will be billed per request.

Read Capacity Unit (RCU) and Write Capacity Unit (WCU) are the basic units DynamoDB meters. In provisioned mode, 1 RCU means one strongly consistent read per second of an item up to 4 KB, or two eventually consistent reads per second; 1 WCU means 1 KB written per second. When an item is larger than 4 KB (reads) or 1 KB (writes), it consumes multiple units rounded up. In on-demand mode you no longer pre-purchase these units, but requests are still converted into the same units for billing. Understanding this helps you convert the ConsumedCapacity number returned by a command into an actual cost: the smaller the capacity unit and the larger the item, the more expensive each request.

The Partition Key Is the First Decision: It Is the Record's Identity

A table is a collection of items, similar to rows; each item is a set of attributes, similar to fields. Every item must have a primary key that is unique across all other items. In the simplest case, the primary key is a single attribute—the partition key. DynamoDB feeds the partition key value into an internal hash function, and the hash result determines where the item is stored.

Think of it as a wall of numbered lockers: the table is the wall, and the partition key is the number on each locker door. To retrieve something, you state the number and walk straight to that locker.

The table in this lab is called hoc-notes, with a partition key of string type noteId and no sort key:

aws dynamodb create-table \
    --table-name hoc-notes \
    --attribute-definitions AttributeName=noteId,AttributeType=S \
    --key-schema AttributeName=noteId,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST \
    --tags Key=project,Value=hands-on-cloud

There are three teaching points in this command:

  • --attribute-definitions lists only the key. You never declare title or body. Apart from the key, the table is schemaless, and each record can carry different attributes.
  • KeyType=HASH marks noteId as the partition key (the older name for a partition key is "hash attribute," derived from the hash function). A sort key would use RANGE. Key attributes must be string, number, or binary.
  • PAY_PER_REQUEST selects on-demand capacity.

Why choose noteId instead of title? Because the app retrieves notes by id from links or API paths, and keys must be unique—titles repeat. As shown later: two notes sharing the same key cannot coexist.

Expected result: the command immediately returns "TableStatus": "CREATING". Re-check with describe-table until the status becomes "ACTIVE", at which point the table is ready for writes.

The partition key determines storage location via the hash function

The hash function here maps a partition key value of arbitrary length to an integer within a fixed range, and DynamoDB uses this to decide which internal physical partition the item lands on. The more evenly distributed the partition key values, the better items spread across partitions, reducing single-partition hotspots and throughput bottlenecks. If a large number of records share the same partition key value, they will concentrate on the same partition and be subject to that partition's throughput limits. When choosing a partition key, in addition to ensuring uniqueness, you should also try to make values well distributed—for example, using user IDs or random suffixes rather than low-cardinality fields like status or date.

Writing Items with Typed JSON, and Duplicate Keys Silently Overwrite

The CLI writes items using typed JSON: every value is wrapped in a type. A string is {"S": "Buy milk"}, and the number 2 is {"N": "2"}.

Notice that the number is in quotes. It looks wrong at first glance, but it's correct: N means this is a number, and the value itself is transmitted as a string. Writing {"N": 2} is the mistake to watch out for.

Write a few notes:

aws dynamodb put-item --table-name hoc-notes \
    --item '{"noteId": {"S": "n-001"}, "title": {"S": "Buy milk"}, "body": {"S": "Two litres"}}'

aws dynamodb put-item --table-name hoc-notes \
    --item '{"noteId": {"S": "n-002"}, "title": {"S": "Read chapter 3"}, "priority": {"N": "2"}}'

Write two or three more, each with a different id. Note that n-002 has a priority attribute while n-001 does not—this is exactly what schemaless storage looks like, like a phone contacts list: some contacts have a birthday, some have an email, and only the ID is mandatory.

Now deliberately cause a failure: write to n-001 again with different content. What you'll see: nothing. No error, no warning. Read it back, and it has become Call home, and there's no body attribute at all. The documentation describes put precisely: it "creates a new item, or replaces an existing item with a new item." Two writes to the same key are two versions of the same record, and the second completely replaces the first, attributes and all. In the locker analogy, you didn't add a second locker—you emptied the original locker and refilled it.

Two writes to the same key leave only one record

Schemaless doesn't mean there are no constraints: apart from the primary key, which must be declared before writing and has a fixed type, each item can freely add or remove attributes, and different items in the same table can have completely different attributes. The trade-off is that the database won't validate the type or existence of non-key attributes—the application layer must ensure consistency itself. In addition, a single item has a size limit (currently 400 KB), and there are limits on the nesting depth of attributes. If you find during design that some kind of data could grow unbounded, consider splitting it into multiple items rather than piling it into one record.

GetItem Reads Only One Record, Because It Goes Straight to the Key

The note-taking app has two different jobs: when opening a link to a specific note, the app knows the key; when showing the home page, the app must list all notes and doesn't know the keys. The two jobs correspond to two read approaches, so both need measuring.

GetItem takes a full primary key and returns that one record, or nothing. Make it report how much capacity it consumed:

aws dynamodb get-item --table-name hoc-notes \
    --key '{"noteId": {"S": "n-002"}}' \
    --return-consumed-capacity TOTAL

You'll see that record, plus a ConsumedCapacity block showing how much read capacity this request used. Note down this number—it's your baseline.

Reads are eventually consistent by default. Adding --consistent-read makes it a strongly consistent read, which reflects all previously completed writes, but at twice the cost. Run each of the two commands once and compare the two ConsumedCapacity numbers to see the difference.

Scan Reads Every Record, and Filter Expressions Don't Make It Cheaper

The home page has no key to go straight to anything, so you have to open every locker on the wall and look through them one by one—that's a Scan:

aws dynamodb scan --table-name hoc-notes --return-consumed-capacity TOTAL

You'll see all notes, a ConsumedCapacity block, and two counts: ScannedCount is the number of records read, and Count is the number of records returned. In an unfiltered Scan the two are equal.

Now use a filter expression to retrieve just one note:

aws dynamodb scan --table-name hoc-notes \
    --filter-expression "title = :t" \
    --expression-attribute-values '{":t": {"S": "Call home"}}' \
    --return-consumed-capacity TOTAL

You'll see Count drop to 1, but ScannedCount still equal to the total number of notes. The documentation explains why plainly: whether or not a filter expression is present, a Scan consumes the same read capacity. Filtering happens after reading—it narrows the answer, not the work, like opening every locker and keeping only the red bags.

Scan also executes in pages: each request reads at most 1 MB, and the CLI keeps requesting the next page until the entire table has been read.

A large ScannedCount alongside a small Count is the hallmark of an inefficient read. With five notes the waste is invisible; with a million notes, scanning to find one title reads a million records, whereas a key-based GetItem still reads just one. On-demand capacity bills by actual amount read, so this is both a performance and a cost problem.

GetItem versus a filtered Scan path

My rule of thumb is simple: use Scan when you genuinely need all records and the table is small (such as listing notes in a practice app); whenever you can state the key, use GetItem.

The difference between eventually consistent and strongly consistent reads is that an eventually consistent read may return slightly stale data but costs only half as much as a strongly consistent read; a strongly consistent read guarantees reading the latest value of previously successful writes but consumes double the read capacity. DynamoDB reads are eventually consistent by default because replicating data across Availability Zones takes time. For scenarios like a note-taking app that don't demand real-time freshness, the default read is fine; only in cases where you "write and then immediately read and must see the latest value" (such as verifying an account balance) is it worth paying double.

Query Is Equivalent to GetItem Here; It Only Shows Its Value with a Sort Key

The third read is Query, which returns all records that share the same partition key value. Its key condition expression must perform an equality check on the partition key, with the value passed through a placeholder starting with a colon:

aws dynamodb query --table-name hoc-notes \
    --key-condition-expression "noteId = :id" \
    --expression-attribute-values '{":id": {"S": "n-002"}}' \
    --return-consumed-capacity TOTAL

You'll see one record. In a table with only a partition key, no two records can share that value, so Query returns at most one—the same one GetItem returns.

Query's real value appears when the table also has a sort key—an optional second key attribute. Records sharing the same partition key are stored together, ordered by sort key. Picture a music table keyed by artist + song name: querying a particular artist is like pulling out the drawer with their name on it, with each song coming out in song-name order, without touching any other drawer.

The differences among the three read approaches

After Cleanup, No Further Charges Are Incurred

An idle on-demand table incurs no throughput charges, but stored data is still billed. Delete the table after the lab:

aws dynamodb delete-table --table-name hoc-notes

You'll see the table's description with the status changing to DELETING; after that, describe-table will report that the table cannot be found. The project = hands-on-cloud tag added at creation makes it easy to confirm the lab left nothing behind.

Key Takeaways

  • Declare only the key when creating a table; everything else is schemaless, and each record can differ.
  • Choose the partition key based on how data is accessed and its uniqueness—an id, never a title. Running put with an existing key silently replaces the entire record.
  • Write values with typed JSON, and put numbers in quotes: {"N": "2"}.
  • Add --return-consumed-capacity TOTAL to any read command to see how much that read cost. For every Scan, compare ScannedCount and Count.
  • GetItem reads only one record; Scan reads everything, and filter expressions don't reduce the amount read.
  • Under on-demand capacity you pay for requests and storage, and strongly consistent reads cost twice as much as the default read.

This lab only measured reads on a small number of records. It did not address how to design keys for richer access patterns, how to use conditional writes to prevent overwrites, or how to restrict application permissions to this one table—each of these deserves its own deep dive.

#DynamoDB#AWS CLI#partition key#read capacity#GetItem#Scan#on-demand capacity
Share:

Related articles