Skip to content

Commit

Permalink
Show filter condition (#87)
Browse files Browse the repository at this point in the history
* Show Filter in EXPLAIN

* Fix value of LatencyTotal

* Add test case of filter

* Do go mod tidy

* Add comment

* Refactor isPredicate

* Use t.Fatal instead of t.Error

* Do gofmt

Co-authored-by: Yuki Furuyama <furuyama@google.com>
  • Loading branch information
apstndb and yfuruyama authored Sep 23, 2020
1 parent 4d581a5 commit 6af0238
Show file tree
Hide file tree
Showing 4 changed files with 375 additions and 4 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
cloud.google.com/go v0.65.0
cloud.google.com/go/spanner v1.10.0
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/golang/protobuf v1.4.2
github.com/google/go-cmp v0.5.2
github.com/jessevdk/go-flags v1.4.0
github.com/mattn/go-runewidth v0.0.8 // indirect
Expand Down
28 changes: 24 additions & 4 deletions query_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ type executionStatsValue struct {
Total string `json:"total"`
}

func (v executionStatsValue) String() string {
if v.Unit == "" {
return v.Total
} else {
return fmt.Sprintf("%s %s", v.Total, v.Unit)
}
}

// queryPlanNodeWithStatsTyped is proto-free typed representation of QueryPlanNodeWithStats
type queryPlanNodeWithStatsTyped struct {
ID int32 `json:"id"`
Expand Down Expand Up @@ -112,6 +120,19 @@ type QueryPlanRow struct {
Predicates []string
}

func isPredicate(planNodes []*pb.PlanNode, childLink *pb.PlanNode_ChildLink) bool {
// Known predicates are Condition(Filter) or Seek Condition/Residual Condition(FilterScan) or Split Range(Distributed Union).
// Agg is a Function but not a predicate.
child := planNodes[childLink.ChildIndex]
if child.DisplayName != "Function" {
return false
}
if strings.HasSuffix(childLink.GetType(), "Condition") || childLink.GetType() == "Split Range" {
return true
}
return false
}

func (n *Node) RenderTreeWithStats(planNodes []*pb.PlanNode) ([]QueryPlanRow, error) {
tree := treeprint.New()
renderTreeWithStats(tree, "", n)
Expand Down Expand Up @@ -142,11 +163,10 @@ func (n *Node) RenderTreeWithStats(planNodes []*pb.PlanNode) ([]QueryPlanRow, er

var predicates []string
for _, cl := range planNodes[planNode.ID].GetChildLinks() {
child := planNodes[cl.ChildIndex]
if child.DisplayName != "Function" || !(cl.GetType() == "Residual Condition" || cl.GetType() == "Seek Condition" || cl.GetType() == "Split Range") {
if !isPredicate(planNodes, cl) {
continue
}
predicates = append(predicates, fmt.Sprintf("%s: %s", cl.GetType(), child.GetShortRepresentation().GetDescription()))
predicates = append(predicates, fmt.Sprintf("%s: %s", cl.GetType(), planNodes[cl.ChildIndex].GetShortRepresentation().GetDescription()))
}

result = append(result, QueryPlanRow{
Expand All @@ -155,7 +175,7 @@ func (n *Node) RenderTreeWithStats(planNodes []*pb.PlanNode) ([]QueryPlanRow, er
Text: branchText + text,
RowsTotal: planNode.ExecutionStats.Rows.Total,
Execution: planNode.ExecutionStats.ExecutionSummary.NumExecutions,
LatencyTotal: fmt.Sprintf("%s %s", planNode.ExecutionStats.Latency.Total, planNode.ExecutionStats.Latency.Unit),
LatencyTotal: planNode.ExecutionStats.Latency.String(),
})
}
return result, nil
Expand Down
73 changes: 73 additions & 0 deletions query_plan_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package main

import (
"io/ioutil"
"testing"

"github.com/google/go-cmp/cmp"
"google.golang.org/genproto/googleapis/spanner/v1"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/structpb"

pb "google.golang.org/genproto/googleapis/spanner/v1"
Expand All @@ -18,6 +20,77 @@ func mustNewStruct(m map[string]interface{}) *structpb.Struct {
}
}

func TestRenderTreeUsingTestdataPlans(t *testing.T) {
for _, test := range []struct {
title string
file string
want []QueryPlanRow
}{
{
// Original Query:
// SELECT s.LastName FROM (SELECT s.LastName FROM Singers AS s WHERE s.FirstName LIKE 'A%' LIMIT 3) s WHERE s.LastName LIKE 'Rich%';
title: "With Filter Operator",
file: "testdata/plans/filter.input.json",
want: []QueryPlanRow{
{
ID: 0,
Text: "Serialize Result",
},
{
ID: 1,
Text: "+- Filter",
Predicates: []string{"Condition: STARTS_WITH($LastName, 'Rich')"},
},
{
ID: 2,
Text: " +- Global Limit",
},
{
ID: 3,
Text: " +- Distributed Union",
Predicates: []string{"Split Range: STARTS_WITH($FirstName, 'A')"},
},
{
ID: 4,
Text: " +- Local Limit",
},
{
ID: 5,
Text: " +- Local Distributed Union",
},
{
ID: 6,
Text: " +- FilterScan",
Predicates: []string{"Seek Condition: STARTS_WITH($FirstName, 'A')"},
},
{
ID: 7,
Text: " +- Index Scan (Index: SingersByFirstLastName)",
},
}},
} {
t.Run(test.title, func(t *testing.T) {
b, err := ioutil.ReadFile(test.file)
if err != nil {
t.Fatal(err)
}
var plan pb.QueryPlan
err = protojson.Unmarshal(b, &plan)
if err != nil {
t.Fatal(err)
}
tree := BuildQueryPlanTree(&plan, 0)
got, err := tree.RenderTreeWithStats(plan.GetPlanNodes())
if err != nil {
t.Errorf("error should be nil, but got = %v", err)
}
if !cmp.Equal(test.want, got) {
t.Errorf("node.RenderTreeWithStats() differ: %s", cmp.Diff(test.want, got))
}
})
}
}

func TestRenderTreeWithStats(t *testing.T) {
for _, test := range []struct {
title string
Expand Down
Loading

0 comments on commit 6af0238

Please sign in to comment.