Repository navigation
Conversation
…to OR branches (apache#19339) When a filter has the shape `P AND (A OR B)`, Pinot evaluates the `(A OR B)` subtree independently of `P`. If a branch of the OR contains a predicate with no index (e.g. IN_SUBQUERY evaluated via ExpressionScanDocIdIterator), that predicate is evaluated for every document matching the branch, not only for the documents that satisfy `P`. This optimizer distributes selective conjuncts (EQUALS/IN on single columns) from an enclosing AND into each OR branch: P AND (A OR B) → P AND ((P AND A) OR (P AND B)) This is sound because P ∧ (A ∨ B) ≡ P ∧ ((P ∧ A) ∨ (P ∧ B)) in three-valued logic when the filter only passes TRUE (SQL WHERE semantics). The optimizer runs after the merge optimizers (MergeEqInFilterOptimizer, MergeRangeFilterOptimizer) so that merges apply to the original AND/OR structure before distribution. Fixes apache#19339
|
Thanks for picking this up so quickly, @waterWang, and for the write-up in the description — the diagnosis is exactly right, and it's a correct rewrite. It's actually sound more generally than you claim: strong Kleene logic is a distributive lattice with an idempotent meet, so After spending more time on the problem I don't think the planner rewrite is the right primary fix, and I've opened #19408 with option 1 instead. My reasoning, in the order I'd weight it: It doesn't remove the cliff. The bug I care most about in #19339 is that adding an index to a column inside an OR branch makes a query slower, because the branch flips to the eager path in It taxes the common case to help a rare one. For a plain Its reach is narrower than the issue needs. Only single-column EQ/IN is distributed, so it can't reach the case @alexch2000 reported above — his selective block is None of this is a criticism of the implementation — it's that the layer is wrong for this particular bug. A planner rewrite has to decide, without segment knowledge, whether duplicating a predicate is cheap; at execution time the AND already has the matching document ids and can simply hand them down, with no heuristic, no duplicated index lookups, and at any nesting depth. Two smaller notes for whatever you work on next, since they'd apply to any version of this: the optimizer runs unconditionally, where #19339 asked for a query option (#19408 ships disabled by default for exactly this reason — the push-down materializes the filter result, so it needs to stay off for queries that stop at their LIMIT); and it inserts the same I'd really like you to stay involved — a review of #19408 from someone who has already been through this code would be genuinely useful. And there are two clear follow-ups if you want a bigger piece: pushing the restriction into the range index itself via |
Fixes #19339
Description
When a filter has the shape
P AND (A OR B), Pinot evaluates the(A OR B)subtree independently ofP. If a branch of the OR contains a predicate with no index (e.g.IN_SUBQUERYevaluated viaExpressionScanDocIdIterator), that predicate is evaluated for every document matching the branch, not only for the documents that satisfyP.This PR adds a new
DistributeConjunctsIntoOrFilterOptimizerthat distributes selective conjuncts (EQUALS/IN on single columns) from an enclosing AND into each OR branch:This rewrite is sound because in three-valued logic,
P ∧ (A ∨ B) ≡ P ∧ ((P ∧ A) ∨ (P ∧ B))when the filter only passes TRUE (SQL WHERE semantics).The bug in detail
The
AndDocIdSet#iterator()method chooses between two strategies at the eager/lazy threshold. When an OR subtree contains an AND with both index-based and scan-based children, the eager path materializes the index-based children into a bitmap over the entire segment before applying the scan-based predicate. This means expensive predicates likeIN_SUBQUERYare evaluated for every document in the segment, not just those matching the enclosing AND's selective predicateP.Adding an index to a column inside an OR branch makes the query slower because it triggers the eager path:
In production, the same query returning the same two rows went from ~500 to ~17,000,000
numEntriesScannedInFilterafter adding indexes to two columns in the OR branch.Test plan
DistributeConjunctsIntoOrFilterOptimizerTestwith 5 test cases:P AND (A OR B)→P AND ((P AND A) OR (P AND B))