diff --git a/arangod/RocksDBEngine/RocksDBOptimizerRules.cpp b/arangod/RocksDBEngine/RocksDBOptimizerRules.cpp index ba863152e0..6856db3d88 100644 --- a/arangod/RocksDBEngine/RocksDBOptimizerRules.cpp +++ b/arangod/RocksDBEngine/RocksDBOptimizerRules.cpp @@ -28,6 +28,7 @@ #include "Aql/ExecutionNode.h" #include "Aql/ExecutionPlan.h" #include "Aql/Function.h" +#include "Aql/IndexHint.h" #include "Aql/IndexNode.h" #include "Aql/ModificationNodes.h" #include "Aql/Optimizer.h" @@ -193,6 +194,7 @@ void RocksDBOptimizerRules::reduceExtractionToProjectionRule( // we must never have a projection on _id, as producing _id is not supported yet // by the primary index iterator EnumerateCollectionNode const* en = ExecutionNode::castTo(n); + auto const& hint = en->hint(); // now check all indexes if they cover the projection auto trx = plan->getAst()->query()->trx(); @@ -202,23 +204,39 @@ void RocksDBOptimizerRules::reduceExtractionToProjectionRule( indexes = en->collection()->getCollection()->getIndexes(); } - for (auto const& idx : indexes) { + auto selectIndexIfPossible = [&picked, &attributes](std::shared_ptr const& idx) -> bool { if (!idx->hasCoveringIterator() || !idx->covers(attributes)) { // index doesn't cover the projection - continue; + return false; } if (idx->type() != arangodb::Index::IndexType::TRI_IDX_TYPE_PRIMARY_INDEX && idx->type() != arangodb::Index::IndexType::TRI_IDX_TYPE_HASH_INDEX && idx->type() != arangodb::Index::IndexType::TRI_IDX_TYPE_SKIPLIST_INDEX && idx->type() != arangodb::Index::IndexType::TRI_IDX_TYPE_PERSISTENT_INDEX) { // only the above index types are supported - continue; + return false; } - if (picked == nullptr || - picked->fields().size() > idx->fields().size()) { - // found an index that would cover the projection - picked = idx; + picked = idx; + return true; + }; + + bool forced = false; + if (hint.type() == aql::IndexHint::HintType::Simple) { + forced = hint.isForced(); + for (std::string const& hinted : hint.hint()) { + auto idx = en->collection()->getCollection()->lookupIndex(hinted); + if (idx && selectIndexIfPossible(idx)) { + break; + } + } + } + + if (!picked && !forced) { + for (auto const& idx : indexes) { + if (selectIndexIfPossible(idx)) { + break; + } } } @@ -269,6 +287,7 @@ void RocksDBOptimizerRules::reduceExtractionToProjectionRule( // primary index colum family. thus in disk-bound workloads scanning the // documents via the primary index should be faster EnumerateCollectionNode* en = ExecutionNode::castTo(n); + auto const& hint = en->hint(); auto trx = plan->getAst()->query()->trx(); std::shared_ptr picked; @@ -277,10 +296,30 @@ void RocksDBOptimizerRules::reduceExtractionToProjectionRule( indexes = en->collection()->getCollection()->getIndexes(); } - for (auto const& idx : indexes) { + auto selectIndexIfPossible = [&picked](std::shared_ptr const& idx) -> bool { if (idx->type() == arangodb::Index::IndexType::TRI_IDX_TYPE_PRIMARY_INDEX) { picked = idx; - break; + return true; + } + return false; + }; + + bool forced = false; + if (hint.type() == aql::IndexHint::HintType::Simple) { + forced = hint.isForced(); + for (std::string const& hinted : hint.hint()) { + auto idx = en->collection()->getCollection()->lookupIndex(hinted); + if (idx && selectIndexIfPossible(idx)) { + break; + } + } + } + + if (!picked && !forced) { + for (auto const& idx : indexes) { + if (selectIndexIfPossible(idx)) { + break; + } } } diff --git a/tests/js/server/aql/aql-optimizer-rule-reduce-extraction-to-projection-rocksdb.js b/tests/js/server/aql/aql-optimizer-rule-reduce-extraction-to-projection-rocksdb.js index 38bf4ef0b2..ce7f5af91f 100644 --- a/tests/js/server/aql/aql-optimizer-rule-reduce-extraction-to-projection-rocksdb.js +++ b/tests/js/server/aql/aql-optimizer-rule-reduce-extraction-to-projection-rocksdb.js @@ -84,9 +84,32 @@ function optimizerRuleTestSuite () { assertEqual(-1, result.plan.rules.indexOf(ruleName), query); }); }, + + testNotActiveBecauseIndexHint : function () { + // these queries may actually use projections, but they must not use the primary + // index for scanning + var queries = [ + "FOR doc IN @@cn OPTIONS { indexHint: 'aha', forceIndexHint: true } RETURN 1", + "FOR doc IN @@cn OPTIONS { indexHint: 'aha', forceIndexHint: true } RETURN doc", + "FOR doc IN @@cn OPTIONS { indexHint: 'aha', forceIndexHint: true } RETURN doc.value1", + "FOR doc IN @@cn OPTIONS { indexHint: 'aha', forceIndexHint: true } RETURN doc.value2", + "FOR doc IN @@cn OPTIONS { indexHint: 'aha', forceIndexHint: true } RETURN doc._key", + "FOR doc IN @@cn OPTIONS { indexHint: 'primary' } RETURN doc", + "FOR doc IN @@cn OPTIONS { indexHint: 'primary' } RETURN doc.value1", + "FOR doc IN @@cn OPTIONS { indexHint: 'primary' } RETURN doc.value2", + ]; + + queries.forEach(function(query) { + var result = AQL_EXPLAIN(query, { "@cn" : cn }).plan; + let nodeTypes = result.nodes.map(function(node) { return node.type; }); + assertEqual(-1, nodeTypes.indexOf("IndexNode")); + }); + }, testActive : function () { var queries = [ + "FOR doc IN @@cn OPTIONS { indexHint: 'primary' } RETURN 1", + "FOR doc IN @@cn OPTIONS { indexHint: 'primary' } RETURN doc._key", "FOR doc IN @@cn FILTER doc.value1 == 1 RETURN doc.value1", "FOR doc IN @@cn FILTER doc.value1 == 1 RETURN doc.value2", "FOR doc IN @@cn FILTER doc.value1 == 1 && doc.value2 == 1 && doc.value3 == 1 RETURN doc.value1", @@ -120,6 +143,18 @@ function optimizerRuleTestSuite () { }); }, + testActiveScanOnly : function () { + var queries = [ + "FOR doc IN @@cn RETURN 1", + "FOR doc IN @@cn OPTIONS { indexHint: 'primary' } RETURN 1", + ]; + + queries.forEach(function(query) { + var result = AQL_EXPLAIN(query, { "@cn" : cn }); + assertNotEqual(-1, result.plan.rules.indexOf(ruleName), query); + }); + }, + testActiveWithIndex : function () { c.ensureIndex({ type: "skiplist", fields: ["value1"] });