Reladynamo · the query planner
Reladomo hands you an arbitrary boolean predicate. DynamoDB answers one shape of question. Everything interesting happens in the gap.
A DynamoDB query starts from one exact partition-key value. From there it can narrow by a sort-key condition, then discard rows with a filter — but the filter runs after the read, so you pay for every row it throws away.
An ORM predicate has no such shape. ruleId in (2,3) and priority > 5 and
businessDate = X is perfectly ordinary and maps onto three different mechanisms. The
planner's whole job is deciding which node goes where — and refusing when nothing fits.
businessDate lands. It is an as-of predicate, yet it becomes
a filter, not a key condition — the reason is the next diagram.The sort key is processing-major: v1#P#<processing>#B#<business>.
DynamoDB compares sort keys as byte strings from the left, so a prefix is queryable and a suffix
is not.
ALL
projection — see the
support contract.
The planner does not pretend the base table can serve it.An adversarial test generator found this one. ruleId in (2,3) and ruleId = 1 is
unsatisfiable — but a planner that consumes ruleId = 1 as the partition key and
forgets the rest will happily fetch row 1 and return it.
The fix was not to keep the dropped predicate as a filter. It was to convert the tree to disjunctive normal form, plan each conjunction independently, and give the planner a fifth outcome:
| Plan kind | When | Calls issued |
|---|---|---|
| GET_ITEM | full key known | 1 |
| QUERY | one partition key | 1 + pages |
| QUERY_FAN_OUT | OR across keys, capped at 100 by default | n |
| SCAN | opt-in only, else refused | whole table |
| EMPTY | provably unsatisfiable | 0 |
EMPTY issues no call at all. That is a correctness result, not an optimisation:
the executor asserts it never touches DynamoDB, because sending a query anyway would suggest the
verdict is advisory.
Why refusing matters more than optimising. A silent Scan does not fail — it returns correct rows, slowly, at rising cost, until the table is large enough that it doesn't. So a missing partition key throws by default, and running a Scan is a decision someone has to make in configuration and can be found later in review.
Almost every real Reladomo query is "the current row for this key": businessDate = X
and processingDate = ∞. If that path is slow, nothing else about the planner
matters. It gets a named fast path — FAST_PATH_CURRENT_ASOF, or
FAST_PATH_POINT_GET when the whole key is known and a single GetItem
will do.
Every plan also carries an explain output — chosen index, key condition, residual predicates, items scanned versus returned. Tests assert against it, so a query that quietly degrades into scanning ten thousand items to return three fails the build rather than the budget.