DynamoDB Projection Expressions
A projection expression is the SELECT col1, col2 of DynamoDB: a
comma-separated list of attribute names that tells GetItem, Query, or Scan
to return only those attributes instead of the whole item.
Do DynamoDB projection expressions reduce read cost?
No. A ProjectionExpression trims the response payload, not the read capacity you're billed. DynamoDB reads the full item from storage, meters the on its on-disk size, then drops the attributes you didn't name on the way out. To actually cut read cost, use a covering instead.
- It trims the payload, not the read cost. DynamoDB reads (and bills) the
full item from storage, then drops the attributes you didn't name on the way
out.
ProjectionExpressionis a network optimisation, not a capacity one. - It's how you fetch a public subset. Name the few attributes a caller is allowed to see; the rest never leave the table.
- Use
#nameplaceholders for anything that might be reserved. Plain attribute names in the expression collide with DynamoDB's ~570 reserved words and fail the request. - For real read savings, use a covering index instead. A GSI that projects only the columns you need is read at its own (smaller) size.
What it actually saves
Coming from SQL, you'd assume SELECT a, b scans less than SELECT *. In
DynamoDB that intuition is wrong. The
capacity unit for a read is computed from
the size of the item on disk, rounded up to the next 4 KB — before the
projection is applied. AWS is explicit: a ProjectionExpression does not change
the read capacity a request consumes.1
So a projection saves you two things, both real but both downstream of the read:
- Bytes over the wire. A 6 KB item returned as two small attributes is a tiny
response. On a
Querythat returns hundreds of items, that adds up fast. - Client-side work. Less to deserialise, less to hold in memory, less to leak into a log or an API response by accident.
What it does not save is the RCU. That's the footgun: people reach for a projection to cut their bill, see no change, and conclude DynamoDB is broken. It isn't — you measured the wrong lever.
Project a public user profile
Say you run a user directory. Each profile is one item, keyed so you can fetch a person by handle:
PK = "PROFILE#ada" (partition key)
SK = "PROFILE#ada" (sort key — single-item collection)
The item is fat. It carries the public face of the account plus a pile of private and operational attributes:
{
"PK": "PROFILE#ada",
"SK": "PROFILE#ada",
"displayName": "Ada L.",
"avatarUrl": "https://cdn.example.com/u/ada.png",
"bio": "Builds things.",
"emailAddress": "ada@example.com",
"passwordResetToken": "…",
"billingCustomerId": "cus_…",
"lastLoginIp": "…",
"internalRiskScore": 0.02
}A public profile card needs three fields. Fetching the whole item means
emailAddress, lastLoginIp, and internalRiskScore travel to a context that
should never see them. Name only the public subset:
GetItem PK = "PROFILE#ada" SK = "PROFILE#ada"
ProjectionExpression: displayName, avatarUrl, bio
The response carries three attributes. The private ones stay in the table — not filtered out by your app after arrival, but never serialised into the response at all. That's the security win, and it's the one that's hard to undo once a secret has already crossed a boundary.
You can assemble and copy this exact request — names, placeholders, and the SDK
call — in the
DynamoDB Expression Builder, which emits
the ProjectionExpression and ExpressionAttributeNames map for you.
Escape reserved words with # placeholders
Here's where a clean projection blows up. DynamoDB reserves a long list of
words — name, status, comment, size, timestamp, and hundreds more.2
If an attribute you're projecting is one of them, the raw name in the expression
is rejected.
Suppose the profile also has a status attribute ("active", "suspended").
This fails:
ProjectionExpression displayName, status
status is reserved. The fix is an expression attribute name — a #-prefixed
placeholder mapped to the real name:
ProjectionExpression displayName, #s
ExpressionAttributeNames { "#s": "status" }
The same mechanism reaches into nested attributes. To pull a single field out of a map, or one element of a list, use document-path syntax — and placeholder every segment, since any of them could be reserved:
ProjectionExpression #addr.#city, tags[0]
ExpressionAttributeNames { "#addr": "address", "#city": "city" }
A practical rule: placeholder everything. You never have to remember which of the ~570 reserved words you're standing on, and the expression reads the same either way.
When a covering index beats a projection
If you genuinely need to cut read cost — not just payload — the lever is a
Global Secondary Index that projects only the attributes you read. A GSI is a
separate copy of the data; you choose KEYS_ONLY, INCLUDE, or ALL for its
projection.3 A KEYS_ONLY or narrow INCLUDE index is physically
smaller per item, so a Query against it is metered at that smaller size.
That's a covering index: the query is answered entirely from the index, no trip back to the base table. Use it when a hot read pattern only ever needs a few attributes from large items.
ProjectionExpression | Covering GSI | |
|---|---|---|
| Cuts payload | Yes | Yes |
| Cuts read cost | No | Yes — read at the index's size |
| Extra storage | None | A second copy of the projected fields |
| Extra write cost | None | Writes propagate to the index |
| Best for | Hiding private fields; small wins | Hot reads of a few fields from big items |
The trade-off is honest: the index costs you storage and write capacity to save
read capacity. Worth it for a frequent read of a thin slice from a heavy item;
not worth it to shave a one-off GetItem. See
GSI vs LSI for picking the index type, and
when a GSI read can be stale before
you put one on the hot path.
Pitfalls and next steps
- Don't expect a smaller bill. A projection alone never changes RCU. If the number didn't move, that's the documented behaviour, not a bug.
- Placeholder reserved words. A bare
nameorstatusin the expression fails the request —#-map it. - Projecting key attributes is free and often useful — DynamoDB returns them cheaply, and they let you page or re-fetch.
- Reach for a covering index only when a hot pattern reads a few fields from large items; weigh the write/storage cost first.
Build the ProjectionExpression and its attribute-name map in the
Expression Builder, and
try DynoTable to run these projections against your own tables and
watch the response shrink.
- AWS DynamoDB Developer Guide, Working with Read and Write Operations — read capacity is based on item size before any
ProjectionExpressionis applied. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ ↩ - AWS DynamoDB Developer Guide, Reserved Words in DynamoDB. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ReservedWords.html ↩
- AWS DynamoDB Developer Guide, Attribute Projections (
KEYS_ONLY/INCLUDE/ALL). https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html ↩