diff --git a/src/optimizer/CMakeLists.txt b/src/optimizer/CMakeLists.txt index 0c62d94c8172b72e3a94f3fc7cb4df471b08a19e..7cc6d6da8ba4515652553deb81bf8b3f9fbfadf3 100644 --- a/src/optimizer/CMakeLists.txt +++ b/src/optimizer/CMakeLists.txt @@ -22,6 +22,7 @@ nebula_add_library( rule/LimitPushDownRule.cpp rule/TopNRule.cpp rule/PushFilterDownAggregateRule.cpp + rule/PushFilterDownProjectRule.cpp ) nebula_add_subdirectory(test) diff --git a/src/optimizer/rule/PushFilterDownProjectRule.cpp b/src/optimizer/rule/PushFilterDownProjectRule.cpp new file mode 100644 index 0000000000000000000000000000000000000000..c5cbee47cceea256ee76864122929d9ac11e3f5b --- /dev/null +++ b/src/optimizer/rule/PushFilterDownProjectRule.cpp @@ -0,0 +1,100 @@ +/* Copyright (c) 2021 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License, + * attached with Common Clause Condition 1.0, found in the LICENSES directory. + */ + +#include "optimizer/rule/PushFilterDownProjectRule.h" + +#include "optimizer/OptContext.h" +#include "optimizer/OptGroup.h" +#include "planner/plan/PlanNode.h" +#include "planner/plan/Query.h" +#include "util/ExpressionUtils.h" + +using nebula::graph::PlanNode; +using nebula::graph::QueryContext; + +namespace nebula { +namespace opt { + +std::unique_ptr<OptRule> PushFilterDownProjectRule::kInstance = + std::unique_ptr<PushFilterDownProjectRule>(new PushFilterDownProjectRule()); + +PushFilterDownProjectRule::PushFilterDownProjectRule() { + RuleSet::QueryRules().addRule(this); +} + +const Pattern& PushFilterDownProjectRule::pattern() const { + static Pattern pattern = Pattern::create(graph::PlanNode::Kind::kFilter, + {Pattern::create(graph::PlanNode::Kind::kProject)}); + return pattern; +} + +StatusOr<OptRule::TransformResult> PushFilterDownProjectRule::transform( + OptContext* octx, + const MatchedResult& matched) const { + auto filterGroupNode = matched.node; + auto oldFilterNode = filterGroupNode->node(); + auto projGroupNode = matched.dependencies.front().node; + auto oldProjNode = projGroupNode->node(); + + auto newFilterNode = static_cast<graph::Filter*>(oldFilterNode->clone()); + auto newProjNode = static_cast<graph::Project*>(oldProjNode->clone()); + const auto condition = newFilterNode->condition(); + + auto varProps = graph::ExpressionUtils::collectAll(condition, {Expression::Kind::kVarProperty}); + if (varProps.empty()) { + return TransformResult::noTransform(); + } + std::vector<std::string> propNames; + for (auto expr : varProps) { + DCHECK(expr->kind() == Expression::Kind::kVarProperty); + propNames.emplace_back(*static_cast<const VariablePropertyExpression*>(expr)->prop()); + } + + auto projColNames = newProjNode->colNames(); + auto projColumns = newProjNode->columns()->columns(); + for (size_t i = 0; i < projColNames.size(); ++i) { + auto column = projColumns[i]; + auto colName = projColNames[i]; + auto iter = std::find_if(propNames.begin(), propNames.end(), [&colName](const auto& name) { + return !colName.compare(name); + }); + if (iter == propNames.end()) continue; + if (!column->alias() && column->expr()->kind() == Expression::Kind::kVarProperty) { + continue; + } else { + // project column contains computing expression, need to rewrite + return TransformResult::noTransform(); + } + } + + // Exchange planNode + newProjNode->setOutputVar(oldFilterNode->outputVar()); + newFilterNode->setInputVar(oldProjNode->inputVar()); + newProjNode->setInputVar(oldProjNode->outputVar()); + newFilterNode->setOutputVar(oldProjNode->outputVar()); + + // Push down filter's optGroup and embed newProjGroupNode into old filter's Group + auto newProjGroupNode = OptGroupNode::create(octx, newProjNode, filterGroupNode->group()); + auto newFilterGroup = OptGroup::create(octx); + auto newFilterGroupNode = newFilterGroup->makeGroupNode(newFilterNode); + newProjGroupNode->dependsOn(newFilterGroup); + for (auto dep : projGroupNode->dependencies()) { + newFilterGroupNode->dependsOn(dep); + } + + TransformResult result; + result.eraseAll = true; + result.newGroupNodes.emplace_back(newProjGroupNode); + return result; +} + +std::string PushFilterDownProjectRule::toString() const { + return "PushFilterDownProjectRule"; +} + +} // namespace opt +} // namespace nebula + diff --git a/src/optimizer/rule/PushFilterDownProjectRule.h b/src/optimizer/rule/PushFilterDownProjectRule.h new file mode 100644 index 0000000000000000000000000000000000000000..14be2df8baf8eb6b9ee2298b124b6fbf556e4cf6 --- /dev/null +++ b/src/optimizer/rule/PushFilterDownProjectRule.h @@ -0,0 +1,35 @@ +/* Copyright (c) 2021 vesoft inc. All rights reserved. + * + * This source code is licensed under Apache 2.0 License, + * attached with Common Clause Condition 1.0, found in the LICENSES directory. + */ + +#ifndef OPTIMIZER_RULE_PUSHFILTERDOWNPROJECTRULE_H_ +#define OPTIMIZER_RULE_PUSHFILTERDOWNPROJECTRULE_H_ + +#include <memory> +#include "optimizer/OptRule.h" + +namespace nebula { +namespace opt { + +class PushFilterDownProjectRule final : public OptRule { +public: + const Pattern &pattern() const override; + + StatusOr<OptRule::TransformResult> transform(OptContext *qctx, + const MatchedResult &matched) const override; + + std::string toString() const override; + +private: + PushFilterDownProjectRule(); + + static std::unique_ptr<OptRule> kInstance; +}; + +} // namespace opt +} // namespace nebula + +#endif // OPTIMIZER_RULE_PUSHFILTERDOWNPROJECTRULE_H_ + diff --git a/tests/tck/features/optimizer/PushFilterDownProjectRule.feature b/tests/tck/features/optimizer/PushFilterDownProjectRule.feature new file mode 100644 index 0000000000000000000000000000000000000000..4593f4e7aafc8de8969bb5087f39f4672fa3ae40 --- /dev/null +++ b/tests/tck/features/optimizer/PushFilterDownProjectRule.feature @@ -0,0 +1,82 @@ +# Copyright (c) 2021 vesoft inc. All rights reserved. +# +# This source code is licensed under Apache 2.0 License, +# attached with Common Clause Condition 1.0, found in the LICENSES directory. +Feature: Push Filter down Project rule + + Background: + Given a graph with space named "nba" + + Scenario: push filter down Project + When profiling query: + """ + MATCH (a:player)--(b)--(c) + WITH a AS a, b AS b, c AS c + WHERE a.age < 25 AND b.age < 25 AND c.age < 25 + RETURN a + """ + Then the result should be, in any order: + | a | + | ("Kristaps Porzingis" :player{age: 23, name: "Kristaps Porzingis"}) | + | ("Kristaps Porzingis" :player{age: 23, name: "Kristaps Porzingis"}) | + | ("Luka Doncic" :player{age: 20, name: "Luka Doncic"}) | + | ("Luka Doncic" :player{age: 20, name: "Luka Doncic"}) | + And the execution plan should be: + | id | name | dependencies | operator info | + | 23 | Project | 40 | | + | 40 | Project | 39 | | + | 39 | Filter | 20 | | + | 20 | Filter | 19 | | + | 19 | Project | 18 | | + | 18 | InnerJoin | 17 | | + | 17 | Project | 28 | | + | 28 | GetVertices | 13 | | + | 13 | InnerJoin | 12 | | + | 12 | Filter | 11 | | + | 11 | Project | 32 | | + | 32 | GetNeighbors | 7 | | + | 7 | Filter | 6 | | + | 6 | Project | 5 | | + | 5 | Filter | 31 | | + | 31 | GetNeighbors | 24 | | + | 24 | IndexScan | 0 | | + | 0 | Start | | | + When profiling query: + """ + MATCH (a:player)--(b)--(c) + WITH a, b, c.age+1 AS cage + WHERE a.name == 'Tim Duncan' AND b.age > 40 + RETURN DISTINCT a, b, cage + """ + Then the result should be, in any order: + | a | b | cage | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"}) | 39 | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"}) | 32 | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Shaquile O'Neal" :player{age: 47, name: "Shaquile O'Neal"}) | NULL | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | NULL | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | 43 | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | 37 | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | 35 | + | ("Tim Duncan" :bachelor{name: "Tim Duncan", speciality: "psychology"} :player{age: 42, name: "Tim Duncan"}) | ("Manu Ginobili" :player{age: 41, name: "Manu Ginobili"}) | 30 | + And the execution plan should be: + | id | name | dependencies | operator info | + | 25 | DataCollect | 24 | | + | 24 | Dedup | 41 | | + | 41 | Project | 40 | | + | 40 | Filter | 20 | | + | 20 | Filter | 19 | | + | 19 | Project | 18 | | + | 18 | InnerJoin | 17 | | + | 17 | Project | 29 | | + | 29 | GetVertices | 13 | | + | 13 | InnerJoin | 12 | | + | 12 | Filter | 11 | | + | 11 | Project | 33 | | + | 33 | GetNeighbors | 7 | | + | 7 | Filter | 6 | | + | 6 | Project | 5 | | + | 5 | Filter | 32 | | + | 32 | GetNeighbors | 26 | | + | 26 | IndexScan | 0 | | + | 0 | Start | | | +