Skip to content

Commit ba20d5a

Browse files
authored
Merge pull request #1610 from ydb-platform/query-stats
refactored query stats: implemetation instead interface
2 parents 152f5fc + 812ea67 commit ba20d5a

File tree

5 files changed

+175
-30
lines changed

5 files changed

+175
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Added immutable range iterators from go1.23 into query stats to iterate over query phases and accessed tables without query stats object mutation
2+
13
## v3.96.2
24
* Fixed broken metric `ydb_go_sdk_ydb_database_sql_conns`
35

internal/stats/query.go

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"time"
55

66
"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_TableStats"
7+
8+
"github.com/ydb-platform/ydb-go-sdk/v3/internal/xiter"
79
)
810

911
type (
@@ -19,12 +21,17 @@ type (
1921
// NextPhase returns next execution phase within query.
2022
// If ok flag is false, then there are no more phases and p is invalid.
2123
NextPhase() (p QueryPhase, ok bool)
24+
25+
// QueryPhases is a range iterator over query phases.
26+
QueryPhases() xiter.Seq[QueryPhase]
2227
}
2328
// QueryPhase holds query execution phase statistics.
2429
QueryPhase interface {
2530
// NextTableAccess returns next accessed table within query execution phase.
2631
// If ok flag is false, then there are no more accessed tables and t is invalid.
2732
NextTableAccess() (t *TableAccess, ok bool)
33+
// TableAccess is a range iterator over query execution phase's accessed tables.
34+
TableAccess() xiter.Seq[*TableAccess]
2835
Duration() time.Duration
2936
CPUTime() time.Duration
3037
AffectedShards() uint64
@@ -86,57 +93,70 @@ func fromOperationStats(pb *Ydb_TableStats.OperationStats) OperationStats {
8693
}
8794
}
8895

89-
func (s *queryStats) ProcessCPUTime() time.Duration {
90-
return fromUs(s.pb.GetProcessCpuTimeUs())
96+
func (stats *queryStats) ProcessCPUTime() time.Duration {
97+
return fromUs(stats.pb.GetProcessCpuTimeUs())
9198
}
9299

93-
func (s *queryStats) Compilation() (c *CompilationStats) {
94-
return fromCompilationStats(s.pb.GetCompilation())
100+
func (stats *queryStats) Compilation() (c *CompilationStats) {
101+
return fromCompilationStats(stats.pb.GetCompilation())
95102
}
96103

97-
func (s *queryStats) QueryPlan() string {
98-
return s.pb.GetQueryPlan()
104+
func (stats *queryStats) QueryPlan() string {
105+
return stats.pb.GetQueryPlan()
99106
}
100107

101-
func (s *queryStats) QueryAST() string {
102-
return s.pb.GetQueryAst()
108+
func (stats *queryStats) QueryAST() string {
109+
return stats.pb.GetQueryAst()
103110
}
104111

105-
func (s *queryStats) TotalCPUTime() time.Duration {
106-
return fromUs(s.pb.GetTotalCpuTimeUs())
112+
func (stats *queryStats) TotalCPUTime() time.Duration {
113+
return fromUs(stats.pb.GetTotalCpuTimeUs())
107114
}
108115

109-
func (s *queryStats) TotalDuration() time.Duration {
110-
return fromUs(s.pb.GetTotalDurationUs())
116+
func (stats *queryStats) TotalDuration() time.Duration {
117+
return fromUs(stats.pb.GetTotalDurationUs())
111118
}
112119

113120
// NextPhase returns next execution phase within query.
114121
// If ok flag is false, then there are no more phases and p is invalid.
115-
func (s *queryStats) NextPhase() (p QueryPhase, ok bool) {
116-
if s.pos >= len(s.pb.GetQueryPhases()) {
122+
func (stats *queryStats) NextPhase() (p QueryPhase, ok bool) {
123+
if stats.pos >= len(stats.pb.GetQueryPhases()) {
117124
return
118125
}
119-
pb := s.pb.GetQueryPhases()[s.pos]
126+
pb := stats.pb.GetQueryPhases()[stats.pos]
120127
if pb == nil {
121128
return
122129
}
123-
s.pos++
130+
stats.pos++
124131

125132
return &queryPhase{
126133
pb: pb,
127134
}, true
128135
}
129136

137+
func (stats *queryStats) QueryPhases() xiter.Seq[QueryPhase] {
138+
return func(yield func(p QueryPhase) bool) {
139+
for _, pb := range stats.pb.GetQueryPhases() {
140+
cont := yield(&queryPhase{
141+
pb: pb,
142+
})
143+
if !cont {
144+
return
145+
}
146+
}
147+
}
148+
}
149+
130150
// NextTableAccess returns next accessed table within query execution phase.
131151
//
132152
// If ok flag is false, then there are no more accessed tables and t is
133153
// invalid.
134-
func (queryPhase *queryPhase) NextTableAccess() (t *TableAccess, ok bool) {
135-
if queryPhase.pos >= len(queryPhase.pb.GetTableAccess()) {
154+
func (phase *queryPhase) NextTableAccess() (t *TableAccess, ok bool) {
155+
if phase.pos >= len(phase.pb.GetTableAccess()) {
136156
return
137157
}
138-
pb := queryPhase.pb.GetTableAccess()[queryPhase.pos]
139-
queryPhase.pos++
158+
pb := phase.pb.GetTableAccess()[phase.pos]
159+
phase.pos++
140160

141161
return &TableAccess{
142162
Name: pb.GetName(),
@@ -147,20 +167,37 @@ func (queryPhase *queryPhase) NextTableAccess() (t *TableAccess, ok bool) {
147167
}, true
148168
}
149169

150-
func (queryPhase *queryPhase) Duration() time.Duration {
151-
return fromUs(queryPhase.pb.GetDurationUs())
170+
func (phase *queryPhase) TableAccess() xiter.Seq[*TableAccess] {
171+
return func(yield func(access *TableAccess) bool) {
172+
for _, pb := range phase.pb.GetTableAccess() {
173+
cont := yield(&TableAccess{
174+
Name: pb.GetName(),
175+
Reads: fromOperationStats(pb.GetReads()),
176+
Updates: fromOperationStats(pb.GetUpdates()),
177+
Deletes: fromOperationStats(pb.GetDeletes()),
178+
PartitionsCount: pb.GetPartitionsCount(),
179+
})
180+
if !cont {
181+
return
182+
}
183+
}
184+
}
185+
}
186+
187+
func (phase *queryPhase) Duration() time.Duration {
188+
return fromUs(phase.pb.GetDurationUs())
152189
}
153190

154-
func (queryPhase *queryPhase) CPUTime() time.Duration {
155-
return fromUs(queryPhase.pb.GetCpuTimeUs())
191+
func (phase *queryPhase) CPUTime() time.Duration {
192+
return fromUs(phase.pb.GetCpuTimeUs())
156193
}
157194

158-
func (queryPhase *queryPhase) AffectedShards() uint64 {
159-
return queryPhase.pb.GetAffectedShards()
195+
func (phase *queryPhase) AffectedShards() uint64 {
196+
return phase.pb.GetAffectedShards()
160197
}
161198

162-
func (queryPhase *queryPhase) IsLiteralPhase() bool {
163-
return queryPhase.pb.GetLiteralPhase()
199+
func (phase *queryPhase) IsLiteralPhase() bool {
200+
return phase.pb.GetLiteralPhase()
164201
}
165202

166203
func FromQueryStats(pb *Ydb_TableStats.QueryStats) QueryStats {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
//go:build go1.23
2+
3+
package stats
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/require"
11+
"github.com/ydb-platform/ydb-go-genproto/protos/Ydb_TableStats"
12+
)
13+
14+
func TestIterateOverQueryPhases(t *testing.T) {
15+
s := FromQueryStats(&Ydb_TableStats.QueryStats{
16+
QueryPhases: []*Ydb_TableStats.QueryPhaseStats{
17+
{
18+
DurationUs: 1,
19+
TableAccess: []*Ydb_TableStats.TableAccessStats{
20+
{
21+
Name: "a",
22+
},
23+
{
24+
Name: "b",
25+
},
26+
{
27+
Name: "c",
28+
},
29+
},
30+
},
31+
{
32+
DurationUs: 2,
33+
TableAccess: []*Ydb_TableStats.TableAccessStats{
34+
{
35+
Name: "d",
36+
},
37+
{
38+
Name: "e",
39+
},
40+
{
41+
Name: "f",
42+
},
43+
},
44+
},
45+
{
46+
DurationUs: 3,
47+
TableAccess: []*Ydb_TableStats.TableAccessStats{
48+
{
49+
Name: "g",
50+
},
51+
{
52+
Name: "h",
53+
},
54+
{
55+
Name: "i",
56+
},
57+
},
58+
},
59+
},
60+
})
61+
t.Run("ImmutableIteration", func(t *testing.T) {
62+
for i := range make([]struct{}, 3) {
63+
t.Run(fmt.Sprintf("Pass#%d", i), func(t *testing.T) {
64+
durations := make([]time.Duration, 0, 3)
65+
tables := make([]string, 0, 9)
66+
for phase := range s.QueryPhases() {
67+
durations = append(durations, phase.Duration())
68+
for access := range phase.TableAccess() {
69+
tables = append(tables, access.Name)
70+
}
71+
}
72+
require.Equal(t, []time.Duration{1000, 2000, 3000}, durations)
73+
require.Equal(t, []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, tables)
74+
})
75+
}
76+
})
77+
t.Run("MutableIteration", func(t *testing.T) {
78+
durations := make([]time.Duration, 0, 3)
79+
tables := make([]string, 0, 9)
80+
for {
81+
phase, ok := s.NextPhase()
82+
if !ok {
83+
break
84+
}
85+
durations = append(durations, phase.Duration())
86+
for {
87+
access, ok := phase.NextTableAccess()
88+
if !ok {
89+
break
90+
}
91+
tables = append(tables, access.Name)
92+
}
93+
}
94+
require.Equal(t, []time.Duration{1000, 2000, 3000}, durations)
95+
require.Equal(t, []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"}, tables)
96+
97+
_, ok := s.NextPhase()
98+
require.False(t, ok)
99+
})
100+
}

internal/xiter/xiter.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@
22

33
package xiter
44

5-
type Seq2[K, V any] func(yield func(K, V) bool)
5+
type (
6+
Seq[T any] func(yield func(T) bool)
7+
Seq2[K, V any] func(yield func(K, V) bool)
8+
)

internal/xiter/xiter_go1.23.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,7 @@ import (
66
"iter"
77
)
88

9-
type Seq2[K, V any] iter.Seq2[K, V]
9+
type (
10+
Seq[T any] iter.Seq[T]
11+
Seq2[K, V any] iter.Seq2[K, V]
12+
)

0 commit comments

Comments
 (0)