Skip to content

Commit 857dcf3

Browse files
authored
Merge pull request #24 from questdb/vi_pgx
chore: default questsb to use pgx for query benchmark
2 parents 7100f7a + 288238c commit 857dcf3

File tree

9 files changed

+415
-119
lines changed

9 files changed

+415
-119
lines changed

.github/workflows/go.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ jobs:
1313
- uses: actions/checkout@v4
1414

1515
- name: Set up Go
16-
uses: actions/setup-go@v4
16+
uses: actions/setup-go@v5
1717
with:
18-
go-version: '1.20'
18+
go-version: '1.23'
1919

2020
- name: Test
2121
run: go test -v -race ./...

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88

99
# High Dynamic Range (HDR) Histogram files
1010
*.hdr
11+
12+
# Compiled binaries
13+
tsbs_*

.travis.yml

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,9 @@ dist: focal
33
jobs:
44
include:
55
- stage: test
6-
name: "Go 1.14"
6+
name: "Go 1.23"
77
go:
8-
- 1.14.x
8+
- 1.23.x
99
install: skip
1010
script:
11-
- GO111MODULE=on go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
12-
- stage: test
13-
name: "Go 1.15"
14-
go:
15-
- 1.15.x
16-
install: skip
17-
script:
18-
- GO111MODULE=on go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...
11+
- go test -v -race -coverprofile=coverage.txt -covermode=atomic ./...

cmd/tsbs_generate_queries/databases/questdb/common.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package questdb
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"net/url"
7+
"strings"
68
"time"
79

810
"github.com/questdb/tsbs/cmd/tsbs_generate_queries/uses/devops"
@@ -19,7 +21,7 @@ func (g *BaseGenerator) GenerateEmptyQuery() query.Query {
1921
return query.NewHTTP()
2022
}
2123

22-
// fillInQuery fills the query struct with data.
24+
// fillInQuery fills the query struct with data (legacy non-parameterized).
2325
func (g *BaseGenerator) fillInQuery(qi query.Query, humanLabel, humanDesc, sql string) {
2426
v := url.Values{}
2527
v.Set("count", "false")
@@ -33,6 +35,55 @@ func (g *BaseGenerator) fillInQuery(qi query.Query, humanLabel, humanDesc, sql s
3335
q.Body = nil
3436
}
3537

38+
// fillInQueryWithParams fills the query struct with parameterized SQL and bind values.
39+
// sqlTemplate uses $1, $2, etc. placeholders for PostgreSQL prepared statements.
40+
// params contains the values to bind at runtime.
41+
func (g *BaseGenerator) fillInQueryWithParams(qi query.Query, humanLabel, humanDesc, sqlTemplate string, params []interface{}) {
42+
q := qi.(*query.HTTP)
43+
q.HumanLabel = []byte(humanLabel)
44+
q.HumanDescription = []byte(humanDesc)
45+
q.Method = []byte("GET")
46+
47+
// Store parameterized SQL in RawQuery
48+
q.RawQuery = []byte(sqlTemplate)
49+
50+
// Store parameters as JSON in Body for pgx mode
51+
paramsJSON, _ := json.Marshal(params)
52+
q.Body = paramsJSON
53+
54+
// For HTTP mode, we still need to generate the full SQL path
55+
// Substitute parameters to create the HTTP URL
56+
fullSQL := substituteParams(sqlTemplate, params)
57+
v := url.Values{}
58+
v.Set("count", "false")
59+
v.Set("query", fullSQL)
60+
q.Path = []byte(fmt.Sprintf("/exec?%s", v.Encode()))
61+
}
62+
63+
// substituteParams replaces $1, $2, etc. with actual values for HTTP mode
64+
func substituteParams(sql string, params []interface{}) string {
65+
result := sql
66+
for i, param := range params {
67+
placeholder := fmt.Sprintf("$%d", i+1)
68+
var replacement string
69+
switch v := param.(type) {
70+
case string:
71+
replacement = fmt.Sprintf("'%s'", v)
72+
case []string:
73+
// Handle string arrays for hostname IN clauses
74+
quoted := make([]string, len(v))
75+
for j, s := range v {
76+
quoted[j] = fmt.Sprintf("'%s'", s)
77+
}
78+
replacement = fmt.Sprintf("(%s)", strings.Join(quoted, ", "))
79+
default:
80+
replacement = fmt.Sprintf("%v", v)
81+
}
82+
result = strings.Replace(result, placeholder, replacement, 1)
83+
}
84+
return result
85+
}
86+
3687
// NewDevops creates a new devops use case query generator.
3788
func (g *BaseGenerator) NewDevops(start, end time.Time, scale int) (utils.QueryGenerator, error) {
3889
core, err := devops.NewCore(start, end, scale)

cmd/tsbs_generate_queries/databases/questdb/devops.go

Lines changed: 73 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,26 @@ func (d *Devops) MaxAllCPU(qi query.Query, nHosts int, duration time.Duration) {
4949
hosts, err := d.GetRandomHosts(nHosts)
5050
panicIfErr(err)
5151

52-
sql := fmt.Sprintf(`
52+
// Parameterized SQL for prepared statements
53+
sqlTemplate := fmt.Sprintf(`
5354
SELECT
54-
date_trunc('hour', timestamp) AS hour,
5555
%s
5656
FROM cpu
57-
WHERE hostname IN ('%s')
58-
AND timestamp >= '%s'
59-
AND timestamp < '%s'
60-
GROUP BY hour
61-
ORDER BY hour`,
62-
strings.Join(selectClauses, ", "),
63-
strings.Join(hosts, "', '"),
57+
WHERE hostname IN $1
58+
AND timestamp >= $2
59+
AND timestamp < $3
60+
SAMPLE BY 1h`,
61+
strings.Join(selectClauses, ", "))
62+
63+
params := []interface{}{
64+
hosts,
6465
interval.StartString(),
65-
interval.EndString())
66+
interval.EndString(),
67+
}
6668

6769
humanLabel := devops.GetMaxAllLabel("QuestDB", nHosts)
6870
humanDesc := fmt.Sprintf("%s: %s", humanLabel, interval.StartString())
69-
d.fillInQuery(qi, humanLabel, humanDesc, sql)
71+
d.fillInQueryWithParams(qi, humanLabel, humanDesc, sqlTemplate, params)
7072
}
7173

7274
// GroupByTimeAndPrimaryTag selects the AVG of metrics in the group `cpu` per device
@@ -82,21 +84,24 @@ func (d *Devops) GroupByTimeAndPrimaryTag(qi query.Query, numMetrics int) {
8284
interval := d.Interval.MustRandWindow(devops.DoubleGroupByDuration)
8385
selectClauses := d.getSelectAggClauses("avg", metrics)
8486

85-
sql := fmt.Sprintf(`
86-
SELECT date_trunc('hour', timestamp) as timestamp, hostname,
87+
// Parameterized SQL for prepared statements
88+
sqlTemplate := fmt.Sprintf(`
89+
SELECT hostname,
8790
%s
8891
FROM cpu
89-
WHERE timestamp >= '%s'
90-
AND timestamp < '%s'
91-
GROUP BY timestamp, hostname
92-
ORDER BY timestamp, hostname`,
93-
strings.Join(selectClauses, ", "),
92+
WHERE timestamp >= $1
93+
AND timestamp < $2
94+
SAMPLE BY 1h`,
95+
strings.Join(selectClauses, ", "))
96+
97+
params := []interface{}{
9498
interval.StartString(),
95-
interval.EndString())
99+
interval.EndString(),
100+
}
96101

97102
humanLabel := devops.GetDoubleGroupByLabel("QuestDB", numMetrics)
98103
humanDesc := fmt.Sprintf("%s: %s", humanLabel, interval.StartString())
99-
d.fillInQuery(qi, humanLabel, humanDesc, sql)
104+
d.fillInQueryWithParams(qi, humanLabel, humanDesc, sqlTemplate, params)
100105
}
101106

102107
// GroupByOrderByLimit populates a query.Query that has a time WHERE clause,
@@ -106,19 +111,23 @@ func (d *Devops) GroupByTimeAndPrimaryTag(qi query.Query, numMetrics int) {
106111
// groupby-orderby-limit
107112
func (d *Devops) GroupByOrderByLimit(qi query.Query) {
108113
interval := d.Interval.MustRandWindow(time.Hour)
109-
sql := fmt.Sprintf(`
110-
SELECT date_trunc('minute', timestamp) AS minute,
111-
max(usage_user)
114+
115+
// Parameterized SQL for prepared statements
116+
sqlTemplate := `
117+
SELECT max(usage_user)
112118
FROM cpu
113-
WHERE timestamp < '%s'
114-
GROUP BY minute
115-
ORDER BY minute DESC
116-
LIMIT 5`,
117-
interval.EndString())
119+
WHERE timestamp < $1
120+
SAMPLE BY 1m
121+
ORDER BY timestamp DESC
122+
LIMIT 5`
123+
124+
params := []interface{}{
125+
interval.EndString(),
126+
}
118127

119128
humanLabel := "QuestDB max cpu over last 5 min-intervals (random end)"
120129
humanDesc := fmt.Sprintf("%s: %s", humanLabel, interval.EndString())
121-
d.fillInQuery(qi, humanLabel, humanDesc, sql)
130+
d.fillInQueryWithParams(qi, humanLabel, humanDesc, sqlTemplate, params)
122131
}
123132

124133
// LastPointPerHost finds the last row for every host in the dataset
@@ -142,36 +151,45 @@ func (d *Devops) LastPointPerHost(qi query.Query) {
142151
// high-cpu-all
143152
func (d *Devops) HighCPUForHosts(qi query.Query, nHosts int) {
144153
interval := d.Interval.MustRandWindow(devops.HighCPUDuration)
145-
sql := ""
154+
155+
var sqlTemplate string
156+
var params []interface{}
157+
146158
if nHosts > 0 {
147159
hosts, err := d.GetRandomHosts(nHosts)
148160
panicIfErr(err)
149161

150-
sql = fmt.Sprintf(`
162+
// Parameterized SQL for prepared statements
163+
sqlTemplate = `
151164
SELECT *
152165
FROM cpu
153166
WHERE usage_user > 90.0
154-
AND hostname IN ('%s')
155-
AND timestamp >= '%s'
156-
AND timestamp < '%s'`,
157-
strings.Join(hosts, "', '"),
167+
AND hostname IN $1
168+
AND timestamp >= $2
169+
AND timestamp < $3`
170+
params = []interface{}{
171+
hosts,
158172
interval.StartString(),
159-
interval.EndString())
173+
interval.EndString(),
174+
}
160175
} else {
161-
sql = fmt.Sprintf(`
176+
// Parameterized SQL for prepared statements (no hostname filter)
177+
sqlTemplate = `
162178
SELECT *
163179
FROM cpu
164180
WHERE usage_user > 90.0
165-
AND timestamp >= '%s'
166-
AND timestamp < '%s'`,
181+
AND timestamp >= $1
182+
AND timestamp < $2`
183+
params = []interface{}{
167184
interval.StartString(),
168-
interval.EndString())
185+
interval.EndString(),
186+
}
169187
}
170188

171189
humanLabel, err := devops.GetHighCPULabel("QuestDB", nHosts)
172190
panicIfErr(err)
173191
humanDesc := fmt.Sprintf("%s: %s", humanLabel, interval.StartString())
174-
d.fillInQuery(qi, humanLabel, humanDesc, sql)
192+
d.fillInQueryWithParams(qi, humanLabel, humanDesc, sqlTemplate, params)
175193
}
176194

177195
// GroupByTime selects the MAX for metrics under 'cpu', per minute for N random
@@ -192,23 +210,26 @@ func (d *Devops) GroupByTime(qi query.Query, nHosts, numMetrics int, timeRange t
192210
hosts, err := d.GetRandomHosts(nHosts)
193211
panicIfErr(err)
194212

195-
sql := fmt.Sprintf(`
196-
SELECT date_trunc('minute', timestamp) as minute,
213+
// Parameterized SQL for prepared statements
214+
sqlTemplate := fmt.Sprintf(`
215+
SELECT
197216
%s
198217
FROM cpu
199-
WHERE hostname IN ('%s')
200-
AND timestamp >= '%s'
201-
AND timestamp < '%s'
202-
GROUP BY minute
203-
ORDER BY minute`,
204-
strings.Join(selectClauses, ", "),
205-
strings.Join(hosts, "', '"),
218+
WHERE hostname IN $1
219+
AND timestamp >= $2
220+
AND timestamp < $3
221+
SAMPLE BY 1m`,
222+
strings.Join(selectClauses, ", "))
223+
224+
params := []interface{}{
225+
hosts,
206226
interval.StartString(),
207-
interval.EndString())
227+
interval.EndString(),
228+
}
208229

209230
humanLabel := fmt.Sprintf(
210231
"QuestDB %d cpu metric(s), random %4d hosts, random %s by 1m",
211232
numMetrics, nHosts, timeRange)
212233
humanDesc := fmt.Sprintf("%s: %s", humanLabel, interval.StartString())
213-
d.fillInQuery(qi, humanLabel, humanDesc, sql)
234+
d.fillInQueryWithParams(qi, humanLabel, humanDesc, sqlTemplate, params)
214235
}

cmd/tsbs_generate_queries/databases/questdb/devops_test.go

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import (
1414
func TestDevopsGroupByTime(t *testing.T) {
1515
expectedHumanLabel := "QuestDB 1 cpu metric(s), random 1 hosts, random 1s by 1m"
1616
expectedHumanDesc := "QuestDB 1 cpu metric(s), random 1 hosts, random 1s by 1m: 1970-01-01T00:05:58Z"
17-
expectedQuery := "SELECT date_trunc('minute', timestamp) as minute, max(usage_user) AS max_usage_user FROM cpu " +
18-
"WHERE hostname IN ('host_9') AND timestamp >= '1970-01-01T00:05:58Z' AND timestamp < '1970-01-01T00:05:59Z' GROUP BY minute ORDER BY minute"
17+
expectedQuery := "SELECT max(usage_user) AS max_usage_user FROM cpu " +
18+
"WHERE hostname IN ('host_9') AND timestamp >= '1970-01-01T00:05:58Z' AND timestamp < '1970-01-01T00:05:59Z' SAMPLE BY 1m"
1919

2020
rand.Seed(123) // Setting seed for testing purposes.
2121
s := time.Unix(0, 0)
@@ -40,8 +40,8 @@ func TestDevopsGroupByTime(t *testing.T) {
4040
func TestDevopsGroupByOrderByLimit(t *testing.T) {
4141
expectedHumanLabel := "QuestDB max cpu over last 5 min-intervals (random end)"
4242
expectedHumanDesc := "QuestDB max cpu over last 5 min-intervals (random end): 1970-01-01T01:16:22Z"
43-
expectedQuery := "SELECT date_trunc('minute', timestamp) AS minute, max(usage_user) FROM cpu " +
44-
"WHERE timestamp < '1970-01-01T01:16:22Z' GROUP BY minute ORDER BY minute DESC LIMIT 5"
43+
expectedQuery := "SELECT max(usage_user) FROM cpu " +
44+
"WHERE timestamp < '1970-01-01T01:16:22Z' SAMPLE BY 1m ORDER BY timestamp DESC LIMIT 5"
4545

4646
rand.Seed(123) // Setting seed for testing purposes.
4747
s := time.Unix(0, 0)
@@ -72,18 +72,18 @@ func TestDevopsGroupByTimeAndPrimaryTag(t *testing.T) {
7272
input: 1,
7373
expectedHumanLabel: "QuestDB mean of 1 metrics, all hosts, random 12h0m0s by 1h",
7474
expectedHumanDesc: "QuestDB mean of 1 metrics, all hosts, random 12h0m0s by 1h: 1970-01-01T00:16:22Z",
75-
expectedQuery: "SELECT date_trunc('hour', timestamp) as timestamp, hostname, avg(usage_user) AS avg_usage_user FROM cpu " +
75+
expectedQuery: "SELECT hostname, avg(usage_user) AS avg_usage_user FROM cpu " +
7676
"WHERE timestamp >= '1970-01-01T00:16:22Z' AND timestamp < '1970-01-01T12:16:22Z' " +
77-
"GROUP BY timestamp, hostname ORDER BY timestamp, hostname",
77+
"SAMPLE BY 1h",
7878
},
7979
{
8080
desc: "5 metrics",
8181
input: 5,
8282
expectedHumanLabel: "QuestDB mean of 5 metrics, all hosts, random 12h0m0s by 1h",
8383
expectedHumanDesc: "QuestDB mean of 5 metrics, all hosts, random 12h0m0s by 1h: 1970-01-01T00:54:10Z",
84-
expectedQuery: "SELECT date_trunc('hour', timestamp) as timestamp, hostname, avg(usage_user) AS avg_usage_user, avg(usage_system) AS avg_usage_system, avg(usage_idle) AS avg_usage_idle, avg(usage_nice) AS avg_usage_nice, avg(usage_iowait) AS avg_usage_iowait FROM cpu " +
84+
expectedQuery: "SELECT hostname, avg(usage_user) AS avg_usage_user, avg(usage_system) AS avg_usage_system, avg(usage_idle) AS avg_usage_idle, avg(usage_nice) AS avg_usage_nice, avg(usage_iowait) AS avg_usage_iowait FROM cpu " +
8585
"WHERE timestamp >= '1970-01-01T00:54:10Z' AND timestamp < '1970-01-01T12:54:10Z' " +
86-
"GROUP BY timestamp, hostname ORDER BY timestamp, hostname",
86+
"SAMPLE BY 1h",
8787
},
8888
}
8989

@@ -112,16 +112,16 @@ func TestMaxAllCPU(t *testing.T) {
112112
input: 1,
113113
expectedHumanLabel: "QuestDB max of all CPU metrics, random 1 hosts, random 8h0m0s by 1h",
114114
expectedHumanDesc: "QuestDB max of all CPU metrics, random 1 hosts, random 8h0m0s by 1h: 1970-01-01T00:54:10Z",
115-
expectedQuery: "SELECT date_trunc('hour', timestamp) AS hour, max(usage_user) AS max_usage_user, max(usage_system) AS max_usage_system, max(usage_idle) AS max_usage_idle, max(usage_nice) AS max_usage_nice, max(usage_iowait) AS max_usage_iowait, max(usage_irq) AS max_usage_irq, max(usage_softirq) AS max_usage_softirq, max(usage_steal) AS max_usage_steal, max(usage_guest) AS max_usage_guest, max(usage_guest_nice) AS max_usage_guest_nice FROM cpu " +
115+
expectedQuery: "SELECT max(usage_user) AS max_usage_user, max(usage_system) AS max_usage_system, max(usage_idle) AS max_usage_idle, max(usage_nice) AS max_usage_nice, max(usage_iowait) AS max_usage_iowait, max(usage_irq) AS max_usage_irq, max(usage_softirq) AS max_usage_softirq, max(usage_steal) AS max_usage_steal, max(usage_guest) AS max_usage_guest, max(usage_guest_nice) AS max_usage_guest_nice FROM cpu " +
116116
"WHERE hostname IN ('host_3') AND timestamp >= '1970-01-01T00:54:10Z' AND timestamp < '1970-01-01T08:54:10Z' " +
117-
"GROUP BY hour ORDER BY hour",
117+
"SAMPLE BY 1h",
118118
},
119119
{
120120
desc: "5 hosts",
121121
input: 5,
122122
expectedHumanLabel: "QuestDB max of all CPU metrics, random 5 hosts, random 8h0m0s by 1h",
123123
expectedHumanDesc: "QuestDB max of all CPU metrics, random 5 hosts, random 8h0m0s by 1h: 1970-01-01T00:37:12Z",
124-
expectedQuery: "SELECT date_trunc('hour', timestamp) AS hour, max(usage_user) AS max_usage_user, max(usage_system) AS max_usage_system, max(usage_idle) AS max_usage_idle, max(usage_nice) AS max_usage_nice, max(usage_iowait) AS max_usage_iowait, max(usage_irq) AS max_usage_irq, max(usage_softirq) AS max_usage_softirq, max(usage_steal) AS max_usage_steal, max(usage_guest) AS max_usage_guest, max(usage_guest_nice) AS max_usage_guest_nice FROM cpu WHERE hostname IN ('host_9', 'host_5', 'host_1', 'host_7', 'host_2') AND timestamp >= '1970-01-01T00:37:12Z' AND timestamp < '1970-01-01T08:37:12Z' GROUP BY hour ORDER BY hour",
124+
expectedQuery: "SELECT max(usage_user) AS max_usage_user, max(usage_system) AS max_usage_system, max(usage_idle) AS max_usage_idle, max(usage_nice) AS max_usage_nice, max(usage_iowait) AS max_usage_iowait, max(usage_irq) AS max_usage_irq, max(usage_softirq) AS max_usage_softirq, max(usage_steal) AS max_usage_steal, max(usage_guest) AS max_usage_guest, max(usage_guest_nice) AS max_usage_guest_nice FROM cpu WHERE hostname IN ('host_9', 'host_5', 'host_1', 'host_7', 'host_2') AND timestamp >= '1970-01-01T00:37:12Z' AND timestamp < '1970-01-01T08:37:12Z' SAMPLE BY 1h",
125125
},
126126
}
127127

0 commit comments

Comments
 (0)