diff --git a/src/graph/optimizer/rule/IndexScanRule.cpp b/src/graph/optimizer/rule/IndexScanRule.cpp index 5ad02a164f4..9287768f3af 100644 --- a/src/graph/optimizer/rule/IndexScanRule.cpp +++ b/src/graph/optimizer/rule/IndexScanRule.cpp @@ -224,11 +224,11 @@ inline bool verifyType(const Value& val) { case Value::Type::SET: case Value::Type::MAP: case Value::Type::DATASET: + case Value::Type::DURATION: case Value::Type::GEOGRAPHY: // TODO(jie) case Value::Type::PATH: { - DLOG(FATAL) << "Not supported value type " << val.type() << "for index."; return false; - } break; + } default: { return true; } diff --git a/src/graph/planner/match/LabelIndexSeek.cpp b/src/graph/planner/match/LabelIndexSeek.cpp index 5a2c66153e1..3733b1004a8 100644 --- a/src/graph/planner/match/LabelIndexSeek.cpp +++ b/src/graph/planner/match/LabelIndexSeek.cpp @@ -89,18 +89,20 @@ StatusOr LabelIndexSeek::transformNode(NodeContext* nodeCtx) { if (whereCtx && whereCtx->filter) { auto* filter = whereCtx->filter; const auto& nodeAlias = nodeCtx->info->alias; + const auto& schemaName = nodeCtx->scanInfo.schemaNames.back(); if (filter->kind() == Expression::Kind::kLogicalOr) { auto exprs = ExpressionUtils::collectAll(filter, {Expression::Kind::kLabelTagProperty}); - bool labelMatched = true; + bool matched = true; for (auto* expr : exprs) { auto tagPropExpr = static_cast(expr); - if (static_cast(tagPropExpr->label())->prop() != nodeAlias) { - labelMatched = false; + if (static_cast(tagPropExpr->label())->prop() != nodeAlias || + tagPropExpr->sym() != schemaName) { + matched = false; break; } } - if (labelMatched) { + if (matched) { auto flattenFilter = ExpressionUtils::flattenInnerLogicalExpr(filter); DCHECK_EQ(flattenFilter->kind(), Expression::Kind::kLogicalOr); auto& filterItems = static_cast(flattenFilter)->operands(); @@ -120,7 +122,6 @@ StatusOr LabelIndexSeek::transformNode(NodeContext* nodeCtx) { storage::cpp2::IndexQueryContext ctx; ctx.filter_ref() = Expression::encode(*flattenFilter); scan->setIndexQueryContext({ctx}); - whereCtx.reset(); } } } diff --git a/src/graph/planner/match/VertexIdSeek.cpp b/src/graph/planner/match/VertexIdSeek.cpp index 9d0df2a085d..4510fee81e8 100644 --- a/src/graph/planner/match/VertexIdSeek.cpp +++ b/src/graph/planner/match/VertexIdSeek.cpp @@ -42,6 +42,10 @@ bool VertexIdSeek::matchNode(NodeContext *nodeCtx) { if (vidResult.spec != VidExtractVisitor::VidPattern::Special::kInUsed) { return false; } + if (vidResult.nodes.size() > 1) { + // where id(v) == 'xxx' or id(t) == 'yyy' + return false; + } for (auto &nodeVid : vidResult.nodes) { if (nodeVid.second.kind == VidExtractVisitor::VidPattern::Vids::Kind::kIn) { if (nodeVid.first == node.alias) { diff --git a/src/graph/util/ExpressionUtils.cpp b/src/graph/util/ExpressionUtils.cpp index 88080ffdc6a..415532cddd2 100644 --- a/src/graph/util/ExpressionUtils.cpp +++ b/src/graph/util/ExpressionUtils.cpp @@ -570,32 +570,32 @@ Expression *ExpressionUtils::rewriteRelExprHelper(const Expression *expr, } StatusOr ExpressionUtils::filterTransform(const Expression *filter) { - // If the filter contains more than one different LabelAttribute expr, this filter cannot be - // pushed down - auto propExprs = ExpressionUtils::collectAll(filter, {Expression::Kind::kLabelTagProperty}); + // Check if any overflow happen before filter tranform + auto initialConstFold = foldConstantExpr(filter); + NG_RETURN_IF_ERROR(initialConstFold); + auto newFilter = initialConstFold.value(); + + // If the filter contains more than one different Label expr, this filter cannot be + // pushed down, such as where v1.player.name == 'xxx' or v2.player.age == 20 + auto propExprs = ExpressionUtils::collectAll(newFilter, {Expression::Kind::kLabel}); // Deduplicate the list std::unordered_set dedupPropExprSet; for (auto &iter : propExprs) { dedupPropExprSet.emplace(iter->toString()); } if (dedupPropExprSet.size() > 1) { - return const_cast(filter); + return const_cast(newFilter); } - // Check if any overflow happen before filter tranform - auto initialConstFold = foldConstantExpr(filter); - NG_RETURN_IF_ERROR(initialConstFold); - // Rewrite relational expression - auto rewrittenExpr = initialConstFold.value(); - rewrittenExpr = rewriteRelExpr(rewrittenExpr); + auto rewrittenExpr = rewriteRelExpr(newFilter->clone()); // Fold constant expression auto constantFoldRes = foldConstantExpr(rewrittenExpr); // If errors like overflow happened during the constant fold, stop transferming and return the // original expression if (!constantFoldRes.ok()) { - return const_cast(filter); + return const_cast(newFilter); } rewrittenExpr = constantFoldRes.value(); diff --git a/src/graph/visitor/VidExtractVisitor.cpp b/src/graph/visitor/VidExtractVisitor.cpp index 75dee22a026..5cfaee38518 100644 --- a/src/graph/visitor/VidExtractVisitor.cpp +++ b/src/graph/visitor/VidExtractVisitor.cpp @@ -85,7 +85,6 @@ void VidExtractVisitor::visit(ConstantExpression *expr) { void VidExtractVisitor::visit(UnaryExpression *expr) { if (expr->kind() == Expression::Kind::kUnaryNot) { - // const auto *expr = static_cast(expr); expr->operand()->accept(this); auto operandResult = moveVidPattern(); if (operandResult.spec == VidPattern::Special::kInUsed) { @@ -119,14 +118,15 @@ void VidExtractVisitor::visit(LabelExpression *expr) { } void VidExtractVisitor::visit(LabelAttributeExpression *expr) { - if (expr->kind() == Expression::Kind::kLabelAttribute) { - const auto *labelExpr = static_cast(expr); - vidPattern_ = - VidPattern{VidPattern::Special::kInUsed, - {{labelExpr->left()->toString(), {VidPattern::Vids::Kind::kOtherSource, {}}}}}; - } else { - vidPattern_ = VidPattern{}; - } + const auto &label = expr->left()->toString(); + vidPattern_ = VidPattern{VidPattern::Special::kInUsed, + {{label, {VidPattern::Vids::Kind::kOtherSource, {}}}}}; +} + +void VidExtractVisitor::visit(LabelTagPropertyExpression *expr) { + const auto &label = static_cast(expr->label())->prop(); + vidPattern_ = VidPattern{VidPattern::Special::kInUsed, + {{label, {VidPattern::Vids::Kind::kOtherSource, {}}}}}; } void VidExtractVisitor::visit(ArithmeticExpression *expr) { @@ -144,7 +144,13 @@ void VidExtractVisitor::visit(RelationalExpression *expr) { {{label, {VidPattern::Vids::Kind::kOtherSource, {}}}}}; return; } - + if (expr->left()->kind() == Expression::Kind::kLabelTagProperty) { + const auto *tagPropExpr = static_cast(expr->left()); + const auto &label = static_cast(tagPropExpr->label())->prop(); + vidPattern_ = VidPattern{VidPattern::Special::kInUsed, + {{label, {VidPattern::Vids::Kind::kOtherSource, {}}}}}; + return; + } if (expr->left()->kind() != Expression::Kind::kFunctionCall || expr->right()->kind() != Expression::Kind::kList || !ExpressionUtils::isEvaluableExpr(expr->right())) { @@ -165,7 +171,6 @@ void VidExtractVisitor::visit(RelationalExpression *expr) { VidPattern{VidPattern::Special::kInUsed, {{fCallExpr->args()->args().front()->toString(), {VidPattern::Vids::Kind::kIn, listExpr->eval(ctx(nullptr)).getList()}}}}; - return; } else if (expr->kind() == Expression::Kind::kRelEQ) { // id(V) == vid if (expr->left()->kind() == Expression::Kind::kLabelAttribute) { @@ -175,6 +180,13 @@ void VidExtractVisitor::visit(RelationalExpression *expr) { {{label, {VidPattern::Vids::Kind::kOtherSource, {}}}}}; return; } + if (expr->left()->kind() == Expression::Kind::kLabelTagProperty) { + const auto *tagPropExpr = static_cast(expr->left()); + const auto &label = static_cast(tagPropExpr->label())->prop(); + vidPattern_ = VidPattern{VidPattern::Special::kInUsed, + {{label, {VidPattern::Vids::Kind::kOtherSource, {}}}}}; + return; + } if (expr->left()->kind() != Expression::Kind::kFunctionCall || expr->right()->kind() != Expression::Kind::kConstant) { vidPattern_ = VidPattern{}; @@ -194,10 +206,13 @@ void VidExtractVisitor::visit(RelationalExpression *expr) { vidPattern_ = VidPattern{VidPattern::Special::kInUsed, {{fCallExpr->args()->args().front()->toString(), {VidPattern::Vids::Kind::kIn, List({constExpr->value()})}}}}; - return; } else { - vidPattern_ = VidPattern{}; - return; + if (ExpressionUtils::isPropertyExpr(expr->left())) { + vidPattern_ = VidPattern{VidPattern::Special::kInUsed, + {{"", {VidPattern::Vids::Kind::kOtherSource, {}}}}}; + } else { + vidPattern_ = VidPattern{}; + } } } @@ -213,11 +228,9 @@ void VidExtractVisitor::visit(AttributeExpression *expr) { void VidExtractVisitor::visit(LogicalExpression *expr) { if (expr->kind() == Expression::Kind::kLogicalAnd) { - // const auto *expr = static_cast(expr); std::vector operandsResult; operandsResult.reserve(expr->operands().size()); for (const auto &operand : expr->operands()) { - // operandsResult.emplace_back(reverseEvalVids(operand.get())); operand->accept(this); operandsResult.emplace_back(moveVidPattern()); } @@ -273,8 +286,6 @@ void VidExtractVisitor::visit(LogicalExpression *expr) { vidPattern_ = std::move(inResult); return; } else if (expr->kind() == Expression::Kind::kLogicalOr) { - // const auto *andExpr = static_cast(expr); std::vector operandsResult; operandsResult.reserve(expr->operands().size()); for (const auto &operand : expr->operands()) { @@ -351,11 +362,6 @@ void VidExtractVisitor::visit(MapExpression *expr) { } // property Expression -void VidExtractVisitor::visit(LabelTagPropertyExpression *expr) { - UNUSED(expr); - vidPattern_ = VidPattern{}; -} - void VidExtractVisitor::visit(TagPropertyExpression *expr) { UNUSED(expr); vidPattern_ = VidPattern{}; diff --git a/src/graph/visitor/test/FilterTransformTest.cpp b/src/graph/visitor/test/FilterTransformTest.cpp index 8b756ea251f..733e50ba3b2 100644 --- a/src/graph/visitor/test/FilterTransformTest.cpp +++ b/src/graph/visitor/test/FilterTransformTest.cpp @@ -32,7 +32,8 @@ TEST_F(FilterTransformTest, TestCalculationOverflow) { auto res = ExpressionUtils::filterTransform(expr); ASSERT(res.ok()); auto expected = expr; - ASSERT_EQ(res.value(), expected) << res.value() << " vs. " << expected->toString(); + ASSERT_EQ(res.value()->toString(), expected->toString()) + << res.value()->toString() << " vs. " << expected->toString(); } // (v.age + 1 < -9223372036854775808) => unchanged { @@ -40,7 +41,8 @@ TEST_F(FilterTransformTest, TestCalculationOverflow) { auto res = ExpressionUtils::filterTransform(expr); ASSERT(res.ok()); auto expected = expr; - ASSERT_EQ(res.value(), expected) << res.value() << " vs. " << expected->toString(); + ASSERT_EQ(res.value()->toString(), expected->toString()) + << res.value()->toString() << " vs. " << expected->toString(); } // (v.age - 1 < 9223372036854775807 + 1) => overflow { @@ -71,7 +73,8 @@ TEST_F(FilterTransformTest, TestCalculationOverflow) { auto res = ExpressionUtils::filterTransform(expr); ASSERT(res.ok()); auto expected = expr; - ASSERT_EQ(res.value(), expected) << res.value()->toString() << " vs. " << expected->toString(); + ASSERT_EQ(res.value()->toString(), expected->toString()) + << res.value()->toString() << " vs. " << expected->toString(); } // !!!(v.age + 1 < -9223372036854775808) => unchanged { @@ -80,12 +83,13 @@ TEST_F(FilterTransformTest, TestCalculationOverflow) { auto res = ExpressionUtils::filterTransform(expr); ASSERT(res.ok()); auto expected = expr; - ASSERT_EQ(res.value(), expected) << res.value()->toString() << " vs. " << expected->toString(); + ASSERT_EQ(res.value()->toString(), expected->toString()) + << res.value()->toString() << " vs. " << expected->toString(); } } TEST_F(FilterTransformTest, TestNoRewrite) { - // Do not rewrite if the filter contains more than one different LabelAttribute expr + // Do not rewrite if the filter contains more than one different Label expr { // (v.age - 1 < v2.age + 2) => Unchanged auto expr = ltExpr(minusExpr(laExpr("v", "age"), constantExpr(1)), diff --git a/src/storage/exec/IndexScanNode.h b/src/storage/exec/IndexScanNode.h index d0df49c5141..f7dc2e13595 100644 --- a/src/storage/exec/IndexScanNode.h +++ b/src/storage/exec/IndexScanNode.h @@ -282,7 +282,7 @@ class QualifiedStrategy { * [start,end) which is generated by `RangeIndex`. Therefore, it is necessary to make additional * judgment on the truncated string type index * For example: - * (ab)c meas that string is "abc" but index val has been truncated to "ab". (ab)c > ab is + * (ab)c means that string is "abc" but index val has been truncated to "ab". (ab)c > ab is * `UNCERTAIN`, and (ab)c > aa is COMPATIBLE. * * Args: diff --git a/tests/tck/features/match/SeekById.feature b/tests/tck/features/match/SeekById.feature index 0c050e3f991..5ac4839f7ac 100644 --- a/tests/tck/features/match/SeekById.feature +++ b/tests/tck/features/match/SeekById.feature @@ -208,12 +208,23 @@ Feature: Match seek by id RETURN v.player.name AS Name, t.team.name AS Team """ Then the result should be, in any order: - | Name | Team | - | 'Paul Gasol' | 'Grizzlies' | - | 'Paul Gasol' | 'Lakers' | - | 'Paul Gasol' | 'Bulls' | - | 'Paul Gasol' | 'Spurs' | - | 'Paul Gasol' | 'Bucks' | + | Name | Team | + | "Paul Gasol" | "Bucks" | + | "Paul Gasol" | "Bulls" | + | "Rudy Gay" | "Grizzlies" | + | "Kyle Anderson" | "Grizzlies" | + | "Paul Gasol" | "Grizzlies" | + | "Marc Gasol" | "Grizzlies" | + | "Vince Carter" | "Grizzlies" | + | "Paul Gasol" | "Spurs" | + | "Dwight Howard" | "Lakers" | + | "Shaquille O'Neal" | "Lakers" | + | "Steve Nash" | "Lakers" | + | "Paul Gasol" | "Lakers" | + | "Kobe Bryant" | "Lakers" | + | "JaVale McGee" | "Lakers" | + | "Rajon Rondo" | "Lakers" | + | "LeBron James" | "Lakers" | Scenario: can't refer When executing query: @@ -252,8 +263,7 @@ Feature: Match seek by id """ Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. - @skip - Scenario: test OR logic (reason = "or logic optimization error") + Scenario: test OR logic When executing query: """ MATCH (v) @@ -261,13 +271,7 @@ Feature: Match seek by id OR v.player.age == 23 RETURN v.player.name AS Name """ - Then the result should be, in any order: - | Name | - | 'James Harden' | - | 'Jonathon Simmons' | - | 'Klay Thompson' | - | 'Dejounte Murray' | - | 'Kristaps Porzingis' | + Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. When executing query: """ MATCH (v) @@ -275,10 +279,7 @@ Feature: Match seek by id OR v.player.age == 23 RETURN v.player.name AS Name """ - Then the result should be, in any order: - | Name | - | 'James Harden' | - | 'Kristaps Porzingis' | + Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. When executing query: """ MATCH (v) @@ -286,8 +287,71 @@ Feature: Match seek by id OR v.player.age != 23 RETURN v.player.name AS Name """ + Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. + When executing query: + """ + MATCH (v:player) + WHERE v.player.name == "Tim Duncan" + OR v.player.age == 23 + RETURN v + """ Then the result should be, in any order: - | Name | + | v | + | ("Kristaps Porzingis" :player{age: 23, name: "Kristaps Porzingis"}) | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | + When executing query: + """ + MATCH (v:player) + WHERE v.player.name == "Tim Duncan" + OR v.noexist.age == 23 + RETURN v + """ + Then the result should be, in any order: + | v | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | + When executing query: + """ + MATCH (v:player) + WHERE v.player.noexist == "Tim Duncan" + OR v.player.age == 23 + RETURN v + """ + Then the result should be, in any order: + | v | + | ("Kristaps Porzingis" :player{age: 23, name: "Kristaps Porzingis"}) | + When executing query: + """ + MATCH (v:player) + WHERE v.player.noexist == "Tim Duncan" + OR v.noexist.age == 23 + RETURN v + """ + Then the result should be, in any order: + | v | + When executing query: + """ + MATCH (v:player) + WHERE "Tim Duncan" == v.player.name + OR 23 + 1 == v.noexist.age - 3 + RETURN v + """ + Then the result should be, in any order: + | v | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | + When executing query: + """ + MATCH (v) + WHERE id(v) IN ['James Harden', 'Jonathon Simmons', 'Klay Thompson', 'Dejounte Murray'] + OR id(v) == 'Yao Ming' + RETURN v + """ + Then the result should be, in any order: + | v | + | ("James Harden" :player{age: 29, name: "James Harden"}) | + | ("Jonathon Simmons" :player{age: 29, name: "Jonathon Simmons"}) | + | ("Klay Thompson" :player{age: 29, name: "Klay Thompson"}) | + | ("Dejounte Murray" :player{age: 29, name: "Dejounte Murray"}) | + | ("Yao Ming" :player{age: 38, name: "Yao Ming"}) | Scenario: Start from end When executing query: diff --git a/tests/tck/features/match/SeekById.intVid.feature b/tests/tck/features/match/SeekById.intVid.feature index 70fec05da7b..e693b3b5dfe 100644 --- a/tests/tck/features/match/SeekById.intVid.feature +++ b/tests/tck/features/match/SeekById.intVid.feature @@ -208,12 +208,23 @@ Feature: Match seek by id RETURN v.player.name AS Name, t.team.name AS Team """ Then the result should be, in any order: - | Name | Team | - | 'Paul Gasol' | 'Grizzlies' | - | 'Paul Gasol' | 'Lakers' | - | 'Paul Gasol' | 'Bulls' | - | 'Paul Gasol' | 'Spurs' | - | 'Paul Gasol' | 'Bucks' | + | Name | Team | + | "Paul Gasol" | "Bucks" | + | "Paul Gasol" | "Bulls" | + | "Rudy Gay" | "Grizzlies" | + | "Kyle Anderson" | "Grizzlies" | + | "Paul Gasol" | "Grizzlies" | + | "Marc Gasol" | "Grizzlies" | + | "Vince Carter" | "Grizzlies" | + | "Paul Gasol" | "Spurs" | + | "Dwight Howard" | "Lakers" | + | "Shaquille O'Neal" | "Lakers" | + | "Steve Nash" | "Lakers" | + | "Paul Gasol" | "Lakers" | + | "Kobe Bryant" | "Lakers" | + | "JaVale McGee" | "Lakers" | + | "Rajon Rondo" | "Lakers" | + | "LeBron James" | "Lakers" | Scenario: can't refer When executing query: @@ -245,8 +256,7 @@ Feature: Match seek by id """ Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. - @skip - Scenario: test OR logic (reason = "or logic optimization error") + Scenario: test OR logic When executing query: """ MATCH (v) @@ -254,13 +264,7 @@ Feature: Match seek by id OR v.player.age == 23 RETURN v.player.name AS Name """ - Then the result should be, in any order: - | Name | - | 'James Harden' | - | 'Jonathon Simmons' | - | 'Klay Thompson' | - | 'Dejounte Murray' | - | 'Kristaps Porzingis' | + Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. When executing query: """ MATCH (v) @@ -268,10 +272,7 @@ Feature: Match seek by id OR v.player.age == 23 RETURN v.player.name AS Name """ - Then the result should be, in any order: - | Name | - | 'James Harden' | - | 'Kristaps Porzingis' | + Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. When executing query: """ MATCH (v) @@ -279,8 +280,71 @@ Feature: Match seek by id OR v.player.age != 23 RETURN v.player.name AS Name """ + Then a ExecutionError should be raised at runtime: Scan vertices or edges need to specify a limit number, or limit number can not push down. + When executing query: + """ + MATCH (v:player) + WHERE v.player.name == "Tim Duncan" + OR v.player.age == 23 + RETURN v.player.name as name + """ Then the result should be, in any order: - | Name | + | name | + | "Kristaps Porzingis" | + | "Tim Duncan" | + When executing query: + """ + MATCH (v:player) + WHERE v.player.name == "Tim Duncan" + OR v.noexist.age == 23 + RETURN v.player.name as name + """ + Then the result should be, in any order: + | name | + | "Tim Duncan" | + When executing query: + """ + MATCH (v:player) + WHERE v.player.noexist == "Tim Duncan" + OR v.player.age == 23 + RETURN v.player.name as name + """ + Then the result should be, in any order: + | name | + | "Kristaps Porzingis" | + When executing query: + """ + MATCH (v:player) + WHERE v.player.noexist == "Tim Duncan" + OR v.noexist.age == 23 + RETURN v + """ + Then the result should be, in any order: + | v | + When executing query: + """ + MATCH (v:player) + WHERE "Tim Duncan" == v.player.name + OR 23 + 1 == v.noexist.age - 3 + RETURN v.player.name as name + """ + Then the result should be, in any order: + | name | + | "Tim Duncan" | + When executing query: + """ + MATCH (v) + WHERE id(v) IN [hash('James Harden'), hash('Jonathon Simmons'), hash('Klay Thompson'), hash('Dejounte Murray')] + OR id(v) == hash('Yao Ming') + RETURN v.player.name as name + """ + Then the result should be, in any order: + | name | + | "James Harden" | + | "Jonathon Simmons" | + | "Klay Thompson" | + | "Dejounte Murray" | + | "Yao Ming" | Scenario: with arithmetic When executing query: diff --git a/tests/tck/features/optimizer/IndexScanRule.feature b/tests/tck/features/optimizer/IndexScanRule.feature index 34a19251298..906d774ef57 100644 --- a/tests/tck/features/optimizer/IndexScanRule.feature +++ b/tests/tck/features/optimizer/IndexScanRule.feature @@ -93,9 +93,10 @@ Feature: Match index selection And the execution plan should be: | id | name | dependencies | operator info | | 6 | Project | 2 | | - | 2 | AppendVertices | 5 | | - | 5 | IndexScan | 0 | | - | 0 | Start | | | + | 9 | Filter | 3 | | + | 3 | AppendVertices | 7 | | + | 7 | IndexScan | 2 | | + | 2 | Start | | | Scenario: degenerate to full tag scan When profiling query: