Skip to content

Commit 99ff1fa

Browse files
authored
[BTS-2268] Fix query plan caching with subqueries (#22146)
* Invalidate cost in splice-subqueries * Fix changelog * Always recalculate cost estimates for plan in optimizer * Address review comments
1 parent c7d556f commit 99ff1fa

File tree

7 files changed

+37
-30
lines changed

7 files changed

+37
-30
lines changed

CHANGELOG

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
devel
22
-----
33

4+
* Fix BTS-2268: Fixed an issue where query plan for queries with subquery
5+
is causing a crash when being stored in query plan cache. The issue
6+
is a missing invalidateCost from the splice-subqueries rule.
7+
48
* FE-905: Update tooltip text for maxSkewThreshold & minDeletionRatio.
59

610
* COR-45: Added support for index hints in vector search queries.

arangod/Aql/Optimizer.cpp

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -277,8 +277,7 @@ void Optimizer::initializeRules(ExecutionPlan* plan,
277277

278278
// @brief the actual optimization
279279
void Optimizer::createPlans(std::unique_ptr<ExecutionPlan> plan,
280-
QueryOptions const& queryOptions,
281-
bool estimateAllPlans) {
280+
QueryOptions const& queryOptions) {
282281
_runOnlyRequiredRules = false;
283282
ExecutionPlan* initialPlan = plan.get();
284283

@@ -416,7 +415,7 @@ void Optimizer::createPlans(std::unique_ptr<ExecutionPlan> plan,
416415

417416
finalizePlans();
418417

419-
estimateCosts(queryOptions, estimateAllPlans);
418+
estimateCosts(queryOptions);
420419

421420
// Best plan should not have forced hints left.
422421
// note: this method will throw in case the plan still contains
@@ -467,29 +466,18 @@ void Optimizer::checkForcedIndexHints() {
467466
TRI_ASSERT(!_plans.list.empty());
468467
}
469468

470-
void Optimizer::estimateCosts(QueryOptions const& queryOptions,
471-
bool estimateAllPlans) {
472-
if (estimateAllPlans || _plans.size() > 1 ||
473-
queryOptions.profile >= ProfileLevel::Blocks) {
474-
// if profiling is turned on, we must do the cost estimation here
475-
// because the cost estimation must be done while the transaction
476-
// is still running
477-
for (auto& plan : _plans.list) {
478-
plan.first->invalidateCost();
479-
plan.first->getCost();
480-
// this value is cached in the plan, so formally this step is
481-
// unnecessary, but for the sake of cleanliness...
482-
}
483-
484-
if (_plans.size() > 1) {
485-
// only sort plans when necessary
486-
std::sort(_plans.list.begin(), _plans.list.end(),
487-
[](PlanList::Entry const& a, PlanList::Entry const& b) -> bool {
488-
return a.first->getCost().estimatedCost <
489-
b.first->getCost().estimatedCost;
490-
});
491-
}
469+
void Optimizer::estimateCosts(QueryOptions const& queryOptions) {
470+
// Invalidate all plans and get new estimates before doing anything
471+
for (auto& plan : _plans.list) {
472+
plan.first->invalidateCost();
473+
plan.first->getCost();
492474
}
475+
476+
std::sort(_plans.list.begin(), _plans.list.end(),
477+
[](PlanList::Entry const& a, PlanList::Entry const& b) -> bool {
478+
return a.first->getCost().estimatedCost <
479+
b.first->getCost().estimatedCost;
480+
});
493481
}
494482

495483
void Optimizer::disableRule(ExecutionPlan* plan, int level) {

arangod/Aql/Optimizer.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class Optimizer {
113113
/// If you need to extract the plans from the optimizer use stealBest or
114114
/// stealPlans.
115115
void createPlans(std::unique_ptr<ExecutionPlan> p,
116-
QueryOptions const& queryOptions, bool estimateAllPlans);
116+
QueryOptions const& queryOptions);
117117

118118
/// @brief add a plan to the optimizer
119119
void addPlan(std::unique_ptr<ExecutionPlan>, OptimizerRule const&,
@@ -173,7 +173,7 @@ class Optimizer {
173173

174174
void checkForcedIndexHints();
175175

176-
void estimateCosts(QueryOptions const& queryOptions, bool estimateAllPlans);
176+
void estimateCosts(QueryOptions const& queryOptions);
177177

178178
/// @brief optimizer statistics
179179
Stats _stats;

arangod/Aql/OptimizerRules.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8854,6 +8854,9 @@ void arangodb::aql::spliceSubqueriesRule(Optimizer* opt,
88548854
TRI_ASSERT(sq->getParents().empty());
88558855
}
88568856

8857+
if (modified) {
8858+
plan->root()->invalidateCost();
8859+
}
88578860
opt->addPlan(std::move(plan), rule, modified);
88588861
}
88598862

arangod/Aql/Query.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ std::unique_ptr<ExecutionPlan> Query::preparePlan() {
691691
enterState(QueryExecutionState::ValueType::PLAN_OPTIMIZATION);
692692
Optimizer opt(*_resourceMonitor, _queryOptions.maxNumberOfPlans);
693693
// get enabled/disabled rules
694-
opt.createPlans(std::move(plan), _queryOptions, false);
694+
opt.createPlans(std::move(plan), _queryOptions);
695695
// Now plan and all derived plans belong to the optimizer
696696
plan = opt.stealBest(); // Now we own the best one again
697697

@@ -1473,7 +1473,7 @@ QueryResult Query::explain() {
14731473
enterState(QueryExecutionState::ValueType::PLAN_OPTIMIZATION);
14741474
Optimizer opt(*_resourceMonitor, _queryOptions.maxNumberOfPlans);
14751475
// get enabled/disabled rules
1476-
opt.createPlans(std::move(plan), _queryOptions, true);
1476+
opt.createPlans(std::move(plan), _queryOptions);
14771477

14781478
enterState(QueryExecutionState::ValueType::FINALIZATION);
14791479

arangod/IResearch/IResearchAqlAnalyzer.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ bool AqlAnalyzer::reset(std::string_view field) noexcept {
499499
optimizer.disableRules(plan.get(), [](OptimizerRule const& rule) -> bool {
500500
return rule.canBeDisabled() || rule.isClusterOnly();
501501
});
502-
optimizer.createPlans(std::move(plan), _query->queryOptions(), false);
502+
optimizer.createPlans(std::move(plan), _query->queryOptions());
503503

504504
_plan = optimizer.stealBest();
505505
TRI_ASSERT(

tests/js/client/aql/aql-query-plan-cache.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,18 @@ function QueryPlanCacheTestSuite () {
697697
assertEqual(1, entries.filter(buildFilter({ query: query2, bindVars: {"@collection": cn2}, fullCount: false, dataSources: [cn2] })).length, entries);
698698
assertEqual(1, entries.filter(buildFilter({ query: query3, bindVars: {}, fullCount: true, dataSources: [cn2] })).length, entries);
699699
},
700+
701+
// Fixes BTS-2268
702+
testPlanCacheEstimatesCrash : function () {
703+
const query = `${uniqid()} FOR doc IN ${cn1} LET var = (FOR i IN 0..10 FILTER i == 2 RETURN i) FILTER doc.value1 IN var RETURN doc.value`;
704+
const options = { optimizePlanForCaching: true, usePlanCache: true };
705+
706+
let res = db._query(query, {}, options);
707+
assertFalse(res.hasOwnProperty("planCacheKey"));
708+
709+
res = db._query(query, {}, options);
710+
assertTrue(res.hasOwnProperty("planCacheKey"));
711+
},
700712
};
701713
}
702714

0 commit comments

Comments
 (0)