diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000000000000000000000000000000000000..5c720ed9c71e7f72ca52154e4019ac2498d5d050
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,157 @@
+# Copyright (c) 2020 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.
+#
+# This is the output of clang-format-7.0 --style=google --dump-config,
+# except for changes mentioned below.
+# We have locked the version of clang-format in order to avoid inconsistencies
+# in the format caused by developers using different clang-format versions.
+---
+Language:        Cpp
+# BasedOnStyle:  Google
+AlignAfterOpenBracket: Align
+AlignConsecutiveDeclarations: false
+AlignOperands:   true
+AlignTrailingComments: true
+AllowShortBlocksOnASingleLine: false
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortIfStatementsOnASingleLine: true
+AllowShortLoopsOnASingleLine: true
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakTemplateDeclarations: Yes
+BraceWrapping:
+  AfterClass:      false
+  AfterControlStatement: false
+  AfterEnum:       false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  AfterExternBlock: false
+  BeforeCatch:     false
+  BeforeElse:      false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: None
+BreakBeforeBraces: Attach
+BreakBeforeInheritanceComma: false
+BreakInheritanceList: BeforeColon
+BreakBeforeTernaryOperators: true
+BreakConstructorInitializersBeforeComma: false
+BreakConstructorInitializers: BeforeColon
+BreakAfterJavaFieldAnnotations: false
+BreakStringLiterals: true
+CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: true
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: true
+DisableFormat:   false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '^<ext/.*\.h>'
+    Priority:        2
+  - Regex:           '^<.*\.h>'
+    Priority:        1
+  - Regex:           '^<.*'
+    Priority:        2
+  - Regex:           '.*'
+    Priority:        3
+IncludeIsMainRegex: '([-_](test|unittest))?$'
+IndentCaseLabels: true
+IndentPPDirectives: None
+IndentWrappedFunctionNames: false
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Never
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 2
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Left
+RawStringFormats:
+  - Language:        Cpp
+    Delimiters:
+      - cc
+      - CC
+      - cpp
+      - Cpp
+      - CPP
+      - 'c++'
+      - 'C++'
+    CanonicalDelimiter: ''
+    BasedOnStyle:    google
+  - Language:        TextProto
+    Delimiters:
+      - pb
+      - PB
+      - proto
+      - PROTO
+    EnclosingFunctions:
+      - EqualsProto
+      - EquivToProto
+      - PARSE_PARTIAL_TEXT_PROTO
+      - PARSE_TEST_PROTO
+      - PARSE_TEXT_PROTO
+      - ParseTextOrDie
+      - ParseTextProtoOrDie
+    CanonicalDelimiter: ''
+    BasedOnStyle:    google
+ReflowComments:  true
+SortIncludes:    true
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: false
+SpaceAfterTemplateKeyword: true
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatements
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyParentheses: false
+SpacesInAngles:  false
+SpacesInContainerLiterals: true
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+TabWidth:        4
+UseTab:          Never
+
+#Different from google style
+Standard:        Cpp11
+AccessModifierOffset: -4
+AllowAllParametersOfDeclarationOnNextLine: false
+ColumnLimit:     100
+ObjCBlockIndentWidth: 4
+AlignEscapedNewlines: Right
+AlwaysBreakBeforeMultilineStrings: false
+BinPackArguments: false
+BinPackParameters: false
+IndentWidth:     4
+SpacesBeforeTrailingComments: 3
+AllowShortFunctionsOnASingleLine: Empty
+...
diff --git a/ci/test.sh b/ci/test.sh
index 49ad302153012314009b04ef2c4fde3d98c5fbf9..14e7926ae836b1d101263588a37e709a153b072c 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -82,6 +82,7 @@ function run_test() {
         $PROJ_DIR/tests/maintain/* \
         $PROJ_DIR/tests/mutate/* \
         $PROJ_DIR/tests/query/stateless/test_new_go.py \
+        $PROJ_DIR/tests/query/stateless/test_new_groupby.py \
         $PROJ_DIR/tests/query/v1/* \
         $PROJ_DIR/tests/query/v2/* \
         $PROJ_DIR/tests/query/stateless/test_schema.py \
diff --git a/src/parser/Sentence.h b/src/parser/Sentence.h
index 95138185cd9097824a92f1f4d94ffc304a3898e1..4b4a62a44c216c1f30d81dda794374bc308fb445 100644
--- a/src/parser/Sentence.h
+++ b/src/parser/Sentence.h
@@ -97,7 +97,7 @@ public:
         kBalance,
         kFindPath,
         kLimit,
-        KGroupBy,
+        kGroupBy,
         kReturn,
         kCreateSnapshot,
         kDropSnapshot,
diff --git a/src/parser/TraverseSentences.h b/src/parser/TraverseSentences.h
index a9db4efc63a999c32499deb769ca358c14ea702d..38b0301fcf6c127655ffefcebb2b9f892a6d67af 100644
--- a/src/parser/TraverseSentences.h
+++ b/src/parser/TraverseSentences.h
@@ -543,7 +543,7 @@ private:
 class GroupBySentence final : public Sentence {
 public:
     GroupBySentence() {
-        kind_ = Kind::KGroupBy;
+        kind_ = Kind::kGroupBy;
     }
 
     void setGroupClause(GroupClause *clause) {
diff --git a/src/service/PermissionCheck.cpp b/src/service/PermissionCheck.cpp
index af5e34af0e2ff1aa4652124c5d211f59a203af1f..f4c71973c69ae57c601ba71e91b3020cf5118546 100644
--- a/src/service/PermissionCheck.cpp
+++ b/src/service/PermissionCheck.cpp
@@ -116,7 +116,7 @@ bool PermissionCheck::permissionCheck(Session *session,
         case Sentence::Kind::kFindPath :
         case Sentence::Kind::kGetSubgraph:
         case Sentence::Kind::kLimit :
-        case Sentence::Kind::KGroupBy :
+        case Sentence::Kind::kGroupBy :
         case Sentence::Kind::kReturn : {
             return PermissionManager::canReadSchemaOrData(session);
         }
diff --git a/src/validator/CMakeLists.txt b/src/validator/CMakeLists.txt
index daa47c3df699f96c5c38816b02236274aa5aff58..2510cf36b0c58c8596d69835b0f64c7626a7dad4 100644
--- a/src/validator/CMakeLists.txt
+++ b/src/validator/CMakeLists.txt
@@ -23,6 +23,7 @@ nebula_add_library(
     OrderByValidator.cpp
     YieldValidator.cpp
     ExplainValidator.cpp
+    GroupByValidator.cpp
 )
 
 add_subdirectory(test)
diff --git a/src/validator/GoValidator.cpp b/src/validator/GoValidator.cpp
index c11874ef879424acd45548f2832fa45c972f8b2d..ceb34486b3017799c5b0e9743df66dfdee1f72d7 100644
--- a/src/validator/GoValidator.cpp
+++ b/src/validator/GoValidator.cpp
@@ -24,15 +24,16 @@ Status GoValidator::validateImpl() {
     NG_RETURN_IF_ERROR(validateWhere(goSentence->whereClause()));
     NG_RETURN_IF_ERROR(validateYield(goSentence->yieldClause()));
 
-    if (!inputProps_.empty() && fromType_ != kPipe) {
+    if (!exprProps_.inputProps().empty() && fromType_ != kPipe) {
         return Status::Error("$- must be referred in FROM before used in WHERE or YIELD");
     }
 
-    if (!varProps_.empty() && fromType_ != kVariable) {
+    if (!exprProps_.varProps().empty() && fromType_ != kVariable) {
         return Status::Error("A variable must be referred in FROM before used in WHERE or YIELD");
     }
 
-    if ((!inputProps_.empty() && !varProps_.empty()) || varProps_.size() > 1) {
+    if ((!exprProps_.inputProps().empty() && !exprProps_.varProps().empty()) ||
+        exprProps_.varProps().size() > 1) {
         return Status::Error("Only support single input in a go sentence.");
     }
 
@@ -169,7 +170,7 @@ Status GoValidator::validateWhere(WhereClause* where) {
         return Status::Error(ss.str());
     }
 
-    auto status = deduceProps(filter_);
+    auto status = deduceProps(filter_, exprProps_);
     if (!status.ok()) {
         return status;
     }
@@ -194,7 +195,7 @@ Status GoValidator::validateYield(YieldClause* yield) {
             auto colName = deduceColName(col);
             colNames_.emplace_back(colName);
             outputs_.emplace_back(colName, Value::Type::STRING);
-            NG_RETURN_IF_ERROR(deduceProps(col->expr()));
+            NG_RETURN_IF_ERROR(deduceProps(col->expr(), exprProps_));
         }
 
         yields_ = newCols;
@@ -221,9 +222,9 @@ Status GoValidator::validateYield(YieldClause* yield) {
             auto type = typeStatus.value();
             outputs_.emplace_back(colName, type);
 
-            NG_RETURN_IF_ERROR(deduceProps(col->expr()));
+            NG_RETURN_IF_ERROR(deduceProps(col->expr(), exprProps_));
         }
-        for (auto& e : edgeProps_) {
+        for (auto& e : exprProps_.edgeProps()) {
             auto found = std::find(edgeTypes_.begin(), edgeTypes_.end(), e.first);
             if (found == edgeTypes_.end()) {
                 return Status::Error("Edges should be declared first in over clause.");
@@ -258,12 +259,13 @@ Status GoValidator::oneStep(PlanNode* dependencyForGn,
     PlanNode* dependencyForProjectResult = gn;
 
     PlanNode* projectSrcEdgeProps = nullptr;
-    if (!inputProps_.empty() || !varProps_.empty() || !dstTagProps_.empty()) {
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty() ||
+        !exprProps_.dstTagProps().empty()) {
         projectSrcEdgeProps = buildProjectSrcEdgePropsForGN(gn);
     }
 
     PlanNode* joinDstProps = nullptr;
-    if (!dstTagProps_.empty() && projectSrcEdgeProps != nullptr) {
+    if (!exprProps_.dstTagProps().empty() && projectSrcEdgeProps != nullptr) {
         joinDstProps = buildJoinDstProps(projectSrcEdgeProps);
     }
     if (joinDstProps != nullptr) {
@@ -271,7 +273,7 @@ Status GoValidator::oneStep(PlanNode* dependencyForGn,
     }
 
     PlanNode* joinInput = nullptr;
-    if (!inputProps_.empty() || !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         joinInput = buildJoinPipeOrVariableInput(
             projectFromJoin,
             joinDstProps == nullptr ? projectSrcEdgeProps : joinDstProps);
@@ -319,7 +321,7 @@ Status GoValidator::buildNStepsPlan() {
     }
 
     Project* projectLeftVarForJoin = nullptr;
-    if (!inputProps_.empty() || !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         projectLeftVarForJoin = buildLeftVarForTraceJoin(projectStartVid);
     }
 
@@ -332,7 +334,7 @@ Status GoValidator::buildNStepsPlan() {
     Project* projectDstFromGN = projectDstVidsFromGN(gn, startVidsVar);
 
     Project* projectFromJoin = nullptr;
-    if ((!inputProps_.empty() || !varProps_.empty()) &&
+    if ((!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) &&
         projectLeftVarForJoin != nullptr && projectDstFromGN != nullptr) {
         projectFromJoin = traceToStartVid(projectLeftVarForJoin, projectDstFromGN);
     }
@@ -366,7 +368,7 @@ PlanNode* GoValidator::buildProjectSrcEdgePropsForGN(PlanNode* gn) {
     DCHECK(gn != nullptr);
 
     auto* plan = qctx_->plan();
-    if (!inputProps_.empty() || !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         auto* srcVidCol = new YieldColumn(
             new VariablePropertyExpression(new std::string(gn->varName()),
                                         new std::string(kVid)),
@@ -375,7 +377,7 @@ PlanNode* GoValidator::buildProjectSrcEdgePropsForGN(PlanNode* gn) {
     }
 
     VLOG(1) << "build dst cols";
-    if (!dstTagProps_.empty()) {
+    if (!exprProps_.dstTagProps().empty()) {
         joinDstVidColName_ = vctx_->anonColGen()->getCol();
         auto* dstVidCol = new YieldColumn(
             new EdgePropertyExpression(new std::string("*"),
@@ -536,7 +538,7 @@ Project* GoValidator::projectDstVidsFromGN(PlanNode* gn, const std::string& outp
     columns->addColumn(column);
 
     srcVidColName_ = vctx_->anonColGen()->getCol();
-    if (!inputProps_.empty() || !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         column =
             new YieldColumn(new InputPropertyExpression(new std::string(kVid)),
                             new std::string(srcVidColName_));
@@ -633,14 +635,15 @@ PlanNode* GoValidator::buildRuntimeInput() {
 
 GetNeighbors::VertexProps GoValidator::buildSrcVertexProps() {
     GetNeighbors::VertexProps vertexProps;
-    if (!srcTagProps_.empty()) {
+    if (!exprProps_.srcTagProps().empty()) {
         vertexProps = std::make_unique<std::vector<storage::cpp2::VertexProp>>(
-            srcTagProps_.size());
-        std::transform(srcTagProps_.begin(), srcTagProps_.end(),
+            exprProps_.srcTagProps().size());
+        std::transform(exprProps_.srcTagProps().begin(), exprProps_.srcTagProps().end(),
                        vertexProps->begin(), [](auto& tag) {
                            storage::cpp2::VertexProp vp;
                            vp.tag = tag.first;
-                           vp.props = std::move(tag.second);
+                           std::vector<std::string>props(tag.second.begin(), tag.second.end());
+                           vp.props = std::move(props);
                            return vp;
                        });
     }
@@ -648,13 +651,14 @@ GetNeighbors::VertexProps GoValidator::buildSrcVertexProps() {
 }
 
 std::vector<storage::cpp2::VertexProp> GoValidator::buildDstVertexProps() {
-    std::vector<storage::cpp2::VertexProp> vertexProps(dstTagProps_.size());
-    if (!dstTagProps_.empty()) {
-        std::transform(dstTagProps_.begin(), dstTagProps_.end(),
+    std::vector<storage::cpp2::VertexProp> vertexProps(exprProps_.dstTagProps().size());
+    if (!exprProps_.dstTagProps().empty()) {
+        std::transform(exprProps_.dstTagProps().begin(), exprProps_.dstTagProps().end(),
                        vertexProps.begin(), [](auto& tag) {
                            storage::cpp2::VertexProp vp;
                            vp.tag = tag.first;
-                           vp.props = std::move(tag.second);
+                           std::vector<std::string>props(tag.second.begin(), tag.second.end());
+                           vp.props = std::move(props);
                            return vp;
                        });
     }
@@ -663,59 +667,75 @@ std::vector<storage::cpp2::VertexProp> GoValidator::buildDstVertexProps() {
 
 GetNeighbors::EdgeProps GoValidator::buildEdgeProps() {
     GetNeighbors::EdgeProps edgeProps;
-    if (!edgeProps_.empty()) {
+    if (!exprProps_.edgeProps().empty()) {
         if (direction_ == storage::cpp2::EdgeDirection::IN_EDGE) {
             edgeProps = std::make_unique<std::vector<storage::cpp2::EdgeProp>>(
-                edgeProps_.size());
-            std::transform(edgeProps_.begin(), edgeProps_.end(),
-                           edgeProps->begin(), [this](auto& edge) {
+                exprProps_.edgeProps().size());
+            std::transform(exprProps_.edgeProps().begin(),
+                           exprProps_.edgeProps().end(),
+                           edgeProps->begin(),
+                           [this](auto& edge) {
                                storage::cpp2::EdgeProp ep;
                                ep.type = -edge.first;
-                               ep.props = std::move(edge.second);
-                               if (!dstTagProps_.empty()) {
+                               std::vector<std::string> props(edge.second.begin(),
+                                                              edge.second.end());
+                               ep.props = std::move(props);
+                               if (!exprProps_.dstTagProps().empty()) {
                                    ep.props.emplace_back(kDst);
                                }
                                return ep;
                            });
         } else if (direction_ == storage::cpp2::EdgeDirection::BOTH) {
-            auto size = edgeProps_.size();
+            auto size = exprProps_.edgeProps().size();
             edgeProps = std::make_unique<std::vector<storage::cpp2::EdgeProp>>(
                 size * 2);
-            std::transform(edgeProps_.begin(), edgeProps_.end(),
-                           edgeProps->begin(), [this](auto& edge) {
+            std::transform(exprProps_.edgeProps().begin(),
+                           exprProps_.edgeProps().end(),
+                           edgeProps->begin(),
+                           [this](auto& edge) {
                                storage::cpp2::EdgeProp ep;
                                ep.type = edge.first;
-                               ep.props = edge.second;
-                               if (!dstTagProps_.empty()) {
+                               std::vector<std::string> props(edge.second.begin(),
+                                                              edge.second.end());
+                               ep.props = std::move(props);
+                               if (!exprProps_.dstTagProps().empty()) {
                                    ep.props.emplace_back(kDst);
                                }
                                return ep;
                            });
-            std::transform(edgeProps_.begin(), edgeProps_.end(),
-                           edgeProps->begin() + size, [this](auto& edge) {
+            std::transform(exprProps_.edgeProps().begin(),
+                           exprProps_.edgeProps().end(),
+                           edgeProps->begin() + size,
+                           [this](auto& edge) {
                                storage::cpp2::EdgeProp ep;
                                ep.type = -edge.first;
-                               ep.props = std::move(edge.second);
-                               if (!dstTagProps_.empty()) {
+                               std::vector<std::string> props(edge.second.begin(),
+                                                              edge.second.end());
+                               ep.props = std::move(props);
+                               if (!exprProps_.dstTagProps().empty()) {
                                    ep.props.emplace_back(kDst);
                                }
                                return ep;
                            });
         } else {
             edgeProps = std::make_unique<std::vector<storage::cpp2::EdgeProp>>(
-                edgeProps_.size());
-            std::transform(edgeProps_.begin(), edgeProps_.end(),
-                           edgeProps->begin(), [this](auto& edge) {
+                exprProps_.edgeProps().size());
+            std::transform(exprProps_.edgeProps().begin(),
+                           exprProps_.edgeProps().end(),
+                           edgeProps->begin(),
+                           [this](auto& edge) {
                                storage::cpp2::EdgeProp ep;
                                ep.type = edge.first;
-                               ep.props = std::move(edge.second);
-                               if (!dstTagProps_.empty()) {
+                               std::vector<std::string> props(edge.second.begin(),
+                                                              edge.second.end());
+                               ep.props = std::move(props);
+                               if (!exprProps_.dstTagProps().empty()) {
                                    ep.props.emplace_back(kDst);
                                }
                                return ep;
                            });
         }
-    } else if (!dstTagProps_.empty()) {
+    } else if (!exprProps_.dstTagProps().empty()) {
         return buildEdgeDst();
     }
 
@@ -724,7 +744,7 @@ GetNeighbors::EdgeProps GoValidator::buildEdgeProps() {
 
 GetNeighbors::EdgeProps GoValidator::buildEdgeDst() {
     GetNeighbors::EdgeProps edgeProps;
-    if (!edgeProps_.empty() || !dstTagProps_.empty()) {
+    if (!exprProps_.edgeProps().empty() || !exprProps_.dstTagProps().empty()) {
         if (direction_ == storage::cpp2::EdgeDirection::IN_EDGE) {
             edgeProps = std::make_unique<std::vector<storage::cpp2::EdgeProp>>(
                 edgeTypes_.size());
@@ -998,19 +1018,21 @@ std::unique_ptr<Expression> GoValidator::rewriteToInputProp(Expression* expr) {
 }
 
 Status GoValidator::buildColumns() {
-    if (dstTagProps_.empty() && inputProps_.empty() && varProps_.empty()) {
-        return Status::OK();
-    }
+  if (exprProps_.dstTagProps().empty() && exprProps_.inputProps().empty() &&
+      exprProps_.varProps().empty()) {
+      return Status::OK();
+  }
 
-    if (!srcTagProps_.empty() || !edgeProps_.empty() || !dstTagProps_.empty()) {
-        srcAndEdgePropCols_ = qctx_->plan()->saveObject(new YieldColumns());
-    }
+  if (!exprProps_.srcTagProps().empty() || !exprProps_.edgeProps().empty() ||
+      !exprProps_.dstTagProps().empty()) {
+      srcAndEdgePropCols_ = qctx_->plan()->saveObject(new YieldColumns());
+  }
 
-    if (!dstTagProps_.empty()) {
+    if (!exprProps_.dstTagProps().empty()) {
         dstPropCols_ = qctx_->plan()->saveObject(new YieldColumns());
     }
 
-    if (!inputProps_.empty() || !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() || !exprProps_.varProps().empty()) {
         inputPropCols_ = qctx_->plan()->saveObject(new YieldColumns());
     }
 
diff --git a/src/validator/GoValidator.h b/src/validator/GoValidator.h
index 775dffe073ef5943f570905a6c22952bcd468690..39f77a69bb52760406165443a6b8910e9a880e2d 100644
--- a/src/validator/GoValidator.h
+++ b/src/validator/GoValidator.h
@@ -95,6 +95,8 @@ private:
     bool                                                    distinct_{false};
     std::string                                             userDefinedVarName_;
 
+    ExpressionProps                                         exprProps_;
+
     // Generated by validator if needed, and the lifecycle of raw pinters would
     // be managed by object pool
     YieldColumns*                                           srcAndEdgePropCols_{nullptr};
diff --git a/src/validator/GroupByValidator.cpp b/src/validator/GroupByValidator.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..51303e5cf51992a264c34982ce96e2b806a43768
--- /dev/null
+++ b/src/validator/GroupByValidator.cpp
@@ -0,0 +1,156 @@
+/* Copyright (c) 2020 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 "validator/GroupByValidator.h"
+#include "planner/Query.h"
+
+
+namespace nebula {
+namespace graph {
+
+Status GroupByValidator::validateImpl() {
+    auto *groupBySentence = static_cast<GroupBySentence*>(sentence_);
+    NG_RETURN_IF_ERROR(validateGroup(groupBySentence->groupClause()));
+    NG_RETURN_IF_ERROR(validateYield(groupBySentence->yieldClause()));
+    NG_RETURN_IF_ERROR(checkInputProps());
+    NG_RETURN_IF_ERROR(checkVarProps());
+
+    if (!exprProps_.srcTagProps().empty() || !exprProps_.dstTagProps().empty()) {
+        return Status::SemanticError("Only support input and variable in GroupBy sentence.");
+    }
+    if (!exprProps_.inputProps().empty() && !exprProps_.varProps().empty()) {
+        return Status::SemanticError("Not support both input and variable in GroupBy sentence.");
+    }
+    return Status::OK();
+}
+
+Status GroupByValidator::checkInputProps() const {
+    auto& inputProps = const_cast<ExpressionProps*>(&exprProps_)->inputProps();
+    if (inputs_.empty() && !inputProps.empty()) {
+        return Status::SemanticError("no inputs for GroupBy.");
+    }
+    for (auto &prop : inputProps) {
+        DCHECK_NE(prop, "*");
+        NG_RETURN_IF_ERROR(checkPropNonexistOrDuplicate(inputs_, prop, "GroupBy sentence"));
+    }
+    return Status::OK();
+}
+
+Status GroupByValidator::checkVarProps() const {
+    auto& varProps = const_cast<ExpressionProps*>(&exprProps_)->varProps();
+    for (auto &pair : varProps) {
+        auto &var = pair.first;
+        if (!vctx_->existVar(var)) {
+            return Status::SemanticError("variable `%s' not exist.", var.c_str());
+        }
+        auto &props = vctx_->getVar(var);
+        for (auto &prop : pair.second) {
+            DCHECK_NE(prop, "*");
+            NG_RETURN_IF_ERROR(checkPropNonexistOrDuplicate(props, prop, "GroupBy sentence"));
+        }
+    }
+    return Status::OK();
+}
+
+Status GroupByValidator::validateYield(const YieldClause *yieldClause) {
+    std::vector<YieldColumn*> columns;
+    if (yieldClause != nullptr) {
+        columns = yieldClause->columns();
+    }
+    if (columns.empty()) {
+        return Status::SemanticError("Yield cols is Empty");
+    }
+
+    for (auto* col : columns) {
+        auto fun = col->getAggFunName();
+        if (!fun.empty()) {
+            auto iter = AggFun::nameIdMap_.find(fun);
+            if (iter == AggFun::nameIdMap_.end()) {
+                return Status::SemanticError("Unkown aggregate function `%s`", fun.c_str());
+            }
+            if (iter->second != AggFun::Function::kCount && col->expr()->toString() == "*") {
+                return Status::SemanticError("`%s` invaild, * valid in count.",
+                                             col->toString().c_str());
+            }
+        }
+
+        // todo(jmq) count(distinct)
+        groupItems_.emplace_back(Aggregate::GroupItem{col->expr(), AggFun::nameIdMap_[fun], false});
+
+        auto status = deduceExprType(col->expr());
+        NG_RETURN_IF_ERROR(status);
+        auto type = std::move(status).value();
+        auto name = deduceColName(col);
+        outputs_.emplace_back(name, type);
+        outputColumnNames_.emplace_back(std::move(name));
+        // todo(jmq) extend $-.*
+
+        yieldCols_.emplace_back(col);
+        if (col->alias() != nullptr) {
+            aliases_.emplace(*col->alias(), col);
+        }
+
+        // check input yield filed without agg function and not in group cols
+        ExpressionProps yieldProps;
+        NG_RETURN_IF_ERROR(deduceProps(col->expr(), yieldProps));
+        if (col->getAggFunName().empty()) {
+            if (!yieldProps.inputProps().empty()) {
+                if (!exprProps_.isSubsetOfInput(yieldProps.inputProps())) {
+                    return Status::SemanticError("Yield `%s` isn't in output fields",
+                                                 col->toString().c_str());
+                }
+            } else if (!yieldProps.varProps().empty()) {
+                if (!exprProps_.isSubsetOfVar(yieldProps.varProps())) {
+                    return Status::SemanticError("Yield `%s` isn't in output fields",
+                                                 col->toString().c_str());
+                }
+            }
+        }
+        exprProps_.unionProps(std::move(yieldProps));
+    }
+    return Status::OK();
+}
+
+
+Status GroupByValidator::validateGroup(const GroupClause *groupClause) {
+    std::vector<YieldColumn*> columns;
+    if (groupClause != nullptr) {
+        columns = groupClause->columns();
+    }
+
+    if (columns.empty()) {
+        return Status::SemanticError("Group cols is Empty");
+    }
+    for (auto* col : columns) {
+        if (col->expr()->kind() != Expression::Kind::kInputProperty &&
+            col->expr()->kind() != Expression::Kind::kFunctionCall) {
+            return Status::SemanticError("Group `%s` invalid", col->expr()->toString().c_str());
+        }
+        if (!col->getAggFunName().empty()) {
+            return Status::SemanticError("Use invalid group function `%s`",
+                                         col->getAggFunName().c_str());
+        }
+        NG_RETURN_IF_ERROR(deduceExprType(col->expr()));
+        NG_RETURN_IF_ERROR(deduceProps(col->expr(), exprProps_));
+
+        groupCols_.emplace_back(col);
+        groupKeys_.emplace_back(col->expr());
+    }
+    return Status::OK();
+}
+
+Status GroupByValidator::toPlan() {
+    auto *plan = qctx_->plan();
+    auto *groupBy =
+        Aggregate::make(plan, nullptr, std::move(groupKeys_), std::move(groupItems_));
+    groupBy->setColNames(std::vector<std::string>(outputColumnNames_));
+    root_ = groupBy;
+    tail_ = groupBy;
+    return Status::OK();
+}
+
+}  // namespace graph
+}  // namespace nebula
diff --git a/src/validator/GroupByValidator.h b/src/validator/GroupByValidator.h
new file mode 100644
index 0000000000000000000000000000000000000000..fa5f2de3f3230f4bc316bf0a337168d6834b7c70
--- /dev/null
+++ b/src/validator/GroupByValidator.h
@@ -0,0 +1,54 @@
+/* Copyright (c) 2020 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 VALIDATOR_GROUPBY_VALIDATOR_H_
+#define VALIDATOR_GROUPBY_VALIDATOR_H_
+
+#include "common/base/Base.h"
+#include "validator/Validator.h"
+#include "planner/Query.h"
+
+
+namespace nebula {
+namespace graph {
+
+class GroupByValidator final : public Validator {
+public:
+    GroupByValidator(Sentence *sentence, QueryContext *context)
+        : Validator(sentence, context) {}
+
+private:
+    Status validateImpl() override;
+
+    Status toPlan() override;
+
+    Status validateGroup(const GroupClause *groupClause);
+
+    Status validateYield(const YieldClause *yieldClause);
+
+    Status checkInputProps() const;
+
+    Status checkVarProps() const;
+
+private:
+    std::vector<YieldColumn*>                         groupCols_;
+    std::vector<YieldColumn*>                         yieldCols_;
+
+    // key: alias, value: input name
+    std::unordered_map<std::string, YieldColumn*>     aliases_;
+
+    std::vector<std::string>                          outputColumnNames_;
+
+    ExpressionProps                                   exprProps_;
+
+    std::vector<Expression*>                          groupKeys_;
+    std::vector<Aggregate::GroupItem>                 groupItems_;
+};
+
+
+}  // namespace graph
+}  // namespace nebula
+#endif
diff --git a/src/validator/MutateValidator.cpp b/src/validator/MutateValidator.cpp
index ffb6db4df25fdb48ee94ef05b62f8cec1d929e90..681c1475e3cd7ecc641ac09f43a114e546ebad94 100644
--- a/src/validator/MutateValidator.cpp
+++ b/src/validator/MutateValidator.cpp
@@ -471,19 +471,20 @@ Status DeleteEdgesValidator::buildEdgeKeyRef(const std::vector<EdgeKey*> &edgeKe
 Status DeleteEdgesValidator::checkInput() {
     CHECK(!edgeKeyRefs_.empty());
     auto &edgeKeyRef = *edgeKeyRefs_.begin();
-    NG_LOG_AND_RETURN_IF_ERROR(deduceProps(edgeKeyRef->srcid()));
-    NG_LOG_AND_RETURN_IF_ERROR(deduceProps(edgeKeyRef->dstid()));
-    NG_LOG_AND_RETURN_IF_ERROR(deduceProps(edgeKeyRef->rank()));
+    NG_LOG_AND_RETURN_IF_ERROR(deduceProps(edgeKeyRef->srcid(), exprProps_));
+    NG_LOG_AND_RETURN_IF_ERROR(deduceProps(edgeKeyRef->dstid(), exprProps_));
+    NG_LOG_AND_RETURN_IF_ERROR(deduceProps(edgeKeyRef->rank(), exprProps_));
 
-    if (!srcTagProps_.empty() || !dstTagProps_.empty() || !edgeProps_.empty()) {
+    if (!exprProps_.srcTagProps().empty() || !exprProps_.dstTagProps().empty() ||
+        !exprProps_.edgeProps().empty()) {
         return Status::SyntaxError("Only support input and variable.");
     }
 
-    if (!inputProps_.empty() && !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() && !exprProps_.varProps().empty()) {
         return Status::Error("Not support both input and variable.");
     }
 
-    if (!varProps_.empty() && varProps_.size() > 1) {
+    if (!exprProps_.varProps().empty() && exprProps_.varProps().size() > 1) {
         return Status::Error("Only one variable allowed to use.");
     }
 
diff --git a/src/validator/MutateValidator.h b/src/validator/MutateValidator.h
index e06cefa7257a4cee4cb93f295a66364e900edb10..4963cb3903674ff2c7b21ceb39e3e505af93b247 100644
--- a/src/validator/MutateValidator.h
+++ b/src/validator/MutateValidator.h
@@ -109,6 +109,7 @@ private:
     // From InputPropertyExpression, ConstantExpression will covert to  InputPropertyExpression
     std::vector<EdgeKeyRef*>                       edgeKeyRefs_;
     std::string                                    edgeKeyVar_;
+    ExpressionProps                                exprProps_;
 };
 
 class UpdateValidator : public Validator {
diff --git a/src/validator/SequentialValidator.cpp b/src/validator/SequentialValidator.cpp
index abc96ed8f8d08064875a8451f96664c98caa2bbb..c9205d3133da5b9bb96534efd16a9fa4fe75bcb3 100644
--- a/src/validator/SequentialValidator.cpp
+++ b/src/validator/SequentialValidator.cpp
@@ -34,7 +34,7 @@ Status SequentialValidator::validateImpl() {
     switch (firstSentence->kind()) {
         case Sentence::Kind::kLimit:
         case Sentence::Kind::kOrderBy:
-        case Sentence::Kind::KGroupBy:
+        case Sentence::Kind::kGroupBy:
             return Status::SyntaxError("Could not start with the statement: %s",
                                        firstSentence->toString().c_str());
         default:
diff --git a/src/validator/Validator.cpp b/src/validator/Validator.cpp
index c6ff0788b54a22c63d8abfa70c59d5ee6afb99dc..08f93ed92c394dafc9efb3d6ae96e33d00c27c75 100644
--- a/src/validator/Validator.cpp
+++ b/src/validator/Validator.cpp
@@ -27,6 +27,7 @@
 #include "validator/SetValidator.h"
 #include "validator/UseValidator.h"
 #include "validator/YieldValidator.h"
+#include "validator/GroupByValidator.h"
 #include "common/function/FunctionManager.h"
 
 namespace nebula {
@@ -62,6 +63,8 @@ std::unique_ptr<Validator> Validator::makeValidator(Sentence* sentence, QueryCon
             return std::make_unique<OrderByValidator>(sentence, context);
         case Sentence::Kind::kYield:
             return std::make_unique<YieldValidator>(sentence, context);
+        case Sentence::Kind::kGroupBy:
+            return std::make_unique<GroupByValidator>(sentence, context);
         case Sentence::Kind::kCreateSpace:
             return std::make_unique<CreateSpaceValidator>(sentence, context);
         case Sentence::Kind::kCreateTag:
@@ -541,7 +544,7 @@ StatusOr<Value::Type> Validator::deduceExprType(const Expression* expr) const {
                                  static_cast<int64_t>(expr->kind()));
 }
 
-Status Validator::deduceProps(const Expression* expr) {
+Status Validator::deduceProps(const Expression* expr, ExpressionProps& exprProps) {
     switch (expr->kind()) {
         case Expression::Kind::kConstant: {
             break;
@@ -562,21 +565,21 @@ Status Validator::deduceProps(const Expression* expr) {
         case Expression::Kind::kLogicalOr:
         case Expression::Kind::kLogicalXor: {
             auto biExpr = static_cast<const BinaryExpression*>(expr);
-            NG_RETURN_IF_ERROR(deduceProps(biExpr->left()));
-            NG_RETURN_IF_ERROR(deduceProps(biExpr->right()));
+            NG_RETURN_IF_ERROR(deduceProps(biExpr->left(), exprProps));
+            NG_RETURN_IF_ERROR(deduceProps(biExpr->right(), exprProps));
             break;
         }
         case Expression::Kind::kUnaryPlus:
         case Expression::Kind::kUnaryNegate:
         case Expression::Kind::kUnaryNot: {
             auto unaryExpr = static_cast<const UnaryExpression*>(expr);
-            NG_RETURN_IF_ERROR(deduceProps(unaryExpr->operand()));
+            NG_RETURN_IF_ERROR(deduceProps(unaryExpr->operand(), exprProps));
             break;
         }
         case Expression::Kind::kFunctionCall: {
             auto funcExpr = static_cast<const FunctionCallExpression*>(expr);
             for (auto& arg : funcExpr->args()->args()) {
-                NG_RETURN_IF_ERROR(deduceProps(arg.get()));
+                NG_RETURN_IF_ERROR(deduceProps(arg.get(), exprProps));
             }
             break;
         }
@@ -584,24 +587,21 @@ Status Validator::deduceProps(const Expression* expr) {
             auto* tagPropExpr = static_cast<const SymbolPropertyExpression*>(expr);
             auto status = qctx_->schemaMng()->toTagID(space_.id, *tagPropExpr->sym());
             NG_RETURN_IF_ERROR(status);
-            auto& props = dstTagProps_[status.value()];
-            props.emplace_back(*tagPropExpr->prop());
+            exprProps.insertDstTagProp(status.value(), *tagPropExpr->prop());
             break;
         }
         case Expression::Kind::kSrcProperty: {
             auto* tagPropExpr = static_cast<const SymbolPropertyExpression*>(expr);
             auto status = qctx_->schemaMng()->toTagID(space_.id, *tagPropExpr->sym());
             NG_RETURN_IF_ERROR(status);
-            auto& props = srcTagProps_[status.value()];
-            props.emplace_back(*tagPropExpr->prop());
+            exprProps.insertSrcTagProp(status.value(), *tagPropExpr->prop());
             break;
         }
         case Expression::Kind::kTagProperty: {
             auto* tagPropExpr = static_cast<const SymbolPropertyExpression*>(expr);
             auto status = qctx_->schemaMng()->toTagID(space_.id, *tagPropExpr->sym());
             NG_RETURN_IF_ERROR(status);
-            auto& props = tagProps_[status.value()];
-            props.emplace_back(*tagPropExpr->prop());
+            exprProps.insertTagProp(status.value(), *tagPropExpr->prop());
             break;
         }
         case Expression::Kind::kEdgeProperty:
@@ -612,27 +612,22 @@ Status Validator::deduceProps(const Expression* expr) {
             auto* edgePropExpr = static_cast<const SymbolPropertyExpression*>(expr);
             auto status = qctx_->schemaMng()->toEdgeType(space_.id, *edgePropExpr->sym());
             NG_RETURN_IF_ERROR(status);
-            auto& props = edgeProps_[status.value()];
-            props.emplace_back(*edgePropExpr->prop());
+            exprProps.insertEdgeProp(status.value(), *edgePropExpr->prop());
             break;
         }
         case Expression::Kind::kInputProperty: {
             auto* inputPropExpr = static_cast<const SymbolPropertyExpression*>(expr);
-            auto* prop = inputPropExpr->prop();
-            inputProps_.emplace_back(*prop);
+            exprProps.insertInputProp(*inputPropExpr->prop());
             break;
         }
         case Expression::Kind::kVarProperty: {
             auto* varPropExpr = static_cast<const SymbolPropertyExpression*>(expr);
-            auto* var = varPropExpr->sym();
-            auto* prop = varPropExpr->prop();
-            auto& props = varProps_[*var];
-            props.emplace_back(*prop);
+            exprProps.insertVarProp(*varPropExpr->sym(), *varPropExpr->prop());
             break;
         }
         case Expression::Kind::kTypeCasting: {
             auto* typeCastExpr = static_cast<const TypeCastingExpression*>(expr);
-            NG_RETURN_IF_ERROR(deduceProps(typeCastExpr->operand()));
+            NG_RETURN_IF_ERROR(deduceProps(typeCastExpr->operand(), exprProps));
             break;
         }
         case Expression::Kind::kUUID:
@@ -730,21 +725,21 @@ bool Validator::evaluableExpr(const Expression* expr) const {
 
 // static
 Status Validator::checkPropNonexistOrDuplicate(const ColsDef& cols,
-                                               const std::string& prop,
-                                               const std::string &validatorName) {
-    auto eq = [&](const ColDef& col) { return col.first == prop; };
+                                               const folly::StringPiece& prop,
+                                               const std::string& validatorName) {
+    auto eq = [&](const ColDef& col) { return col.first == prop.str(); };
     auto iter = std::find_if(cols.cbegin(), cols.cend(), eq);
     if (iter == cols.cend()) {
         return Status::SemanticError("%s: prop `%s' not exists",
                                       validatorName.c_str(),
-                                      prop.c_str());
+                                      prop.str().c_str());
     }
 
     iter = std::find_if(iter + 1, cols.cend(), eq);
     if (iter != cols.cend()) {
         return Status::SemanticError("%s: duplicate prop `%s'",
                                       validatorName.c_str(),
-                                      prop.c_str());
+                                      prop.str().c_str());
     }
 
     return Status::OK();
@@ -781,5 +776,94 @@ StatusOr<std::string> Validator::checkRef(const Expression* ref, Value::Type typ
     }
 }
 
-}  // namespace graph
-}  // namespace nebula
+void ExpressionProps::insertVarProp(const std::string& varName, folly::StringPiece prop) {
+    auto& props = varProps_[varName];
+    props.emplace(prop);
+}
+
+void ExpressionProps::insertInputProp(folly::StringPiece prop) {
+    inputProps_.emplace(prop);
+}
+
+void ExpressionProps::insertSrcTagProp(TagID tagId, folly::StringPiece prop) {
+    auto& props = srcTagProps_[tagId];
+    props.emplace(prop);
+}
+
+void ExpressionProps::insertDstTagProp(TagID tagId, folly::StringPiece prop) {
+    auto& props = dstTagProps_[tagId];
+    props.emplace(prop);
+}
+
+void ExpressionProps::insertEdgeProp(EdgeType edgeType, folly::StringPiece prop) {
+    auto& props = edgeProps_[edgeType];
+    props.emplace(prop);
+}
+
+void ExpressionProps::insertTagProp(TagID tagId, folly::StringPiece prop) {
+    auto& props = tagProps_[tagId];
+    props.emplace(prop);
+}
+
+bool ExpressionProps::isSubsetOfInput(const std::set<folly::StringPiece>& props) {
+    for (auto& prop : props) {
+        if (inputProps_.find(prop) == inputProps_.end()) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool ExpressionProps::isSubsetOfVar(const VarPropMap& props) {
+    for (auto &iter : props) {
+        if (varProps_.find(iter.first) == varProps_.end()) {
+            return false;
+        }
+        for (auto& prop : iter.second) {
+            if (varProps_[iter.first].find(prop) == varProps_[iter.first].end()) {
+                return false;
+            }
+        }
+    }
+    return true;
+}
+
+void ExpressionProps::unionProps(ExpressionProps exprProps) {
+    if (!exprProps.inputProps().empty()) {
+        inputProps_.insert(std::make_move_iterator(exprProps.inputProps().begin()),
+                           std::make_move_iterator(exprProps.inputProps().end()));
+    }
+    if (!exprProps.srcTagProps().empty()) {
+        for (auto& iter : exprProps.srcTagProps()) {
+            srcTagProps_[iter.first].insert(std::make_move_iterator(iter.second.begin()),
+                                            std::make_move_iterator(iter.second.end()));
+        }
+    }
+    if (!exprProps.dstTagProps().empty()) {
+        for (auto& iter : exprProps.dstTagProps()) {
+            dstTagProps_[iter.first].insert(std::make_move_iterator(iter.second.begin()),
+                                            std::make_move_iterator(iter.second.end()));
+        }
+    }
+    if (!exprProps.tagProps().empty()) {
+        for (auto& iter : exprProps.tagProps()) {
+            tagProps_[iter.first].insert(std::make_move_iterator(iter.second.begin()),
+                                         std::make_move_iterator(iter.second.end()));
+        }
+    }
+    if (!exprProps.varProps().empty()) {
+        for (auto& iter : exprProps.varProps()) {
+            varProps_[iter.first].insert(std::make_move_iterator(iter.second.begin()),
+                                         std::make_move_iterator(iter.second.end()));
+        }
+    }
+    if (!exprProps.edgeProps().empty()) {
+        for (auto& iter : exprProps.edgeProps()) {
+            edgeProps_[iter.first].insert(std::make_move_iterator(iter.second.begin()),
+                                          std::make_move_iterator(iter.second.end()));
+        }
+    }
+}
+}   // namespace graph
+}   // namespace nebula
+
diff --git a/src/validator/Validator.h b/src/validator/Validator.h
index 70c818c45590ff9521092e5540d6944b858a598b..99cb67edc86bd9d2fc0c6e6161a6807da30b4e0f 100644
--- a/src/validator/Validator.h
+++ b/src/validator/Validator.h
@@ -19,6 +19,58 @@ class YieldColumns;
 
 namespace graph {
 
+class ExpressionProps final {
+public:
+    using TagIDPropsMap =  std::unordered_map<TagID, std::set<folly::StringPiece>>;
+    using EdgePropMap = std::unordered_map<EdgeType, std::set<folly::StringPiece>>;
+    using VarPropMap = std::unordered_map<std::string, std::set<folly::StringPiece>>;
+
+    void insertInputProp(folly::StringPiece prop);
+
+    void insertVarProp(const std::string& varName, folly::StringPiece prop);
+
+    void insertSrcTagProp(TagID tagId, folly::StringPiece prop);
+
+    void insertDstTagProp(TagID tagId, folly::StringPiece prop);
+
+    void insertEdgeProp(EdgeType edgeType, folly::StringPiece prop);
+
+    void insertTagProp(TagID tagId, folly::StringPiece prop);
+
+    std::set<folly::StringPiece>& inputProps() {
+        return inputProps_;
+    }
+    TagIDPropsMap& srcTagProps() {
+        return srcTagProps_;
+    }
+    TagIDPropsMap& dstTagProps() {
+        return dstTagProps_;
+    }
+    TagIDPropsMap& tagProps() {
+        return tagProps_;
+    }
+    EdgePropMap& edgeProps() {
+        return edgeProps_;
+    }
+    VarPropMap& varProps() {
+        return varProps_;
+    }
+
+    bool isSubsetOfInput(const std::set<folly::StringPiece>& props);
+
+    bool isSubsetOfVar(const VarPropMap& props);
+
+    void unionProps(ExpressionProps exprProps);
+
+private:
+    std::set<folly::StringPiece>  inputProps_;
+    VarPropMap                    varProps_;
+    TagIDPropsMap                 srcTagProps_;
+    TagIDPropsMap                 dstTagProps_;
+    EdgePropMap                   edgeProps_;
+    TagIDPropsMap                 tagProps_;
+};
+
 class Validator {
 public:
     virtual ~Validator() = default;
@@ -82,12 +134,12 @@ protected:
 
     StatusOr<Value::Type> deduceExprType(const Expression* expr) const;
 
-    Status deduceProps(const Expression* expr);
+    Status deduceProps(const Expression* expr, ExpressionProps& exprProps);
 
     bool evaluableExpr(const Expression* expr) const;
 
     static Status checkPropNonexistOrDuplicate(const ColsDef& cols,
-                                               const std::string& prop,
+                                               const folly::StringPiece& prop,
                                                const std::string &validatorName);
 
     static Status appendPlan(PlanNode* plan, PlanNode* appended);
@@ -111,14 +163,6 @@ protected:
     std::string                     inputVarName_;
     // Admin sentences do not requires a space to be chosen.
     bool                            noSpaceRequired_{false};
-
-    // properties
-    std::vector<std::string> inputProps_;
-    std::unordered_map<std::string, std::vector<std::string>> varProps_;
-    std::unordered_map<TagID, std::vector<std::string>> srcTagProps_;
-    std::unordered_map<TagID, std::vector<std::string>> dstTagProps_;
-    std::unordered_map<EdgeType, std::vector<std::string>> edgeProps_;
-    std::unordered_map<TagID, std::vector<std::string>> tagProps_;
 };
 
 }  // namespace graph
diff --git a/src/validator/YieldValidator.cpp b/src/validator/YieldValidator.cpp
index 25b9896ada7f67ee67b8c74481b668269873144b..c19aa601ebbd1a0797286ab1c3c21df81dd4ddbb 100644
--- a/src/validator/YieldValidator.cpp
+++ b/src/validator/YieldValidator.cpp
@@ -23,15 +23,16 @@ Status YieldValidator::validateImpl() {
     NG_RETURN_IF_ERROR(validateYieldAndBuildOutputs(yield->yield()));
     NG_RETURN_IF_ERROR(validateWhere(yield->where()));
 
-    if (!srcTagProps_.empty() || !dstTagProps_.empty() || !edgeProps_.empty()) {
+    if (!exprProps_.srcTagProps().empty() || !exprProps_.dstTagProps().empty() ||
+        !exprProps_.edgeProps().empty()) {
         return Status::SemanticError("Only support input and variable in yield sentence.");
     }
 
-    if (!inputProps_.empty() && !varProps_.empty()) {
+    if (!exprProps_.inputProps().empty() && !exprProps_.varProps().empty()) {
         return Status::SemanticError("Not support both input and variable.");
     }
 
-    if (!varProps_.empty() && varProps_.size() > 1) {
+    if (!exprProps_.varProps().empty() && exprProps_.varProps().size() > 1) {
         return Status::SemanticError("Only one variable allowed to use.");
     }
 
@@ -64,10 +65,11 @@ Status YieldValidator::checkAggFunAndBuildGroupItems(const YieldClause *clause)
 }
 
 Status YieldValidator::checkInputProps() const {
-    if (inputs_.empty() && !inputProps_.empty()) {
+    auto& inputProps = const_cast<ExpressionProps*>(&exprProps_)->inputProps();
+    if (inputs_.empty() && !inputProps.empty()) {
         return Status::SemanticError("no inputs for yield columns.");
     }
-    for (auto &prop : inputProps_) {
+    for (auto &prop : inputProps) {
         DCHECK_NE(prop, "*");
         NG_RETURN_IF_ERROR(checkPropNonexistOrDuplicate(inputs_, prop, "Yield sentence"));
     }
@@ -75,7 +77,8 @@ Status YieldValidator::checkInputProps() const {
 }
 
 Status YieldValidator::checkVarProps() const {
-    for (auto &pair : varProps_) {
+    auto& varProps = const_cast<ExpressionProps*>(&exprProps_)->varProps();
+    for (auto &pair : varProps) {
         auto &var = pair.first;
         if (!vctx_->existVar(var)) {
             return Status::SemanticError("variable `%s' not exist.", var.c_str());
@@ -94,7 +97,7 @@ Status YieldValidator::makeOutputColumn(YieldColumn *column) {
 
     auto expr = column->expr();
     DCHECK(expr != nullptr);
-    NG_RETURN_IF_ERROR(deduceProps(expr));
+    NG_RETURN_IF_ERROR(deduceProps(expr, exprProps_));
 
     auto status = deduceExprType(expr);
     NG_RETURN_IF_ERROR(status);
@@ -168,7 +171,7 @@ Status YieldValidator::validateWhere(const WhereClause *clause) {
         filter = clause->filter();
     }
     if (filter != nullptr) {
-        NG_RETURN_IF_ERROR(deduceProps(filter));
+        NG_RETURN_IF_ERROR(deduceProps(filter, exprProps_));
     }
     return Status::OK();
 }
@@ -202,9 +205,9 @@ Status YieldValidator::toPlan() {
         tail_ = dedupDep;
     }
 
-    if (!varProps_.empty()) {
-        DCHECK_EQ(varProps_.size(), 1u);
-        auto var = varProps_.cbegin()->first;
+    if (!exprProps_.varProps().empty()) {
+        DCHECK_EQ(exprProps_.varProps().size(), 1u);
+        auto var = exprProps_.varProps().cbegin()->first;
         static_cast<SingleInputNode *>(tail_)->setInputVar(var);
     }
 
diff --git a/src/validator/YieldValidator.h b/src/validator/YieldValidator.h
index e436329c58c2789b0fa48cfefa7c380cc38c9a64..61fc9eccf5b4afd6b0c055615388bd6d1da56f56 100644
--- a/src/validator/YieldValidator.h
+++ b/src/validator/YieldValidator.h
@@ -43,9 +43,11 @@ private:
     Status makeOutputColumn(YieldColumn *column);
 
     bool hasAggFun_{false};
+
     YieldColumns *columns_{nullptr};
     std::vector<std::string> outputColumnNames_;
     std::vector<Aggregate::GroupItem> groupItems_;
+    ExpressionProps  exprProps_;
 };
 
 }   // namespace graph
diff --git a/src/validator/test/CMakeLists.txt b/src/validator/test/CMakeLists.txt
index f5c32cf70461fa65523fcfa35684a7ce90b186a7..66878617e4c980c203083315a68928c0f134e005 100644
--- a/src/validator/test/CMakeLists.txt
+++ b/src/validator/test/CMakeLists.txt
@@ -60,6 +60,7 @@ nebula_add_test(
         MutateValidatorTest.cpp
         YieldValidatorTest.cpp
         ExplainValidatorTest.cpp
+        GroupByValidatorTest.cpp
     OBJECTS ${VALIDATOR_TEST_LIBS}
     LIBRARIES
         gtest
diff --git a/src/validator/test/GroupByValidatorTest.cpp b/src/validator/test/GroupByValidatorTest.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..ac7f1bdb6860ac6fb6a4fa2b8fb37a7d29db387a
--- /dev/null
+++ b/src/validator/test/GroupByValidatorTest.cpp
@@ -0,0 +1,271 @@
+/* Copyright (c) 2020 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 "validator/GroupByValidator.h"
+
+#include "validator/test/ValidatorTestBase.h"
+
+namespace nebula {
+namespace graph {
+
+class GroupByValidatorTest : public ValidatorTestBase {
+public:
+};
+
+using PK = nebula::graph::PlanNode::Kind;
+
+TEST_F(GroupByValidatorTest, TestGroupBy) {
+    {
+        std::string query =
+            "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+            "| GROUP BY $-.age YIELD COUNT($-.id)";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kAggregate,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query =
+            "GO FROM \"NoExist\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+            "| GROUP BY $-.id YIELD $-.id AS id";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kAggregate,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        std::string query = "GO FROM \"1\", \"2\" OVER like "
+                            "YIELD $$.person.name as name, "
+                            "$$.person.age AS dst_age, "
+                            "$$.person.age AS src_age"
+                            "| GROUP BY $-.name "
+                            "YIELD $-.name AS name, "
+                            "SUM($-.dst_age) AS sum_dst_age, "
+                            "AVG($-.dst_age) AS avg_dst_age, "
+                            "MAX($-.src_age) AS max_src_age, "
+                            "MIN($-.src_age) AS min_src_age, "
+                            "STD($-.src_age) AS std_src_age, "
+                            "BIT_AND(1) AS bit_and, "
+                            "BIT_OR(2) AS bit_or, "
+                            "BIT_XOR(3) AS bit_xor";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kAggregate,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetVertices,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        // group one col
+        std::string query = "GO FROM \"1\" OVER like "
+                            "YIELD $$.person.name as name, "
+                            "like._dst AS id, "
+                            "like.start AS start_year, "
+                            "like.end AS end_year "
+                            "| GROUP BY $-.start_year "
+                            "YIELD COUNT($-.id), "
+                            "$-.start_year AS start_year, "
+                            "AVG($-.end_year) AS avg";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kAggregate,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetVertices,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        // group has fun col
+        std::string query = "GO FROM \"1\" OVER like "
+                            "YIELD $$.person.name as name, "
+                            "like._dst AS id, "
+                            "like.start AS start_year, "
+                            "like.end AS end_year"
+                            "| GROUP BY $-.name, abs(5) "
+                            "YIELD $-.name AS name, "
+                            "SUM(1.5) AS sum, "
+                            "COUNT(*) AS count, "
+                            "1+1 AS cal";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kAggregate,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetVertices,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+    {
+        // group has fun col
+        std::string query = "GO FROM \"1\" OVER like "
+                            "YIELD $$.person.name as name, "
+                            "like._dst AS id, "
+                            "like.start AS start_year, "
+                            "like.end AS end_year"
+                            "| GROUP BY $-.name, $-.id "
+                            "YIELD $-.name AS name, "
+                            "SUM(1.5) AS sum, "
+                            "COUNT(*) AS count, "
+                            "1+1 AS cal";
+        std::vector<PlanNode::Kind> expected = {
+            PK::kAggregate,
+            PK::kProject,
+            PK::kDataJoin,
+            PK::kProject,
+            PK::kGetVertices,
+            PK::kProject,
+            PK::kGetNeighbors,
+            PK::kStart
+        };
+        EXPECT_TRUE(checkResult(query, expected));
+    }
+}
+
+
+TEST_F(GroupByValidatorTest, InvalidTest) {
+    {
+        // use groupby without input
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY 1+1 YIELD COUNT(1), 1+1";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()), "SemanticError: Group `(1+1)` invalid");
+    }
+    {
+        // src
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY $-.age YIELD COUNT($var)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()), "SyntaxError: syntax error near `)'");
+    }
+    {
+        // use dst
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY $-.age YIELD COUNT($$.person.name)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: Only support input and variable in GroupBy sentence.");
+    }
+    {
+        // group input noexist
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY $-.start_year YIELD COUNT($-.age)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: `$-.start_year', not exist prop `start_year'");
+    }
+    {
+        // group name noexist
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY noexist YIELD COUNT($-.age)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()), "SemanticError: Group `noexist` invalid");
+    }
+    {
+        // use sum(*)
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY $-.id YIELD SUM(*)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: `SUM(*)` invaild, * valid in count.");
+    }
+    {
+        // use agg fun has more than two inputs
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY $-.id YIELD COUNT($-.id, $-.age)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()), "SyntaxError: syntax error near `, $-.age'");
+    }
+    {
+        // group col has agg fun
+        std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age "
+                            "| GROUP BY $-.id, SUM($-.age) YIELD $-.id, SUM($-.age)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()), "SemanticError: Use invalid group function `SUM`");
+    }
+    {
+        // yield without group by
+        std::string query = "GO FROM \"1\" OVER like YIELD $^.person.age AS age, "
+                            "COUNT(like._dst) AS id ";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: `COUNT(like._dst) AS id', not support "
+                  "aggregate function in go sentence.");
+    }
+     {
+        // yield col not in group output
+        std::string query = "GO FROM \"1\" OVER like "
+                            "YIELD $$.person.name as name, "
+                            "like._dst AS id, "
+                            "like.start AS start_year, "
+                            "like.end AS end_year"
+                            "| GROUP BY $-.start_year, abs(5) "
+                            "YIELD $-.name AS name, "
+                            "SUM(1.5) AS sum, "
+                            "COUNT(*) AS count, "
+                            "1+1 AS cal";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: Yield `$-.name AS name` isn't in output fields");
+    }
+    {
+        // duplicate col
+        std::string query =
+            "GO FROM \"1\" OVER like YIELD $$.person.age AS age, $^.person.age AS age"
+            "| GROUP BY $-.age YIELD $-.age, 1+1";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: GroupBy sentence: duplicate prop `age'");
+    }
+    {
+        // duplicate col
+        std::string query = "GO FROM \"1\" OVER like "
+                            "YIELD $$.person.age AS age, $^.person.age AS age, like._dst AS id "
+                            "| GROUP BY $-.id YIELD $-.id, COUNT($-.age)";
+        auto result = checkResult(query);
+        EXPECT_EQ(std::string(result.message()),
+                  "SemanticError: GroupBy sentence: duplicate prop `age'");
+    }
+
+    // {
+    //     // todo(jmq) not support $-.*
+    //     std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age
+    //                         "
+    //                         "| GROUP BY $-.id YIELD  COUNT($-.*)";
+    //     auto result = checkResult(query);
+    //     EXPECT_EQ(std::string(result.message()), "SemanticError: Use invalid group function
+    //     `SUM`");
+    // }
+    // {
+    //     //  todo(jmq) not support $-.*
+    //     std::string query = "GO FROM \"1\" OVER like YIELD like._dst AS id, $^.person.age AS age
+    //                         "
+    //                         "| GROUP BY $-.* YIELD  $-.*";
+    //     auto result = checkResult(query);
+    //     EXPECT_EQ(std::string(result.message()), "SemanticError: Use invalid group function
+    //     `SUM`");
+    // }
+}
+
+}  // namespace graph
+}  // namespace nebula
diff --git a/tests/query/stateless/test_new_groupby.py b/tests/query/stateless/test_new_groupby.py
new file mode 100644
index 0000000000000000000000000000000000000000..c2427c88136d90f7545d25c3e8b9fd35fcf1be48
--- /dev/null
+++ b/tests/query/stateless/test_new_groupby.py
@@ -0,0 +1,367 @@
+# --coding:utf-8--
+#
+# Copyright (c) 2020 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.
+
+from tests.common.nebula_test_suite import NebulaTestSuite
+from tests.common.nebula_test_suite import T_EMPTY, T_NULL
+import pytest
+
+class TestGroupBy(NebulaTestSuite):
+    @classmethod
+    def prepare(self):
+        self.load_data()
+
+    def cleanup():
+        pass
+
+
+    def test_syntax_error(self):
+        # Use groupby without input
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name
+                | GROUP BY 1+1 YIELD COUNT(1), 1+1 '''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # use var
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve.end_year AS end_year | GROUP BY $-.start_year YIELD COUNT($var) '''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # use dst
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve.end_year AS end_year | GROUP BY $-.start_year YIELD COUNT($$.team.name) '''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # groupby input noexist
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve._dst AS id | GROUP BY $-.start_year YIELD COUNT($-.id) '''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # group alias noexist
+        # stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+        #         serve._dst AS id | GROUP BY team YIELD COUNT($-.id), $-.name AS teamName '''
+        # resp = self.execute_query(stmt)
+        # self.check_resp_failed(resp)
+
+        # Field nonexistent
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve._dst AS id | GROUP BY $-.name YIELD COUNT($-.start_year) '''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # use sum(*)
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve._dst AS id | GROUP BY $-.name YIELD SUM(*) '''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # use agg fun has more than one inputs
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve._dst AS id | GROUP BY $-.name YIELD COUNT($-.name, $-.id)'''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # group col has agg fun
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                serve._dst AS id | GROUP BY $-.name, SUM($-.id) YIELD $-.name,  SUM($-.id)'''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        # yield without group by
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve YIELD $$.team.name AS name,
+                COUNT(serve._dst) AS id'''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+    def test_group_by(self):
+        stmt = '''GO FROM 'Aron Baynes', 'Tracy McGrady' OVER serve
+                YIELD $$.team.name AS name,
+                serve._dst AS id,
+                serve.start_year AS start_year,
+                serve.end_year AS end_year
+                | GROUP BY $-.name, $-.start_year
+                YIELD $-.name AS teamName,
+                $-.start_year AS start_year,
+                MAX($-.start_year),
+                MIN($-.end_year),
+                AVG($-.end_year) AS avg_end_year,
+                STD($-.end_year) AS std_end_year,
+                COUNT($-.id)'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["teamName", "start_year", "MAX($-.start_year)", "MIN($-.end_year)", "avg_end_year", "std_end_year", "COUNT($-.id)"],
+            "rows" : [
+                ["Celtics", 2017, 2017, 2019, 2019.0, 0.0, 1],
+                ["Magic", 2000, 2000, 2004, 2004.0, 0.0, 1],
+                ["Pistons", 2015, 2015, 2017, 2017.0, 0.0, 1],
+                ["Raptors", 1997, 1997, 2000, 2000.0, 0.0, 1],
+                ["Rockets", 2004, 2004, 2010, 2010.0, 0.0, 1],
+                ["Spurs", 2013, 2013, 2013, 2014.0, 1.0, 2]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # group one col
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve
+                YIELD $$.team.name AS name,
+                serve._dst AS id,
+                serve.start_year AS start_year,
+                serve.end_year AS end_year
+                | GROUP BY $-.start_year
+                YIELD COUNT($-.id),
+                $-.start_year AS start_year,
+                AVG($-.end_year) as avg'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["COUNT($-.id)", "start_year", "avg"],
+            "rows" : [
+                [2, 2018, 2018.5],
+                [1, 2017, 2018.0],
+                [1, 2016, 2017.0],
+                [1, 2009, 2010.0],
+                [1, 2007, 2009.0],
+                [1, 2012, 2013.0],
+                [1, 2015, 2016.0]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # group by aliasName   not implement
+        # stmt = '''GO FROM 'Aron Baynes', 'Tracy McGrady' OVER serve YIELD $$.team.name AS name,
+        #         serve._dst AS id, serve.start_year AS start_year, serve.end_year AS end_year
+        #         | GROUP BY teamName, start_year YIELD $-.name AS teamName, $-.start_year AS start_year,
+        #         MAX($-.start), MIN($-.end), AVG($-.end) AS avg_end_year, STD($-.end) AS std_end_year,
+        #         COUNT($-.id)'''
+        # resp = self.execute_query(stmt)
+        # self.check_resp_succeeded(resp)
+        # expected_data = {
+        #     "column_names" : ["teamName", "start_year", "MAX(%-.start)", "MIN($-.end)", "avg_end_year", "std_end_year", "COUNT($-.id)"],
+        #     "rows" : [
+        #         ["Celtics", 2017, 2017, 2019, 2019.0, 0, 1],
+        #         ["Magic", 2000, 2000, 2004, 2004.0, 0, 1],
+        #         ["Pistons", 2015, 2015, 2017, 2017.0, 0, 1],
+        #         ["Raptors", 1997, 1997, 2000, 2000.0, 0, 1],
+        #         ["Rockets", 2004, 2004, 2010, 2010.0, 0, 1],
+        #         ["Spurs", 2013, 2013, 2013, 2014.0, 1, 2]
+        #     ]
+        # }
+        # self.check_column_names(resp, expected_data["column_names"])
+        # self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # count(distinct) not implement
+        # stmt = '''GO FROM 'Carmelo Anthony', 'Dwyane Wade' OVER like
+        #         YIELD $$.player.name AS name,
+        #         $$.player.age AS dst_age,
+        #         $$.player.age AS src_age,
+        #         like.likeness AS likeness
+        #         | GROUP BY $-.name
+        #         YIELD $-.name AS name,
+        #         SUM($-.dst_age) AS sum_dst_age,
+        #         AVG($-.dst_age) AS avg_dst_age,
+        #         MAX($-.src_age) AS max_src_age,
+        #         MIN($-.src_age) AS min_src_age,
+        #         BIT_AND(1) AS bit_and,
+        #         BIT_OR(2) AS bit_or,
+        #         BIT_XOR(3) AS bit_xor,
+        #         COUNT($-.likeness),
+        #         COUNT_DISTINCT($-.likeness)'''
+        # resp = self.execute_query(stmt)
+        # self.check_resp_succeeded(resp)
+        # expected_data = {
+        #     "column_names" : ["name", "sum_dst_age", "avg_dst_age", "max_src_age", "min_src_age", "bit_and",
+        #                       "bit_or", "bit_xor", "COUNT($-.likeness)", "COUNT_DISTINCT($-.likeness)"],
+        #     "rows" : [
+        #         ["LeBron James", 68, 34.0, 34, 34, 1, 2, 0, 2, 1],
+        #         ["Chris Paul", 66, 33.0, 33, 33, 1, 2, 0, 2, 1],
+        #         ["Dwyane Wade", 37, 37.0, 37, 37, 1, 2, 3, 1, 1],
+        #         ["Carmelo Anthony", 34, 34.0, 34, 34, 1, 2, 3, 1, 1]
+        #     ]
+        # }
+        # self.check_column_names(resp, expected_data["column_names"])
+        # self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # group has all agg fun
+        stmt = '''GO FROM 'Carmelo Anthony', 'Dwyane Wade' OVER like
+                YIELD $$.player.name AS name,
+                $$.player.age AS dst_age,
+                $$.player.age AS src_age,
+                like.likeness AS likeness
+                | GROUP BY $-.name
+                YIELD $-.name AS name,
+                SUM($-.dst_age) AS sum_dst_age,
+                AVG($-.dst_age) AS avg_dst_age,
+                MAX($-.src_age) AS max_src_age,
+                MIN($-.src_age) AS min_src_age,
+                BIT_AND(1) AS bit_and,
+                BIT_OR(2) AS bit_or,
+                BIT_XOR(3) AS bit_xor,
+                COUNT($-.likeness)'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["name", "sum_dst_age", "avg_dst_age", "max_src_age", "min_src_age", "bit_and",
+                              "bit_or", "bit_xor", "COUNT($-.likeness)"],
+            "rows" : [
+                ["LeBron James", 68, 34.0, 34, 34, 1, 2, 0, 2],
+                ["Chris Paul", 66, 33.0, 33, 33, 1, 2, 0, 2],
+                ["Dwyane Wade", 37, 37.0, 37, 37, 1, 2, 3, 1],
+                ["Carmelo Anthony", 34, 34.0, 34, 34, 1, 2, 3, 1]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # group has fun col
+        stmt = '''GO FROM 'Carmelo Anthony', 'Dwyane Wade' OVER like
+                YIELD $$.player.name AS name
+                | GROUP BY $-.name, abs(5)
+                YIELD $-.name AS name,
+                SUM(1.5) AS sum,
+                COUNT(*) AS count,
+                1+1 AS cal'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["name", "sum", "count", "cal"],
+            "rows" : [
+                ["LeBron James", 3.0, 2, 2],
+                ["Chris Paul", 3.0, 2, 2],
+                ["Dwyane Wade", 1.5, 1, 2],
+                ["Carmelo Anthony", 1.5, 1, 2]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # output next
+        stmt = '''GO FROM 'Paul Gasol' OVER like
+                YIELD $$.player.age AS age,
+                like._dst AS id
+                | GROUP BY $-.id
+                YIELD $-.id AS id,
+                SUM($-.age) AS age
+                | GO FROM $-.id OVER serve
+                YIELD $$.team.name AS name,
+                $-.age AS sumAge'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["name", "sumAge"],
+            "rows" : [
+                ["Grizzlies", 34],
+                ["Raptors", 34],
+                ["Lakers", 40]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])
+
+    def test_empty_input(self):
+        stmt = '''GO FROM 'noexist' OVER like
+                YIELD $$.player.name AS name
+                | GROUP BY $-.name, abs(5)
+                YIELD $-.name AS name,
+                SUM(1.5) AS sum,
+                COUNT(*) AS count
+                | ORDER BY $-.sum | LIMIT 2'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        self.check_empty_result(resp)
+
+        stmt = '''GO FROM 'noexist' OVER serve
+                YIELD $^.player.name as name,
+                serve.start_year as start,
+                $$.team.name as team
+                | YIELD $-.name as name
+                WHERE $-.start > 20000
+                | GROUP BY $-.name
+                YIELD $-.name AS name'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        self.check_empty_result(resp)
+
+        stmt = '''GO FROM 'noexist' OVER serve
+                YIELD $^.player.name as name,
+                serve.start_year as start,
+                $$.team.name as team
+                | YIELD $-.name as name
+                WHERE $-.start > 20000
+                | Limit 1'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        self.check_empty_result(resp)
+
+    def test_duplicate_column(self):
+        stmt = '''GO FROM 'Marco Belinelli' OVER serve
+                YIELD $$.team.name AS name,
+                serve._dst AS id,
+                serve.start_year AS start_year,
+                serve.end_year AS start_year
+                | GROUP BY $-.start_year
+                YIELD COUNT($-.id),
+                $-.start_year AS start_year,
+                AVG($-.end_year) as avg'''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+        stmt = '''GO FROM 'noexist' OVER serve
+                YIELD $^.player.name as name,
+                serve.start_year as start,
+                $$.team.name as name
+                | GROUP BY $-.name
+                YIELD $-.name AS name'''
+        resp = self.execute_query(stmt)
+        self.check_resp_failed(resp)
+
+    def test_groupby_orderby_limit(self):
+        # with orderby
+        stmt = '''GO FROM 'Carmelo Anthony', 'Dwyane Wade' OVER like
+                YIELD $$.player.name AS name
+                | GROUP BY $-.name, abs(5)
+                YIELD $-.name AS name,
+                SUM(1.5) AS sum,
+                COUNT(*) AS count
+                | ORDER BY $-.sum, $-.name'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["name", "sum", "count"],
+            "rows" : [
+                ["Carmelo Anthony", 1.5, 1],
+                ["Dwyane Wade", 1.5, 1],
+                ["Chris Paul", 3.0, 2],
+                ["LeBron James", 3.0, 2]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])
+
+        # with limit ()
+        stmt = '''GO FROM 'Carmelo Anthony', 'Dwyane Wade' OVER like
+                YIELD $$.player.name AS name
+                | GROUP BY $-.name, abs(5)
+                YIELD $-.name AS name,
+                SUM(1.5) AS sum,
+                COUNT(*) AS count
+                | ORDER BY $-.sum, $-.name  DESC | LIMIT 2'''
+        resp = self.execute_query(stmt)
+        self.check_resp_succeeded(resp)
+        expected_data = {
+            "column_names" : ["name", "sum", "count"],
+            "rows" : [
+                ["Carmelo Anthony", 1.5, 1],
+                ["Dwyane Wade", 1.5, 1]
+            ]
+        }
+        self.check_column_names(resp, expected_data["column_names"])
+        self.check_out_of_order_result(resp, expected_data["rows"])