From 7c011287909d811c2ab55c962dfac4b338dd0aed Mon Sep 17 00:00:00 2001 From: "kyle.cao" Date: Fri, 7 Apr 2023 09:34:53 +0800 Subject: [PATCH] Split optimizer rules (#5470) Fix compile small rename Fix tck Fix tck fmt Fix tck Fix tck --- src/common/expression/Expression.h | 4 + src/common/expression/PropertyExpression.h | 4 + src/graph/optimizer/CMakeLists.txt | 1 + .../rule/PushFilterDownTraverseRule.cpp | 93 ++++++------- .../PushFilterThroughAppendVerticesRule.cpp | 123 ++++++++++++++++++ .../PushFilterThroughAppendVerticesRule.h | 46 +++++++ .../bugfix/LackFilterGetEdges.feature | 19 +++ tests/tck/features/match/SeekById.feature | 1 + .../optimizer/CollapseProjectRule.feature | 2 + .../optimizer/PrunePropertiesRule.feature | 1 + .../optimizer/PushFilterDownBugFixes.feature | 2 + .../PushFilterDownCrossJoinRule.feature | 25 ++-- .../PushFilterDownTraverseRule.feature | 4 +- .../PushLimitDownProjectRule.feature | 1 + .../RemoveAppendVerticesBelowJoinRule.feature | 4 +- 15 files changed, 259 insertions(+), 71 deletions(-) create mode 100644 src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.cpp create mode 100644 src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.h diff --git a/src/common/expression/Expression.h b/src/common/expression/Expression.h index a23340589fe..31b4dcfac73 100644 --- a/src/common/expression/Expression.h +++ b/src/common/expression/Expression.h @@ -167,6 +167,10 @@ class Expression { return false; } + virtual bool isPropertyExpr() const { + return false; + } + virtual bool isContainerExpr() const { return false; } diff --git a/src/common/expression/PropertyExpression.h b/src/common/expression/PropertyExpression.h index b2cf81b7343..ab267bd184f 100644 --- a/src/common/expression/PropertyExpression.h +++ b/src/common/expression/PropertyExpression.h @@ -32,6 +32,10 @@ class PropertyExpression : public Expression { public: bool operator==(const Expression& rhs) const override; + bool isPropertyExpr() const override { + return true; + } + const Value& eval(ExpressionContext& ctx) override; const std::string& ref() const { diff --git a/src/graph/optimizer/CMakeLists.txt b/src/graph/optimizer/CMakeLists.txt index 2d98c2c1a14..bd22e5204ed 100644 --- a/src/graph/optimizer/CMakeLists.txt +++ b/src/graph/optimizer/CMakeLists.txt @@ -59,6 +59,7 @@ nebula_add_library( rule/PushLimitDownScanEdgesRule.cpp rule/PushFilterDownTraverseRule.cpp rule/RemoveAppendVerticesBelowJoinRule.cpp + rule/PushFilterThroughAppendVerticesRule.cpp ) nebula_add_subdirectory(test) diff --git a/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp b/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp index 6c9b507763c..60dc074a414 100644 --- a/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp +++ b/src/graph/optimizer/rule/PushFilterDownTraverseRule.cpp @@ -32,9 +32,7 @@ PushFilterDownTraverseRule::PushFilterDownTraverseRule() { const Pattern& PushFilterDownTraverseRule::pattern() const { static Pattern pattern = - Pattern::create(PlanNode::Kind::kFilter, - {Pattern::create(PlanNode::Kind::kAppendVertices, - {Pattern::create(PlanNode::Kind::kTraverse)})}); + Pattern::create(PlanNode::Kind::kFilter, {Pattern::create(PlanNode::Kind::kTraverse)}); return pattern; } @@ -42,28 +40,23 @@ bool PushFilterDownTraverseRule::match(OptContext* ctx, const MatchedResult& mat if (!OptRule::match(ctx, matched)) { return false; } - DCHECK_EQ(matched.dependencies[0].dependencies[0].node->node()->kind(), - PlanNode::Kind::kTraverse); - auto traverse = - static_cast(matched.dependencies[0].dependencies[0].node->node()); + DCHECK_EQ(matched.dependencies[0].node->node()->kind(), PlanNode::Kind::kTraverse); + auto traverse = static_cast(matched.dependencies[0].node->node()); return traverse->isOneStep(); } StatusOr PushFilterDownTraverseRule::transform( - OptContext* ctx, const MatchedResult& matched) const { - auto* filterGroupNode = matched.node; - auto* filterGroup = filterGroupNode->group(); - auto* filter = static_cast(filterGroupNode->node()); + OptContext* octx, const MatchedResult& matched) const { + auto* filterGNode = matched.node; + auto* filterGroup = filterGNode->group(); + auto* filter = static_cast(filterGNode->node()); auto* condition = filter->condition(); - auto* avGroupNode = matched.dependencies[0].node; - auto* av = static_cast(avGroupNode->node()); + auto* tvGNode = matched.dependencies[0].node; + auto* tvNode = static_cast(tvGNode->node()); + auto& edgeAlias = tvNode->edgeAlias(); - auto* tvGroupNode = matched.dependencies[0].dependencies[0].node; - auto* tv = static_cast(tvGroupNode->node()); - auto& edgeAlias = tv->edgeAlias(); - - auto qctx = ctx->qctx(); + auto qctx = octx->qctx(); auto pool = qctx->objPool(); // Pick the expr looks like `$-.e[0].likeness @@ -105,49 +98,39 @@ StatusOr PushFilterDownTraverseRule::transform( } auto* newFilterPicked = graph::ExpressionUtils::rewriteEdgePropertyFilter(pool, edgeAlias, filterPicked->clone()); - - Filter* newFilter = nullptr; - OptGroupNode* newFilterGroupNode = nullptr; - if (filterUnpicked) { - newFilter = Filter::make(qctx, nullptr, filterUnpicked); - newFilter->setOutputVar(filter->outputVar()); - newFilter->setColNames(filter->colNames()); - newFilterGroupNode = OptGroupNode::create(ctx, newFilter, filterGroup); - } - - auto* newAv = static_cast(av->clone()); - - OptGroupNode* newAvGroupNode = nullptr; - if (newFilterGroupNode) { - auto* newAvGroup = OptGroup::create(ctx); - newAvGroupNode = newAvGroup->makeGroupNode(newAv); - newFilterGroupNode->dependsOn(newAvGroup); - newFilter->setInputVar(newAv->outputVar()); - } else { - newAvGroupNode = OptGroupNode::create(ctx, newAv, filterGroup); - newAv->setOutputVar(filter->outputVar()); - } - - auto* eFilter = tv->eFilter(); + auto* eFilter = tvNode->eFilter(); Expression* newEFilter = eFilter ? LogicalExpression::makeAnd(pool, newFilterPicked, eFilter->clone()) : newFilterPicked; - auto* newTv = static_cast(tv->clone()); - newAv->setInputVar(newTv->outputVar()); - newTv->setEdgeFilter(newEFilter); - - auto* newTvGroup = OptGroup::create(ctx); - newAvGroupNode->dependsOn(newTvGroup); - auto* newTvGroupNode = newTvGroup->makeGroupNode(newTv); - - for (auto dep : tvGroupNode->dependencies()) { - newTvGroupNode->dependsOn(dep); - } + // produce new Traverse node + auto* newTvNode = static_cast(tvNode->clone()); + newTvNode->setEdgeFilter(newEFilter); + newTvNode->setInputVar(tvNode->inputVar()); + newTvNode->setColNames(tvNode->outputVarPtr()->colNames); + // connect the optimized plan TransformResult result; - result.eraseCurr = true; - result.newGroupNodes.emplace_back(newFilterGroupNode ? newFilterGroupNode : newAvGroupNode); + result.eraseAll = true; + if (filterUnpicked) { + auto* newFilterNode = graph::Filter::make(qctx, newTvNode, filterUnpicked); + newFilterNode->setOutputVar(filter->outputVar()); + newFilterNode->setColNames(filter->colNames()); + auto newFilterGNode = OptGroupNode::create(octx, newFilterNode, filterGroup); + // assemble the new Traverse group below Filter + auto newTvGroup = OptGroup::create(octx); + auto newTvGNode = newTvGroup->makeGroupNode(newTvNode); + newTvGNode->setDeps(tvGNode->dependencies()); + newFilterGNode->setDeps({newTvGroup}); + newFilterNode->setInputVar(newTvNode->outputVar()); + result.newGroupNodes.emplace_back(newFilterGNode); + } else { + // replace the new Traverse node with the old Filter group + auto newTvGNode = OptGroupNode::create(octx, newTvNode, filterGroup); + newTvNode->setOutputVar(filter->outputVar()); + newTvGNode->setDeps(tvGNode->dependencies()); + result.newGroupNodes.emplace_back(newTvGNode); + } return result; } diff --git a/src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.cpp b/src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.cpp new file mode 100644 index 00000000000..0f16dc0bd46 --- /dev/null +++ b/src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.cpp @@ -0,0 +1,123 @@ +/* Copyright (c) 2023 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#include "graph/optimizer/rule/PushFilterThroughAppendVerticesRule.h" + +#include "common/expression/Expression.h" +#include "graph/optimizer/OptContext.h" +#include "graph/optimizer/OptGroup.h" +#include "graph/planner/plan/PlanNode.h" +#include "graph/planner/plan/Query.h" +#include "graph/util/ExpressionUtils.h" +#include "graph/visitor/ExtractFilterExprVisitor.h" + +using nebula::Expression; +using nebula::graph::AppendVertices; +using nebula::graph::Filter; +using nebula::graph::PlanNode; +using nebula::graph::QueryContext; + +namespace nebula { +namespace opt { + +std::unique_ptr PushFilterThroughAppendVerticesRule::kInstance = + std::unique_ptr(new PushFilterThroughAppendVerticesRule()); + +PushFilterThroughAppendVerticesRule::PushFilterThroughAppendVerticesRule() { + RuleSet::QueryRules().addRule(this); +} + +const Pattern& PushFilterThroughAppendVerticesRule::pattern() const { + static Pattern pattern = + Pattern::create(PlanNode::Kind::kFilter, {Pattern::create(PlanNode::Kind::kAppendVertices)}); + return pattern; +} + +StatusOr PushFilterThroughAppendVerticesRule::transform( + OptContext* octx, const MatchedResult& matched) const { + auto* oldFilterGroupNode = matched.node; + auto* oldFilterGroup = oldFilterGroupNode->group(); + auto* oldFilterNode = static_cast(oldFilterGroupNode->node()); + auto* condition = oldFilterNode->condition(); + auto* oldAvGroupNode = matched.dependencies[0].node; + auto* oldAvNode = static_cast(oldAvGroupNode->node()); + auto& dstNode = oldAvNode->nodeAlias(); + + auto inputColNames = oldAvNode->inputVars().front()->colNames; + auto qctx = octx->qctx(); + + auto picker = [&inputColNames, &dstNode](const Expression* expr) -> bool { + auto finder = [&inputColNames, &dstNode](const Expression* e) -> bool { + if (e->isPropertyExpr() && + std::find(inputColNames.begin(), + inputColNames.end(), + (static_cast(e)->prop())) == inputColNames.end()) { + return true; + } + if (e->kind() == Expression::Kind::kVar && + static_cast(e)->var() == dstNode) { + return true; + } + return false; + }; + graph::FindVisitor visitor(finder); + const_cast(expr)->accept(&visitor); + return visitor.results().empty(); + }; + Expression* filterPicked = nullptr; + Expression* filterUnpicked = nullptr; + graph::ExpressionUtils::splitFilter(condition, picker, &filterPicked, &filterUnpicked); + + if (!filterPicked) { + return TransformResult::noTransform(); + } + + // produce new Filter node below + auto* newBelowFilterNode = + graph::Filter::make(qctx, const_cast(oldAvNode->dep()), filterPicked); + newBelowFilterNode->setInputVar(oldAvNode->inputVar()); + newBelowFilterNode->setColNames(oldAvNode->inputVars().front()->colNames); + auto newBelowFilterGroup = OptGroup::create(octx); + auto newFilterGroupNode = newBelowFilterGroup->makeGroupNode(newBelowFilterNode); + newFilterGroupNode->setDeps(oldAvGroupNode->dependencies()); + + // produce new AppendVertices node + auto* newAvNode = static_cast(oldAvNode->clone()); + newAvNode->setInputVar(newBelowFilterNode->outputVar()); + + TransformResult result; + result.eraseAll = true; + if (filterUnpicked) { + // produce new Filter node above + auto* newAboveFilterNode = graph::Filter::make(octx->qctx(), newAvNode, filterUnpicked); + newAboveFilterNode->setOutputVar(oldFilterNode->outputVar()); + auto newAboveFilterGroupNode = OptGroupNode::create(octx, newAboveFilterNode, oldFilterGroup); + + auto newAvGroup = OptGroup::create(octx); + auto newAvGroupNode = newAvGroup->makeGroupNode(newAvNode); + newAvGroupNode->setDeps({newBelowFilterGroup}); + newAvNode->setInputVar(newBelowFilterNode->outputVar()); + newAboveFilterGroupNode->setDeps({newAvGroup}); + newAboveFilterNode->setInputVar(newAvNode->outputVar()); + result.newGroupNodes.emplace_back(newAboveFilterGroupNode); + } else { + newAvNode->setOutputVar(oldFilterNode->outputVar()); + // newAvNode's col names, on the hand, should inherit those of the oldAvNode + // since they are the same project. + newAvNode->setColNames(oldAvNode->outputVarPtr()->colNames); + auto newAvGroupNode = OptGroupNode::create(octx, newAvNode, oldFilterGroup); + newAvGroupNode->setDeps({newBelowFilterGroup}); + newAvNode->setInputVar(newBelowFilterNode->outputVar()); + result.newGroupNodes.emplace_back(newAvGroupNode); + } + return result; +} + +std::string PushFilterThroughAppendVerticesRule::toString() const { + return "PushFilterThroughAppendVerticesRule"; +} + +} // namespace opt +} // namespace nebula diff --git a/src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.h b/src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.h new file mode 100644 index 00000000000..db2ce986fab --- /dev/null +++ b/src/graph/optimizer/rule/PushFilterThroughAppendVerticesRule.h @@ -0,0 +1,46 @@ +/* Copyright (c) 2023 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License. + */ + +#ifndef GRAPH_OPTIMIZER_RULE_PUSHFILTERTHROUGHAPPENDVERTICES_H_ +#define GRAPH_OPTIMIZER_RULE_PUSHFILTERTHROUGHAPPENDVERTICES_H_ + +#include "graph/optimizer/OptRule.h" + +namespace nebula { +namespace opt { + +/* + * Before: + * Filter(e.likeness > 78 and v.prop > 40) + * | + * AppendVertices(v) + * + * After : + * Filter(v.prop > 40) + * | + * AppendVertices(v) + * | + * Filter(e.likeness > 78) + * + */ + +class PushFilterThroughAppendVerticesRule final : public OptRule { + public: + const Pattern &pattern() const override; + + StatusOr transform(OptContext *ctx, const MatchedResult &matched) const override; + + std::string toString() const override; + + private: + PushFilterThroughAppendVerticesRule(); + + static std::unique_ptr kInstance; +}; + +} // namespace opt +} // namespace nebula + +#endif // GRAPH_OPTIMIZER_RULE_PUSHFILTERTHROUGHAPPENDVERTICES_H_ diff --git a/tests/tck/features/bugfix/LackFilterGetEdges.feature b/tests/tck/features/bugfix/LackFilterGetEdges.feature index b13a0ae0bf7..210f2148e79 100644 --- a/tests/tck/features/bugfix/LackFilterGetEdges.feature +++ b/tests/tck/features/bugfix/LackFilterGetEdges.feature @@ -8,6 +8,25 @@ Feature: Test lack filter of get edges transform # #5145 Scenario: Lack filter of get edges transform + When profiling query: + """ + match ()-[e*1]->() + where e[0].likeness > 78 or uuid() > 100 + return rank(e[0]) AS re limit 3 + """ + Then the result should be, in any order: + | re | + | 0 | + | 0 | + | 0 | + And the execution plan should be: + | id | name | dependencies | operator info | + | 24 | Project | 20 | | + | 20 | Limit | 21 | | + | 21 | AppendVertices | 23 | | + | 23 | Traverse | 22 | { "edge filter": "((*.likeness>78) OR (uuid()>100))"} | + | 22 | ScanVertices | 3 | | + | 3 | Start | | | When profiling query: """ match ()-[e]->() diff --git a/tests/tck/features/match/SeekById.feature b/tests/tck/features/match/SeekById.feature index 5917f341d77..a325cbecf4a 100644 --- a/tests/tck/features/match/SeekById.feature +++ b/tests/tck/features/match/SeekById.feature @@ -860,6 +860,7 @@ Feature: Match seek by id | 11 | Project | 8 | | | 8 | Filter | 4 | | | 4 | AppendVertices | 10 | | + | 10 | Filter | 10 | | | 10 | Traverse | 2 | | | 2 | Dedup | 4 | | | 1 | PassThrough | 3 | | diff --git a/tests/tck/features/optimizer/CollapseProjectRule.feature b/tests/tck/features/optimizer/CollapseProjectRule.feature index 924b61635ba..a7b7afaf3ed 100644 --- a/tests/tck/features/optimizer/CollapseProjectRule.feature +++ b/tests/tck/features/optimizer/CollapseProjectRule.feature @@ -6,6 +6,7 @@ Feature: Collapse Project Rule Background: Given a graph with space named "nba" + @czp Scenario: Collapse Project When profiling query: """ @@ -99,6 +100,7 @@ Feature: Collapse Project Rule | 14 | Project | 12 | | | 12 | Filter | 6 | | | 6 | AppendVertices | 5 | | + | 5 | Filter | 5 | | | 5 | Traverse | 4 | | | 4 | Traverse | 2 | | | 2 | Dedup | 1 | | diff --git a/tests/tck/features/optimizer/PrunePropertiesRule.feature b/tests/tck/features/optimizer/PrunePropertiesRule.feature index 5731e68177c..b360166f8a3 100644 --- a/tests/tck/features/optimizer/PrunePropertiesRule.feature +++ b/tests/tck/features/optimizer/PrunePropertiesRule.feature @@ -579,6 +579,7 @@ Feature: Prune Properties rule | 21 | Project | 20 | | | 20 | Filter | 25 | | | 25 | AppendVertices | 24 | { "props": "[{ \"props\":[\"name\",\"_tag\"]}]" } | + | 24 | Filter | 24 | | | 24 | Traverse | 23 | {"vertexProps": "[{ \"props\":[\"age\"]}]", "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | | 23 | Traverse | 22 | {"vertexProps": "", "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | | 22 | Traverse | 2 | {"vertexProps": "", "edgeProps": "[{ \"props\": [\"_type\", \"_rank\", \"_dst\"]}, { \"props\": [\"_type\", \"_rank\", \"_dst\"]}]" } | diff --git a/tests/tck/features/optimizer/PushFilterDownBugFixes.feature b/tests/tck/features/optimizer/PushFilterDownBugFixes.feature index c52ee81cc16..1a71028e0f0 100644 --- a/tests/tck/features/optimizer/PushFilterDownBugFixes.feature +++ b/tests/tck/features/optimizer/PushFilterDownBugFixes.feature @@ -24,6 +24,7 @@ Feature: Bug fixes on filter push-down | 11 | Project | 10 | | | 10 | Filter | 6 | | | 6 | AppendVertices | 5 | | + | 5 | Filter | 5 | | | 5 | Traverse | 4 | | | 4 | Traverse | 2 | | | 2 | Dedup | 1 | | @@ -45,6 +46,7 @@ Feature: Bug fixes on filter push-down | 11 | Project | 10 | | | 10 | Filter | 6 | | | 6 | AppendVertices | 5 | | + | 5 | Filter | 5 | | | 5 | Traverse | 4 | | | 4 | Traverse | 2 | | | 2 | Dedup | 1 | | diff --git a/tests/tck/features/optimizer/PushFilterDownCrossJoinRule.feature b/tests/tck/features/optimizer/PushFilterDownCrossJoinRule.feature index ee2263c921c..ad578e635bf 100644 --- a/tests/tck/features/optimizer/PushFilterDownCrossJoinRule.feature +++ b/tests/tck/features/optimizer/PushFilterDownCrossJoinRule.feature @@ -18,15 +18,16 @@ Feature: Push Filter down HashInnerJoin rule | count(e) | | 8 | And the execution plan should be: - | id | name | dependencies | operator info | - | 11 | Aggregate | 14 | | - | 14 | CrossJoin | 1,16 | | - | 1 | Project | 2 | | - | 2 | Start | | | - | 16 | Project | 15 | | - | 15 | Filter | 18 | {"condition": "((id($-.v1) IN [\"Tim Duncan\",\"Tony Parker\"]) AND (id($-.v2) IN [\"Tim Duncan\",\"Tony Parker\"]))"} | - | 18 | AppendVertices | 17 | | - | 17 | Traverse | 4 | | - | 4 | Dedup | 3 | | - | 3 | PassThrough | 5 | | - | 5 | Start | | | + | id | name | dependencies | operator info | + | 11 | Aggregate | 14 | | + | 14 | CrossJoin | 1,16 | | + | 1 | Project | 2 | | + | 2 | Start | | | + | 16 | Project | 15 | | + | 15 | Filter | 18 | {"condition": "(id($-.v2) IN [\"Tim Duncan\",\"Tony Parker\"])"} | + | 18 | AppendVertices | 17 | | + | 17 | Filter | 17 | {"condition": "(id($-.v1) IN [\"Tim Duncan\",\"Tony Parker\"])"} | + | 17 | Traverse | 4 | | + | 4 | Dedup | 3 | | + | 3 | PassThrough | 5 | | + | 5 | Start | | | diff --git a/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature b/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature index ce9de5da759..d0ee360932d 100644 --- a/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature +++ b/tests/tck/features/optimizer/PushFilterDownTraverseRule.feature @@ -137,9 +137,9 @@ Feature: Push Filter down Traverse rule | 17 | Aggregate | 16 | | | | 16 | HashLeftJoin | 10,15 | | | | 10 | Dedup | 28 | | | - | 28 | Project | 22 | | | - | 22 | Filter | 26 | | | + | 28 | Project | 26 | | | | 26 | AppendVertices | 25 | | | + | 25 | Filter | 25 | | | | 25 | Traverse | 24 | | {"filter": "(serve.start_year>2010)"} | | 24 | Traverse | 2 | | | | 2 | Dedup | 1 | | | diff --git a/tests/tck/features/optimizer/PushLimitDownProjectRule.feature b/tests/tck/features/optimizer/PushLimitDownProjectRule.feature index 5af5c3525ff..54dcf98fa2b 100644 --- a/tests/tck/features/optimizer/PushLimitDownProjectRule.feature +++ b/tests/tck/features/optimizer/PushLimitDownProjectRule.feature @@ -27,6 +27,7 @@ Feature: Push Limit down project rule | 16 | Limit | 11 | | | 11 | Filter | 4 | | | 4 | AppendVertices | 3 | | + | 3 | Filter | 3 | | | 3 | Traverse | 2 | | | 2 | Dedup | 1 | | | 1 | PassThrough | 0 | | diff --git a/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature b/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature index c04a130e4b6..bc5eb8b182d 100644 --- a/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature +++ b/tests/tck/features/optimizer/RemoveAppendVerticesBelowJoinRule.feature @@ -50,9 +50,9 @@ Feature: Remove AppendVertices Below Join | 17 | Aggregate | 16 | | | 16 | HashLeftJoin | 10,15 | {"hashKeys": ["_joinkey($-.friendTeam)", "_joinkey($-.friend)"], "probeKeys": ["$-.friendTeam", "_joinkey($-.friend)"]} | | 10 | Dedup | 28 | | - | 28 | Project | 22 | | - | 22 | Filter | 26 | | + | 28 | Project | 26 | | | 26 | AppendVertices | 25 | | + | 25 | Filter | 25 | | | 25 | Traverse | 24 | | | 24 | Traverse | 2 | | | 2 | Dedup | 1 | |