Skip to content

Commit 50ce6e9

Browse files
authored
[clickhouse][perf] Restructure ClickHouse FindTraceIDs Query to Improve Performance (#8125)
1 parent 3ead761 commit 50ce6e9

File tree

8 files changed

+274
-243
lines changed

8 files changed

+274
-243
lines changed

internal/storage/v2/clickhouse/sql/queries.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -206,22 +206,29 @@ FROM
206206

207207
const SelectSpansByTraceID = SelectSpansQuery + " WHERE s.trace_id = ?"
208208

209-
// SearchTraceIDs is the base SQL fragment used by FindTraceIDs.
209+
// SearchTraceIDsBase is the inner SQL fragment for finding distinct trace IDs.
210210
//
211211
// The query begins with a no-op predicate (`WHERE 1=1`) so that additional
212212
// filters can be appended unconditionally using `AND` without needing to check
213213
// whether this is the first WHERE clause.
214-
//
215-
// The query joins with trace_id_timestamps to retrieve the start and end times
216-
// for each trace ID.
214+
const SearchTraceIDsBase = `SELECT DISTINCT
215+
s.trace_id
216+
FROM spans s
217+
WHERE 1=1`
218+
219+
// SearchTraceIDs wraps a trace ID subquery with a JOIN to
220+
// trace_id_timestamps to retrieve the start and end times for each trace.
221+
// The %s placeholder is replaced with the complete inner subquery
222+
// (SearchTraceIDsBase + conditions + LIMIT).
217223
const SearchTraceIDs = `
218-
SELECT DISTINCT
219-
s.trace_id,
224+
SELECT
225+
l.trace_id,
220226
t.start,
221227
t.end
222-
FROM spans s
223-
LEFT JOIN trace_id_timestamps t ON s.trace_id = t.trace_id
224-
WHERE 1=1`
228+
FROM (
229+
%s
230+
) l
231+
LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id`
225232

226233
const SelectServices = `
227234
SELECT

internal/storage/v2/clickhouse/tracestore/driver_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,11 @@ type testDriver struct {
107107
func (t *testDriver) Query(_ context.Context, query string, _ ...any) (driver.Rows, error) {
108108
t.recordedQueries = append(t.recordedQueries, query)
109109

110+
// Normalize whitespace so substring matching works regardless of indentation.
111+
normalized := strings.Join(strings.Fields(query), " ")
110112
for querySubstring, response := range t.queryResponses {
111-
if strings.Contains(query, querySubstring) {
113+
normalizedQuerySubstring := strings.Join(strings.Fields(querySubstring), " ")
114+
if strings.Contains(normalized, normalizedQuerySubstring) {
112115
return response.rows, response.err
113116
}
114117
}

internal/storage/v2/clickhouse/tracestore/query_builder.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,32 +114,33 @@ func (r *Reader) buildFindTraceIDsQuery(
114114
return "", nil, fmt.Errorf("search depth %d exceeds maximum allowed %d", limit, r.config.MaxSearchDepth)
115115
}
116116

117-
var q strings.Builder
118-
q.WriteString(sql.SearchTraceIDs)
117+
// Build the inner subquery that finds distinct trace IDs from spans.
118+
var inner strings.Builder
119+
inner.WriteString(sql.SearchTraceIDsBase)
119120
args := []any{}
120121

121122
if query.ServiceName != "" {
122-
appendAnd(&q, "s.service_name = ?")
123+
appendAnd(&inner, "s.service_name = ?")
123124
args = append(args, query.ServiceName)
124125
}
125126
if query.OperationName != "" {
126-
appendAnd(&q, "s.name = ?")
127+
appendAnd(&inner, "s.name = ?")
127128
args = append(args, query.OperationName)
128129
}
129130
if query.DurationMin > 0 {
130-
appendAnd(&q, "s.duration >= ?")
131+
appendAnd(&inner, "s.duration >= ?")
131132
args = append(args, query.DurationMin.Nanoseconds())
132133
}
133134
if query.DurationMax > 0 {
134-
appendAnd(&q, "s.duration <= ?")
135+
appendAnd(&inner, "s.duration <= ?")
135136
args = append(args, query.DurationMax.Nanoseconds())
136137
}
137138
if !query.StartTimeMin.IsZero() {
138-
appendAnd(&q, "s.start_time >= ?")
139+
appendAnd(&inner, "s.start_time >= ?")
139140
args = append(args, query.StartTimeMin)
140141
}
141142
if !query.StartTimeMax.IsZero() {
142-
appendAnd(&q, "s.start_time <= ?")
143+
appendAnd(&inner, "s.start_time <= ?")
143144
args = append(args, query.StartTimeMax)
144145
}
145146

@@ -148,15 +149,19 @@ func (r *Reader) buildFindTraceIDsQuery(
148149
return "", nil, fmt.Errorf("failed to get attribute metadata: %w", err)
149150
}
150151

151-
args, err = buildAttributeConditions(&q, args, query.Attributes, attributeMetadata)
152+
args, err = buildAttributeConditions(&inner, args, query.Attributes, attributeMetadata)
152153
if err != nil {
153154
return "", nil, err
154155
}
155156

156-
q.WriteString("\nLIMIT ?")
157+
inner.WriteString("\nLIMIT ?")
157158
args = append(args, limit)
158159

159-
return q.String(), args, nil
160+
// Wrap the inner subquery with a JOIN to trace_id_timestamps
161+
// to retrieve start/end times only for the limited set of trace IDs.
162+
q := fmt.Sprintf(sql.SearchTraceIDs, indentBlock(inner.String()))
163+
164+
return q, args, nil
160165
}
161166

162167
func buildAttributeConditions(q *strings.Builder, args []any, attributes pcommon.Map, metadata attributeMetadata) ([]any, error) {

internal/storage/v2/clickhouse/tracestore/reader_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -868,7 +868,7 @@ func TestFindTraceIDs(t *testing.T) {
868868
scanFn: scanAttributeMetadataFn(),
869869
},
870870
},
871-
sql.SearchTraceIDs: {
871+
sql.SearchTraceIDsBase: {
872872
rows: &testRows[[]any]{
873873
data: testTraceIDsData,
874874
scanFn: scanTraceIDFn(),
@@ -910,7 +910,7 @@ func TestFindTraceIDs_SearchDepthExceedsMax(t *testing.T) {
910910
driver := &testDriver{
911911
t: t,
912912
queryResponses: map[string]*testQueryResponse{
913-
sql.SearchTraceIDs: {
913+
sql.SearchTraceIDsBase: {
914914
rows: &testRows[[]any]{
915915
data: [][]any{
916916
{
@@ -942,7 +942,7 @@ func TestFindTraceIDs_YieldFalseOnSuccessStopsIteration(t *testing.T) {
942942
conn := &testDriver{
943943
t: t,
944944
queryResponses: map[string]*testQueryResponse{
945-
sql.SearchTraceIDs: {
945+
sql.SearchTraceIDsBase: {
946946
rows: &testRows[[]any]{
947947
data: testTraceIDsData,
948948
scanFn: scanTraceIDFn(),
@@ -988,7 +988,7 @@ func TestFindTraceIDs_ScanErrorContinues(t *testing.T) {
988988
conn := &testDriver{
989989
t: t,
990990
queryResponses: map[string]*testQueryResponse{
991-
sql.SearchTraceIDs: {
991+
sql.SearchTraceIDsBase: {
992992
rows: &testRows[[]any]{
993993
data: testTraceIDsData,
994994
scanFn: scanFn,
@@ -1022,7 +1022,7 @@ func TestFindTraceIDs_DecodeErrorContinues(t *testing.T) {
10221022
conn := &testDriver{
10231023
t: t,
10241024
queryResponses: map[string]*testQueryResponse{
1025-
sql.SearchTraceIDs: {
1025+
sql.SearchTraceIDsBase: {
10261026
rows: &testRows[[]any]{
10271027
data: [][]any{
10281028
testTraceIDsData[0],
@@ -1088,7 +1088,7 @@ func TestFindTraceIDs_ErrorCases(t *testing.T) {
10881088
driver: &testDriver{
10891089
t: t,
10901090
queryResponses: map[string]*testQueryResponse{
1091-
sql.SearchTraceIDs: {
1091+
sql.SearchTraceIDsBase: {
10921092
rows: nil,
10931093
err: assert.AnError,
10941094
},
@@ -1101,7 +1101,7 @@ func TestFindTraceIDs_ErrorCases(t *testing.T) {
11011101
driver: &testDriver{
11021102
t: t,
11031103
queryResponses: map[string]*testQueryResponse{
1104-
sql.SearchTraceIDs: {
1104+
sql.SearchTraceIDsBase: {
11051105
rows: &testRows[[]any]{
11061106
data: testTraceIDsData,
11071107
scanErr: assert.AnError,
@@ -1117,7 +1117,7 @@ func TestFindTraceIDs_ErrorCases(t *testing.T) {
11171117
driver: &testDriver{
11181118
t: t,
11191119
queryResponses: map[string]*testQueryResponse{
1120-
sql.SearchTraceIDs: {
1120+
sql.SearchTraceIDsBase: {
11211121
rows: &testRows[[]any]{
11221122
data: [][]any{
11231123
{
Lines changed: 105 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,107 @@
1-
SELECT DISTINCT
2-
s.trace_id,
1+
SELECT
2+
l.trace_id,
33
t.start,
44
t.end
5-
FROM spans s
6-
LEFT JOIN trace_id_timestamps t ON s.trace_id = t.trace_id
7-
WHERE 1=1
8-
AND s.service_name = ?
9-
AND s.name = ?
10-
AND s.duration >= ?
11-
AND s.duration <= ?
12-
AND s.start_time >= ?
13-
AND s.start_time <= ?
14-
AND (
15-
arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value)
16-
OR
17-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_bool_attributes.key, s.resource_bool_attributes.value)
18-
OR
19-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.events)
20-
OR
21-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.links)
22-
)
23-
AND (
24-
arrayExists((key, value) -> key = ? AND value = ?, s.double_attributes.key, s.double_attributes.value)
25-
OR
26-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value)
27-
OR
28-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.events)
29-
OR
30-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.links)
31-
)
32-
AND (
33-
arrayExists((key, value) -> key = ? AND value = ?, s.int_attributes.key, s.int_attributes.value)
34-
OR
35-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_int_attributes.key, s.resource_int_attributes.value)
36-
OR
37-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.events)
38-
OR
39-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.links)
40-
)
41-
AND (
42-
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
43-
OR
44-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
45-
OR
46-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events)
47-
OR
48-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links)
49-
)
50-
AND (
51-
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
52-
OR
53-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
54-
OR
55-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events)
56-
OR
57-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links)
58-
)
59-
AND (
60-
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
61-
OR
62-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
63-
OR
64-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events)
65-
OR
66-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links)
67-
)
68-
AND (
69-
arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value)
70-
OR
71-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_str_attributes.key, s.resource_str_attributes.value)
72-
OR
73-
arrayExists((key, value) -> key = ? AND value = ?, s.scope_str_attributes.key, s.scope_str_attributes.value)
74-
OR
75-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events)
76-
OR
77-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.links)
78-
)
79-
AND (
80-
arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value)
81-
)
82-
AND (
83-
arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value)
84-
)
85-
AND (
86-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value)
87-
)
88-
AND (
89-
arrayExists((key, value) -> key = ? AND value = ?, s.scope_int_attributes.key, s.scope_int_attributes.value)
90-
)
91-
AND (
92-
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
93-
)
94-
AND (
95-
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
96-
)
97-
AND (
98-
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
99-
)
100-
AND (
101-
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events)
102-
)
103-
LIMIT ?
5+
FROM (
6+
SELECT DISTINCT
7+
s.trace_id
8+
FROM spans s
9+
WHERE 1=1
10+
AND s.service_name = ?
11+
AND s.name = ?
12+
AND s.duration >= ?
13+
AND s.duration <= ?
14+
AND s.start_time >= ?
15+
AND s.start_time <= ?
16+
AND (
17+
arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value)
18+
OR
19+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_bool_attributes.key, s.resource_bool_attributes.value)
20+
OR
21+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.events)
22+
OR
23+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.bool_attributes.key, x.bool_attributes.value), s.links)
24+
)
25+
AND (
26+
arrayExists((key, value) -> key = ? AND value = ?, s.double_attributes.key, s.double_attributes.value)
27+
OR
28+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value)
29+
OR
30+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.events)
31+
OR
32+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.double_attributes.key, x.double_attributes.value), s.links)
33+
)
34+
AND (
35+
arrayExists((key, value) -> key = ? AND value = ?, s.int_attributes.key, s.int_attributes.value)
36+
OR
37+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_int_attributes.key, s.resource_int_attributes.value)
38+
OR
39+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.events)
40+
OR
41+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.int_attributes.key, x.int_attributes.value), s.links)
42+
)
43+
AND (
44+
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
45+
OR
46+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
47+
OR
48+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events)
49+
OR
50+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links)
51+
)
52+
AND (
53+
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
54+
OR
55+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
56+
OR
57+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events)
58+
OR
59+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links)
60+
)
61+
AND (
62+
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
63+
OR
64+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
65+
OR
66+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.events)
67+
OR
68+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.complex_attributes.key, x.complex_attributes.value), s.links)
69+
)
70+
AND (
71+
arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value)
72+
OR
73+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_str_attributes.key, s.resource_str_attributes.value)
74+
OR
75+
arrayExists((key, value) -> key = ? AND value = ?, s.scope_str_attributes.key, s.scope_str_attributes.value)
76+
OR
77+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events)
78+
OR
79+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.links)
80+
)
81+
AND (
82+
arrayExists((key, value) -> key = ? AND value = ?, s.str_attributes.key, s.str_attributes.value)
83+
)
84+
AND (
85+
arrayExists((key, value) -> key = ? AND value = ?, s.bool_attributes.key, s.bool_attributes.value)
86+
)
87+
AND (
88+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_double_attributes.key, s.resource_double_attributes.value)
89+
)
90+
AND (
91+
arrayExists((key, value) -> key = ? AND value = ?, s.scope_int_attributes.key, s.scope_int_attributes.value)
92+
)
93+
AND (
94+
arrayExists((key, value) -> key = ? AND value = ?, s.resource_complex_attributes.key, s.resource_complex_attributes.value)
95+
)
96+
AND (
97+
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
98+
)
99+
AND (
100+
arrayExists((key, value) -> key = ? AND value = ?, s.complex_attributes.key, s.complex_attributes.value)
101+
)
102+
AND (
103+
arrayExists(x -> arrayExists((key, value) -> key = ? AND value = ?, x.str_attributes.key, x.str_attributes.value), s.events)
104+
)
105+
LIMIT ?
106+
) l
107+
LEFT JOIN trace_id_timestamps t ON l.trace_id = t.trace_id

0 commit comments

Comments
 (0)