,

DynamoDB Filtered Export: Key vs Filter vs Projection

Robotic arm pulling one partition of records from a DynamoDB table into an S3 bucket through a filter

DynamoDB filtered export landed on October 1, 2026, and it quietly kills one of the dumbest rituals in serverless data engineering: exporting a 2 TB table to S3 so you can throw away 1.9 TB of it in Athena. You now hand export-table-to-point-in-time a filter specification, and DynamoDB writes only the items and attributes you asked for. The catch is that the three expression types inside that spec behave very differently on your bill.

What DynamoDB filtered export actually adds

The feature is a new --filter-specification parameter on the existing export-table-to-point-in-time API. Everything else you know about exports to S3 still holds: the export is asynchronous, it reads from point-in-time recovery data rather than the live table, so it consumes zero RCUs and never throttles production traffic, and you need PITR enabled first. It works for both FULL and INCREMENTAL exports, in all commercial Regions except GovCloud.

The spec is a JSON document with up to five keys:

  • KeyConditionExpression uses Query syntax. The partition key must be a single equality; the sort key can take any key condition (=, BETWEEN, begins_with, comparisons).
  • FilterExpression uses Scan syntax and is evaluated after items are read.
  • ProjectionExpression picks which attributes land in the output.
  • ExpressionAttributeNames and ExpressionAttributeValues work exactly like they do in Query and Scan, with typed values such as {"S":"tenant-A"}.

KeyCondition reduces what you pay; Filter does not

This is the part people will get wrong in their first month. Export billing is based on the amount of data read, at the same rate as a regular export, with a 10 MB minimum per export. Check the current number on the DynamoDB pricing page rather than trusting a blog post.

A KeyConditionExpression narrows the read itself. DynamoDB only touches the partition (and sort-key range) you named, so the billed size drops along with the output. A FilterExpression is applied after the read, exactly like a Scan filter: the output shrinks, the S3 bill shrinks, the downstream Athena bill shrinks, but the export charge is still computed on everything DynamoDB had to read. ProjectionExpression trims columns on the way out and also does not change the data read.

So the rule is simple: if your selection can be phrased as a key condition, phrase it that way. If your data model makes that impossible, that is a data model conversation, not an export conversation.

{
"KeyConditionExpression": "#pk = :pk AND begins_with(#sk, :sk)",
"ProjectionExpression": "#pk, #sk, #st, total",
"ExpressionAttributeNames": {
"#pk": "pk",
"#sk": "sk",
"#st": "Status"
},
"ExpressionAttributeValues": {
":pk": {"S": "TENANT#acme"},
":sk": {"S": "ORDER#2026-09"}
}
}

Note the #st alias. Status is on the reserved words list, and the export API is just as picky about that as Query is.

Three conveyor belts showing key condition, filter and projection stages of a DynamoDB export

Kicking it off and checking what happened

aws dynamodb export-table-to-point-in-time \
--table-arn arn:aws:dynamodb:us-east-1:111122223333:table/orders \
--s3-bucket my-export-bucket \
--s3-prefix exports/acme/ \
--export-format DYNAMODB_JSON \
--filter-specification file://filter.json
aws dynamodb describe-export \
--export-arn arn:aws:dynamodb:us-east-1:111122223333:table/orders/export/01234567890123-abcdefgh

describe-export echoes the FilterSpecification back, which is handy for audit trails, along with the usual status, item count and billed size. Classmethod ran an early hands-on test with AWS CLI 2.37.8: a key-condition export returned 100 items, matching a query --select COUNT on the same key, and a Filter plus Projection export returned 333 items with the excluded attributes simply absent from each record. Output formats are DYNAMODB_JSON or ION, laid out as described in the export output docs.

Incremental exports have their own semantics

Filters on incremental exports are where you want to slow down and read twice. Each incremental record carries Keys, Metadata, NewImage and OldImage. Inserts carry only NewImage, deletes carry only OldImage. With a filter in play:

  • The filter is evaluated against the latest image of the item in the window.
  • Deletes are evaluated against the old image, since there is no new one.
  • The projection applies to both OldImage and NewImage.
  • An item that was created and deleted inside the same window does not appear at all.

The practical consequence: if you filter on a mutable attribute like Status = "SHIPPED", an item that moved from SHIPPED to RETURNED inside the window is evaluated on RETURNED and drops out. Your downstream table will never learn that it changed. Filtering on immutable attributes, or better, on keys, avoids that whole class of bug.

aws dynamodb export-table-to-point-in-time \
--table-arn arn:aws:dynamodb:us-east-1:111122223333:table/orders \
--s3-bucket my-export-bucket \
--s3-prefix exports/acme/incr/ \
--export-format DYNAMODB_JSON \
--export-type INCREMENTAL_EXPORT \
--incremental-export-specification ExportFromTime=2026-10-15T00:00:00Z,ExportToTime=2026-10-15T01:00:00Z,ExportViewType=NEW_AND_OLD_IMAGES \
--filter-specification file://filter.json

The ValidationException greatest hits

The API rejects malformed specs up front, which beats discovering the problem three hours into an export. The ones you will hit:

  • An empty filter specification. If you want everything, leave the parameter off.
  • A key attribute referenced in FilterExpression when a KeyConditionExpression is also present. Put key logic in the key condition where it belongs.
  • A non-equality condition on the partition key. No begins_with on pk; that is a sort-key privilege.
  • An un-aliased reserved word such as Status, Name or Date. Alias everything through ExpressionAttributeNames and stop thinking about it.

A cheap habit that pays off: before you launch an export, run the same key condition as a query --select COUNT against the table. If the count looks wrong there, it will look wrong in S3 too, and the query costs you a handful of read units instead of an export minimum and a wasted afternoon.

When to reach for it

Single-tenant extracts on a TENANT#id partition key are the obvious win, since that is a pure key condition and the read cost tracks the tenant size instead of the table size. Time-bounded slices on a date-prefixed sort key are another. Column-pruned exports for analytics are fine too, as long as you understand that projection saves storage and query cost, not export cost.

Where it is not a fit: cross-partition selections that only a filter can express on a huge table. You still pay to read the whole thing. At that point a GSI-shaped rethink, or a Streams-based feed, may be cheaper over a year. And remember that PITR is a hard prerequisite, so if a table has it switched off for cost reasons, that decision just got more expensive.

The short version: key conditions save money, filters save storage, projections save columns. Design your exports in that order.

At Enkompass (enkompass.net), this is our home turf. We design DynamoDB key schemas for multi-tenant SaaS teams so that exports, offboarding and analytics extracts fall out of a single key condition instead of a full-table scan. If you’d rather have seasoned cloud people handle it instead of learning the hard way at 2 AM, let’s talk. Reach out through our contact page or call us directly at +1-828-436-5667 — we’ll help you turn this from a someday-project into a done-deal.

Ready when you are

Let’s map your route to AWS

A free 60-minute architecture review with a senior engineer. No slides, no SDR, just a plan.