// Go MySQL Driver - A MySQL-Driver for Go's database/sql package // // Copyright 2013 The Go-MySQL-Driver Authors. All rights reserved. // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this file, // You can obtain one at http://mozilla.org/MPL/2.0/. package mysql import ( "bytes" "context" "database/sql" "database/sql/driver" "fmt" "math" "runtime" "strings" "sync" "sync/atomic" "testing" "time" ) type TB testing.B func (tb *TB) check(err error) { if err != nil { tb.Fatal(err) } } func (tb *TB) checkDB(db *sql.DB, err error) *sql.DB { tb.check(err) return db } func (tb *TB) checkRows(rows *sql.Rows, err error) *sql.Rows { tb.check(err) return rows } func (tb *TB) checkStmt(stmt *sql.Stmt, err error) *sql.Stmt { tb.check(err) return stmt } func initDB(b *testing.B, compress bool, queries ...string) *sql.DB { tb := (*TB)(b) comprStr := "" if compress { comprStr = "&compress=1" } db := tb.checkDB(sql.Open(driverNameTest, dsn+comprStr)) for _, query := range queries { if _, err := db.Exec(query); err != nil { b.Fatalf("error on %q: %v", query, err) } } return db } const concurrencyLevel = 10 func BenchmarkQuery(b *testing.B) { benchmarkQuery(b, false) } func BenchmarkQueryCompressed(b *testing.B) { benchmarkQuery(b, true) } func benchmarkQuery(b *testing.B, compr bool) { tb := (*TB)(b) b.ReportAllocs() db := initDB(b, compr, "DROP TABLE IF EXISTS foo", "CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))", `INSERT INTO foo VALUES (1, "one")`, `INSERT INTO foo VALUES (2, "two")`, ) db.SetMaxIdleConns(concurrencyLevel) defer db.Close() stmt := tb.checkStmt(db.Prepare("SELECT val FROM foo WHERE id=?")) defer stmt.Close() remain := int64(b.N) var wg sync.WaitGroup wg.Add(concurrencyLevel) defer wg.Wait() b.StartTimer() for range concurrencyLevel { go func() { for { if atomic.AddInt64(&remain, -1) < 0 { wg.Done() return } var got string tb.check(stmt.QueryRow(1).Scan(&got)) if got != "one" { b.Errorf("query = %q; want one", got) wg.Done() return } } }() } } func BenchmarkExec(b *testing.B) { tb := (*TB)(b) db := tb.checkDB(sql.Open(driverNameTest, dsn)) db.SetMaxIdleConns(concurrencyLevel) defer db.Close() stmt := tb.checkStmt(db.Prepare("DO 1")) defer stmt.Close() remain := int64(b.N) var wg sync.WaitGroup wg.Add(concurrencyLevel) defer wg.Wait() b.ReportAllocs() b.ResetTimer() for range concurrencyLevel { go func() { for { if atomic.AddInt64(&remain, -1) < 0 { wg.Done() return } if _, err := stmt.Exec(); err != nil { b.Logf("stmt.Exec failed: %v", err) b.Fail() } } }() } } // data, but no db writes var roundtripSample []byte func initRoundtripBenchmarks() ([]byte, int, int) { if roundtripSample == nil { roundtripSample = []byte(strings.Repeat("0123456789abcdef", 1024*1024)) } return roundtripSample, 16, len(roundtripSample) } func BenchmarkRoundtripTxt(b *testing.B) { sample, min, max := initRoundtripBenchmarks() sampleString := string(sample) tb := (*TB)(b) db := tb.checkDB(sql.Open(driverNameTest, dsn)) defer db.Close() b.ReportAllocs() var result string for i := 0; b.Loop(); i++ { length := min + i if length > max { length = max } test := sampleString[0:length] rows := tb.checkRows(db.Query(`SELECT "` + test + `"`)) if !rows.Next() { rows.Close() b.Fatalf("crashed") } err := rows.Scan(&result) if err != nil { rows.Close() b.Fatalf("crashed") } if result != test { rows.Close() b.Errorf("mismatch") } rows.Close() } } func BenchmarkRoundtripBin(b *testing.B) { sample, min, max := initRoundtripBenchmarks() tb := (*TB)(b) db := tb.checkDB(sql.Open(driverNameTest, dsn)) defer db.Close() stmt := tb.checkStmt(db.Prepare("SELECT ?")) defer stmt.Close() b.ReportAllocs() var result sql.RawBytes for i := 0; b.Loop(); i++ { length := min + i if length > max { length = max } test := sample[0:length] rows := tb.checkRows(stmt.Query(test)) if !rows.Next() { rows.Close() b.Fatalf("crashed") } err := rows.Scan(&result) if err != nil { rows.Close() b.Fatalf("crashed") } if !bytes.Equal(result, test) { rows.Close() b.Errorf("mismatch") } rows.Close() } } func BenchmarkInterpolation(b *testing.B) { mc := &mysqlConn{ cfg: &Config{ InterpolateParams: true, Loc: time.UTC, }, maxAllowedPacket: maxPacketSize, maxWriteSize: maxPacketSize - 1, buf: newBuffer(), } args := []driver.Value{ int64(42424242), float64(math.Pi), false, time.Unix(1423411542, 807015000), []byte("bytes containing special chars ' \" \a \x00"), "string containing special chars ' \" \a \x00", } q := "SELECT ?, ?, ?, ?, ?, ?" b.ReportAllocs() for b.Loop() { _, err := mc.interpolateParams(q, args) if err != nil { b.Fatal(err) } } } func benchmarkQueryContext(b *testing.B, db *sql.DB, p int) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() db.SetMaxIdleConns(p * runtime.GOMAXPROCS(0)) tb := (*TB)(b) stmt := tb.checkStmt(db.PrepareContext(ctx, "SELECT val FROM foo WHERE id=?")) defer stmt.Close() b.SetParallelism(p) b.ReportAllocs() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { var got string for pb.Next() { tb.check(stmt.QueryRow(1).Scan(&got)) if got != "one" { b.Fatalf("query = %q; want one", got) } } }) } func BenchmarkQueryContext(b *testing.B) { db := initDB(b, false, "DROP TABLE IF EXISTS foo", "CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))", `INSERT INTO foo VALUES (1, "one")`, `INSERT INTO foo VALUES (2, "two")`, ) defer db.Close() for _, p := range []int{1, 2, 3, 4} { b.Run(fmt.Sprintf("%d", p), func(b *testing.B) { benchmarkQueryContext(b, db, p) }) } } func benchmarkExecContext(b *testing.B, db *sql.DB, p int) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() db.SetMaxIdleConns(p * runtime.GOMAXPROCS(0)) tb := (*TB)(b) stmt := tb.checkStmt(db.PrepareContext(ctx, "DO 1")) defer stmt.Close() b.SetParallelism(p) b.ReportAllocs() b.ResetTimer() b.RunParallel(func(pb *testing.PB) { for pb.Next() { if _, err := stmt.ExecContext(ctx); err != nil { b.Fatal(err) } } }) } func BenchmarkExecContext(b *testing.B) { db := initDB(b, false, "DROP TABLE IF EXISTS foo", "CREATE TABLE foo (id INT PRIMARY KEY, val CHAR(50))", `INSERT INTO foo VALUES (1, "one")`, `INSERT INTO foo VALUES (2, "two")`, ) defer db.Close() for _, p := range []int{1, 2, 3, 4} { b.Run(fmt.Sprintf("%d", p), func(b *testing.B) { benchmarkExecContext(b, db, p) }) } } // BenchmarkQueryRawBytes benchmarks fetching 100 blobs using sql.RawBytes. // "size=" means size of each blobs. func BenchmarkQueryRawBytes(b *testing.B) { var sizes []int = []int{100, 1000, 2000, 4000, 8000, 12000, 16000, 32000, 64000, 256000} db := initDB(b, false, "DROP TABLE IF EXISTS bench_rawbytes", "CREATE TABLE bench_rawbytes (id INT PRIMARY KEY, val LONGBLOB)", ) defer db.Close() blob := make([]byte, sizes[len(sizes)-1]) for i := range blob { blob[i] = 42 } for i := range 100 { _, err := db.Exec("INSERT INTO bench_rawbytes VALUES (?, ?)", i, blob) if err != nil { b.Fatal(err) } } for _, s := range sizes { b.Run(fmt.Sprintf("size=%v", s), func(b *testing.B) { db.SetMaxIdleConns(0) db.SetMaxIdleConns(1) b.ReportAllocs() b.ResetTimer() for j := 0; j < b.N; j++ { rows, err := db.Query("SELECT LEFT(val, ?) as v FROM bench_rawbytes", s) if err != nil { b.Fatal(err) } nrows := 0 for rows.Next() { var buf sql.RawBytes err := rows.Scan(&buf) if err != nil { b.Fatal(err) } if len(buf) != s { b.Fatalf("size mismatch: expected %v, got %v", s, len(buf)) } nrows++ } rows.Close() if nrows != 100 { b.Fatalf("numbers of rows mismatch: expected %v, got %v", 100, nrows) } } }) } } func benchmark10kRows(b *testing.B, compress bool) { // Setup -- prepare 10000 rows. db := initDB(b, compress, "DROP TABLE IF EXISTS foo", "CREATE TABLE foo (id INT PRIMARY KEY, val TEXT)") defer db.Close() sval := strings.Repeat("x", 50) stmt, err := db.Prepare(`INSERT INTO foo (id, val) VALUES (?, ?)` + strings.Repeat(",(?,?)", 99)) if err != nil { b.Errorf("failed to prepare query: %v", err) return } args := make([]any, 200) for i := 1; i < 200; i += 2 { args[i] = sval } for i := 0; i < 10000; i += 100 { for j := range 100 { args[j*2] = i + j } _, err := stmt.Exec(args...) if err != nil { b.Error(err) return } } stmt.Close() // benchmark function called several times with different b.N. // it means heavy setup is called multiple times. // Use b.Run() to run expensive setup only once. // Go 1.24 introduced b.Loop() for this purpose. But we keep this // benchmark compatible with Go 1.20. b.Run("query", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { rows, err := db.Query(`SELECT id, val FROM foo`) if err != nil { b.Errorf("failed to select: %v", err) return } // rows.Scan() escapes arguments. So these variables must be defined // before loop. var i int var s sql.RawBytes for rows.Next() { if err := rows.Scan(&i, &s); err != nil { b.Errorf("failed to scan: %v", err) rows.Close() return } } if err = rows.Err(); err != nil { b.Errorf("failed to read rows: %v", err) } rows.Close() } }) } // BenchmarkReceive10kRows measures performance of receiving large number of rows. func BenchmarkReceive10kRows(b *testing.B) { benchmark10kRows(b, false) } func BenchmarkReceive10kRowsCompressed(b *testing.B) { benchmark10kRows(b, true) } // BenchmarkReceiveMetadata measures performance of receiving lots of metadata compare to data in rows func BenchmarkReceiveMetadata(b *testing.B) { tb := (*TB)(b) // Create a table with 1000 integer fields var createTableQuery strings.Builder createTableQuery.WriteString("CREATE TABLE large_integer_table (") for i := range 1000 { createTableQuery.WriteString(fmt.Sprintf("col_%d INT", i)) if i < 999 { createTableQuery.WriteString(", ") } } createTableQuery.WriteString(")") // Initialize database db := initDB(b, false, "DROP TABLE IF EXISTS large_integer_table", createTableQuery.String(), "INSERT INTO large_integer_table VALUES ("+ strings.Repeat("0,", 999)+"0)", // Insert a row of zeros ) defer db.Close() b.Run("query", func(b *testing.B) { db.SetMaxIdleConns(0) db.SetMaxIdleConns(1) // Create a slice to scan all columns values := make([]any, 1000) valuePtrs := make([]any, 1000) for j := range values { valuePtrs[j] = &values[j] } // Prepare a SELECT query to retrieve metadata stmt := tb.checkStmt(db.Prepare("SELECT * FROM large_integer_table LIMIT 1")) defer stmt.Close() // Benchmark metadata retrieval b.ReportAllocs() b.ResetTimer() for range b.N { rows := tb.checkRows(stmt.Query()) rows.Next() // Scan the row err := rows.Scan(valuePtrs...) tb.check(err) rows.Close() } }) }