DynamoDB Query with the AWS CLI

aws dynamodb query reads one partition, optionally narrowed by the sort key (Query vs Scan covers when that is the right call, and key condition expressions lists every legal operator). What the CLI adds on top is a pagination layer of its own, and it is the source of most surprises on this command.

Code

aws dynamodb query \
  --table-name 'Music' \
  --key-condition-expression '#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)' \
  --expression-attribute-names '{"#hashKey":"Artist","#rangeKey":"SongTitle"}' \
  --expression-attribute-values '{":hashKeyValue":{"S":"Arturo Sandoval"},":rangeKeyValue":{"S":"C"}}'

The #hashKey/#rangeKey aliases resolve to Artist/SongTitle through --expression-attribute-names, which is what keeps a reserved word from breaking the command. Add --no-scan-index-forward for descending sort-key order; ascending is the default.

Pagination

By default the CLI auto-paginates — it follows LastEvaluatedKey internally and prints the combined result. To page manually (e.g. for large result sets), control it with:

aws dynamodb query \
  --table-name 'Music' \
  --key-condition-expression '#hashKey = :hashKeyValue' \
  --expression-attribute-names '{"#hashKey":"Artist"}' \
  --expression-attribute-values '{":hashKeyValue":{"S":"Arturo Sandoval"}}' \
  --page-size 100 \
  --max-items 50
# The output includes a "NextToken"; pass it back with --starting-token to continue.

Explanation

The CLI hides the pagination, including from the cost figure. Seeded a partition with 30 items of ~60 KB each, about 1.8 MB and therefore two service pages, then ran the same query three ways with --return-consumed-capacity TOTAL:

default (auto-paginate)  Count: 30   CapacityUnits: 132.0   LastEvaluatedKey: null
--no-paginate            Count: 18   CapacityUnits: 132.0   LastEvaluatedKey: {…S017}
--max-items 3            Count: 18   items printed: 3       NextToken: eyJFeGNsdXNpdmVTdGFydEtleSI6…

Paging by hand showed the true cost: page 1 was 18 items at 132.0 units, page 2 was 12 items at 88.0, so the query really consumed 220.0 read units. The auto-paginated run made both calls, returned all 30 items, and reported 132.0. The CLI merges Items and Count across pages but not ConsumedCapacity, so the printed number understates this query by 40%. If you are sizing capacity from CLI output, page manually or you will size for one page.

--max-items is a printing limit. It is not a Limit. The third run above printed three items and still reported Count: 18 and ScannedCount: 18, because the service page it truncated was 18 items and roughly 1 MB. You paid for all of it. The DynamoDB parameter that genuinely bounds the read is Limit, and the CLI exposes it as --page-size.

So the two flags do unrelated jobs. --page-size becomes the API's Limit and changes what each service call reads; --max-items only decides how much of the merged result reaches your terminal, and emits a NextToken for the rest. That token is a base64 blob of the CLI's own bookkeeping, not DynamoDB's LastEvaluatedKey, and it goes back in through --starting-token.

There is no --limit and no --exclusive-start-key. Check aws dynamodb query help on 2.36.9 and neither appears in the synopsis: the CLI removes both of DynamoDB's pagination parameters and substitutes its own three. So the natural loop, take LastEvaluatedKey from one call and feed it to the next, has no flag to feed it to. The way back to the raw API is --cli-input-json, which takes the request verbatim:

--cli-input-json with "Limit": 5 and an "ExclusiveStartKey"
  → Count: 5   CapacityUnits: 37.0   LastEvaluatedKey: {"Artist":…,"SongTitle":"S007"}

Note that this also switched the paginator off: the run returned one page and a real LastEvaluatedKey even without --no-paginate. If you are writing a shell loop over a large partition, --cli-input-json is the honest form, and --no-paginate is the quick one.

--query runs after the money is spent. The global --query flag is JMESPath applied to the response in your shell. A JMESPath expression such as Items[?Year > '2010'] looks like a filter and is not: every item was read, transferred and billed before JMESPath saw it. --filter-expression at least stops the data being transferred, but AWS is explicit that it "is applied after the items have already been read; the process of filtering does not consume any additional read capacity units" (fetched 2026-07-28). That cuts both ways, since it means the filter does not reduce them either. The only way to read less is a narrower key condition or an index.

A page is 1 MB, regardless of what you asked for. "A single Query operation will read up to the maximum number of items set (if using the Limit parameter) or a maximum of 1 MB of data" (fetched 2026-07-28). A partition wider than that always paginates, which is why the 30-item query above was never one call.

Querying an index takes one more flag. --index-name switches the key condition to that index's keys; a global secondary index also rejects --consistent-read. See Query a GSI with the AWS CLI.

Do it visually

Getting the key condition, the two placeholder maps and the pagination loop right in one command is the whole difficulty here. The free DynamoDB Query Builder composes the request, including the index and the paging, and emits it as a runnable CLI command.

To run queries against your own tables — key-condition form, a grid that pages as you scroll, copy the request back out as a CLI command — download DynoTable.

References

Measured 2026-07-28 with aws-cli/2.36.9 against DynamoDB Local (amazon/dynamodb-local) on port 9000, over a partition of 30 items at ~60 KB each. The counts, tokens and capacity readings above are captured output. DynamoDB Local computes capacity with the documented rounding rules; treat the absolute figures as a demonstration of the shape, and measure your own tables against the service before sizing.

Build this request visually

Compose this operation in the free DynamoDB Query Builder — key condition, filter, index, Limit, sort order, and a pagination loop — and copy it back as a runnable SDK v3, CLI, or boto3 program.

Open the DynamoDB Query Builder

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.