Repository navigation
Conversation
Selection among the plans in one tertiary plan-cache entry went by smallest plan hash, so an unconstrained plan could poison the entry and keep being handed to queries that qualified for a better plan. Replaces the hash-based StableSelectorCostModel with PlanCacheCostModel, composed from the tiebreakers introduced in #3993. It is PlanningCostModel minus every tiebreaker that reads a plan operator's match candidate, and it always costs under the default planner configuration. Fixes #4642.
|
I'm trying to model a use case:
#1: |
| @Nonnull | ||
| @Override | ||
| public RecordQueryPlannerConfiguration getConfiguration() { | ||
| return RecordQueryPlannerConfiguration.defaultPlannerConfiguration(); |
There was a problem hiding this comment.
defaultPlannerConfiguration returns builder().build() here - it is default PREFER_SCAN from record_planner_config.proto:28
however panner uses PREFER_INDEX as I understand.
Probably for configuration we need planContext.getRecordQueryPlannerConfiguration() here
There was a problem hiding this comment.
Good callout, the configuration is already part of the plan cache key, and it should be the one considered when comparing physical plans in the tertiary cache.
|
and I would try to sync somehow the rules lists in |
|
I did not get why it is |
| if (!(candidate instanceof QueryPlan.PhysicalQueryPlan)) { | ||
| return candidate; |
There was a problem hiding this comment.
Should this be returning acc? It would seem weird to return the candidate if it is not a physical query plan
There was a problem hiding this comment.
Good catch, this was supposed a final refactoring round from Claude which was over-done, incorrectly. I restored it to the previous version of the code, replacing the stable selector with the new plan-cache cost mode.
| // This currently can't be done without pulling the RecordQueryPlans out of the QueryPlan objects, | ||
| // running the cost model, and then re-associate it with the original Plan<?>, which is a bit | ||
| // costly. So, for now, just use the cost model as a comparator | ||
| stableSelectorCostModel.compare(candidateQueryPlan, bestQueryPlanSoFar) < 0) { |
There was a problem hiding this comment.
Is this supposed to be equivalent to the old code, but with the planCacheCostModel substituted for the stableSelectorCostModel, or is there another more subtle change going on?
There was a problem hiding this comment.
See my other comment, the refactoring was incorrect.
I pushed a new commit that uses the planner configuration stored in the secondary cache key to compare equivalent plans at the tertiary cache. I believe this should be now more accurate. BTW, preferring scan over index scan can be sometimes more efficient so it is not necessarily wrong.
I think we rarely add new costing rules, and so I wouldn't be worried much about divergence, they are already divergent because they work in different runtime contexts BTW, this is also ok(-ish); as we're not trying to replicate the planning cost model here, but rather come up with a good enough cost model for the plan cache that is better than a trivial hash-code comparator.
This is mostly for historical reasons; we keep the logic used exclusively by the plan generator in |
sergei-pustovykh
left a comment
There was a problem hiding this comment.
Clear improvement!
One fact worth to mention - the cost model only chooses among plans that are already in the cache. On an any cache hit planner does not run, so a potentially better plan that was never cached can not win. A plan's constraint says the plan is valid for the given values, not that it is the best one for them.
So cache poisoning is still possible. It just depends on the order of queries:
INDEX idx AS SELECT x FROM T WHERE x > 200 ORDER BY x
- x > 100: miss. The planner caches the unconstrained
SCAN | FILTER. - x > 300: hit on that scan, because it matches every value. The covering plan is never built, and every later query in this range gets the scan.
Two servers with different query histories can also serve different plans for the same query.
To close this gap we need a way to re-plan, for example re-planning on a hit from time to time, or when the cached plan has residual filters that a constrained plan could remove. That is out of scope for this PR.
Correct, that is known and well understood behavior, it is one of the reasons behind attaching a TTL for cache entries giving the system a chance for finding a different plan over time, should it stuck in a local optima. There were other proposals revolving around replanning at random times even if the cache is warm to overcome this problem, as you mentioned, it would be interesting to see how realistic this problem actually is in reality, this can be done for example by checking whether the imposed query constraints across multiple cached plans (over)fits a given distribution of query ranges correlating the distributions standard deviation with acceptable SLOs. We could think about doing this by introducing a monitoring "plugin" that examines the load and builds, over time, the aforementioned stats.
Correct, it is a different trajectory though. Unifying plan cache across multiple servers is something that was discussed few times in the past, there are few interesting ideas that are probably worth investigating once we draw a clear line between embedded vs. DBAAS which we don't have at the moment. |
📊 Metrics Diff Analysis ReportSummary
ℹ️ About this analysisThis automated analysis compares query planner metrics between the base branch and this PR. It categorizes changes into:
The last category in particular may indicate planner regressions that should be investigated. New QueriesCount of new queries by file:
|
Selection among the plans in one tertiary plan-cache entry went by smallest plan hash, so an unconstrained plan (e.g. a full scan) could poison the entry and keep being handed to queries that qualified for a better plan.
This replaces the hash-based
StableSelectorCostModelwithPlanCacheCostModel, composed from the tiebreakers introduced in #3993. It isPlanningCostModelminus every tiebreaker that reads a plan operator's match candidate.Composing outside the
costingpackage required makingTiebreaker,TiebreakerResult,PickRightTiebreakerand the candidate-free tiebreaker factories public.Fixes #4642.