Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Artikel ini memberikan panduan tentang mengoptimalkan performa aplikasi Go yang menggunakan go-mssqldb driver dengan SQL Server.
Mulailah dengan perubahan berdampak tertinggi
Sebagian besar aplikasi tidak memerlukan penyetelan khusus driver pada hari pertama. Mulailah dengan langkah-langkah ini sebelum Anda mengubah ukuran paket, menambahkan pernyataan yang disiapkan di mana saja, atau menyetel opsi massal:
- Tetapkan ukuran kumpulan koneksi terbatas yang cocok dengan batas server Anda.
- Tambahkan timeout konteks agar panggilan SQL yang terblokir tidak menahan koneksi dan membuat aplikasi tampak beku saat beban tinggi.
- Perbaiki kueri lambat, indeks yang hilang, dan perjalanan pulang pergi yang tidak perlu di SQL Server.
- Tolok ukur beban kerja sebelum dan sesudah setiap perubahan.
Untuk banyak layanan, pengaturan kumpulan koneksi dan bentuk kueri lebih penting daripada ukuran paket atau persiapan pernyataan.
Penyetelan kumpulan koneksi
Kumpulan database/sql koneksi adalah tuas kinerja yang paling berdampak. Pool yang kekurangan kapasitas menyebabkan goroutine terblokir saat menunggu koneksi, sementara pool yang kelebihan kapasitas memboroskan sumber daya server.
db.SetMaxOpenConns(25) // Match your workload concurrency
db.SetMaxIdleConns(10) // Keep warm connections ready
db.SetConnMaxLifetime(5 * time.Minute) // Recycle connections periodically
db.SetConnMaxIdleTime(1 * time.Minute) // Close stale idle connections
Pantau db.Stats().WaitCount dan db.Stats().WaitDuration untuk mendeteksi perselisihan kumpulan. Untuk detail selengkapnya, lihat Pengumpulan koneksi.
Meningkatkan ukuran paket
Ukuran paket TDS default adalah 4.096 byte. Untuk beban kerja yang mentransfer kumpulan hasil besar atau data massal, meningkatkan ukuran paket mengurangi jumlah perjalanan pulang pergi jaringan.
sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&packet+size=16384
Kisaran yang berlaku: 512 hingga 32.767. Nilai 8.192 atau 16.384 umum untuk skenario throughput tinggi.
Pertahankan ukuran paket default kecuali data tolok ukur menunjukkan bahwa transfer jaringan mendominasi beban kerja. Untuk lalu lintas gaya OLTP dengan kueri dan baris kecil, paket yang lebih besar sering menambah kerumitan tanpa keuntungan yang berarti.
Gunakan pernyataan yang sudah disiapkan
Pernyataan yang disiapkan menghindari penguraian kueri berulang dan kompilasi rencana di server. Gunakan db.PrepareContext saat Anda menjalankan kueri yang sama berkali-kali dengan parameter yang berbeda:
stmt, err := db.PrepareContext(ctx,
"SELECT TOP (1) FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE CountryRegionName = @p1")
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
for _, loc := range locations {
var name string
stmt.QueryRowContext(ctx, loc).Scan(&name)
}
Tip
Tutup prepared statement dengan menggunakan defer stmt.Close() agar tidak menggunakan handle prepared statement di sisi server.
Jangan menyiapkan setiap pernyataan secara default. Mereka paling membantu ketika pernyataan yang sama berjalan berulang kali di jalan panas. Untuk pertanyaan satu kali, QueryContext atau ExecContext biasanya lebih mudah dan cukup cepat.
Menggunakan salinan massal untuk sisipan besar
Pernyataan individual INSERT lambat untuk pemuatan data besar. Penyalinan massal menyalurkan data langsung ke server, tanpa melalui pemroses kueri:
stmt, err := txn.Prepare(mssql.CopyIn("MyTable", mssql.BulkOptions{
Tablock: true,
RowsPerBatch: 5000,
}, "Col1", "Col2"))
Untuk detailnya, lihat Operasi massal.
Gunakan varchar bila sesuai
Secara default, kode mengirimkan string parameter sebagai nvarchar (Unicode). Jika kolom Anda menggunakan varchar, server mungkin melakukan konversi implisit dan melewati indeks. Gunakan mssql.VarChar untuk mengirim varchar parameter.
db.QueryContext(ctx, "SELECT * FROM Production.Product WHERE ProductNumber = @p1",
mssql.VarChar("FR-R92B-58"))
Gunakan batas waktu konteks
Tetapkan batas waktu konteks pada setiap kueri untuk mencegah panggilan SQL yang terblokir menahan koneksi tetap terikat dan menghambat proses pemanggil.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM LargeTable")
Tutup sumber daya segera
Buka *sql.Rows, *sql.Tx, dan *sql.Conn objek menyematkan koneksi dari kolam. Selalu tutup sesegera mungkin.
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
// process
}
return rows.Err()
Kurangi perjalanan pulang pergi
-
Gabungkan kueri ke dalam satu panggilan jika memungkinkan:
"SELECT ...; SELECT ...;". -
Gunakan
OUTPUTklausa alih-alih panggilan terpisahSELECT SCOPE_IDENTITY(). - Gunakan parameter bernilai tabel untuk mengirim beberapa baris dalam satu panggilan alih-alih mengulang sisipan individual.
Daftar Periksa Kinerja
| Area | Recommendation |
|---|---|
| Kolam | Tetapkan MaxOpenConns berdasarkan batas server dan konkurensi beban kerja. |
| Kolam | Atur MaxIdleConns setidaknya setengah dari MaxOpenConns. |
| Network | Ubah packet size hanya setelah membandingkan transfer besar atau beban massal. |
| Queries | Gunakan pernyataan yang disiapkan hanya untuk pernyataan yang sering diulang. |
| Queries | Gunakan mssql.VarChar untuk varchar kolom untuk menghindari konversi implisit. |
| Beban besar | Gunakan salinan massal (mssql.CopyIn) untuk sisipan batch. |
| Resources | Tutup Rows, Tx, dan Conn objek segera. |
| Batas Waktu | Tetapkan tenggat waktu konteks pada semua kueri dan pernyataan. |
| Baca berat | Gunakan ApplicationIntent=ReadOnly untuk replika baca. |
| Tolok ukur | Gunakan testing.B untuk mengukur sebelum dan sesudah pengoptimalan. |
| Monitoring | Aktifkan Query Store dan gunakan laporan SSMS atau Query Performance Insight. |
| Monitoring | Ekspor db.Stats() ke Prometheus atau OpenTelemetry. |
Uji tolok ukur operasi basis data
Gunakan Go testing.B untuk mengukur performa operasi database dan memvalidasi perubahan pengoptimalan:
func BenchmarkInsertSingle(b *testing.B) {
db := setupDB(b)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := db.ExecContext(ctx,
"INSERT INTO BenchTable (Name) VALUES (@p1)",
sql.Named("p1", fmt.Sprintf("bench-%d", i)))
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkInsertBulk(b *testing.B) {
db := setupDB(b)
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
txn, err := db.BeginTx(ctx, nil)
if err != nil {
b.Fatal(err)
}
stmt, err := txn.Prepare(mssql.CopyIn("BenchTable",
mssql.BulkOptions{}, "Name"))
if err != nil {
b.Fatal(err)
}
for j := 0; j < 1000; j++ {
if _, err := stmt.Exec(fmt.Sprintf("bench-%d-%d", i, j)); err != nil {
b.Fatal(err)
}
}
if _, err := stmt.Exec(); err != nil {
b.Fatal(err)
}
stmt.Close()
if err := txn.Commit(); err != nil {
b.Fatal(err)
}
}
}
Jalankan tolok ukur dengan:
go test -bench=BenchmarkInsert -benchmem -count=5
Tip
Gunakan -count=5 atau lebih tinggi untuk mendapatkan hasil yang bermakna secara statistik. Gunakan benchstat untuk membandingkan hasil benchmark sebelum dan sesudah perubahan.
Gunakan perutean baca-saja
Jika lingkungan SQL Server Anda memiliki grup ketersediaan dengan replika sekunder yang dapat dibaca, arahkan kueri hanya baca ke replika sekunder dengan menetapkan ApplicationIntent=ReadOnly di string koneksi:
sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly
Buat instans terpisah *sql.DB untuk beban kerja baca dan tulis:
writeDB, err := sql.Open("sqlserver",
"sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025")
if err != nil {
log.Fatal(err)
}
readDB, err := sql.Open("sqlserver",
"sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly")
if err != nil {
log.Fatal(err)
}
// Use readDB for reports, dashboards, and analytics.
// Use writeDB for inserts, updates, and deletes.
Analisis rencana kueri
Gunakan SET SHOWPLAN_XML untuk mengambil rencana eksekusi kueri tanpa menjalankannya. Metode ini membantu Anda mengidentifikasi pemindaian tabel, indeks yang hilang, dan operasi yang mahal:
func getQueryPlan(ctx context.Context, db *sql.DB, query string) (string, error) {
// Use a dedicated connection so SHOWPLAN mode doesn't affect other queries.
conn, err := db.Conn(ctx)
if err != nil {
return "", err
}
defer conn.Close()
// Enable SHOWPLAN_XML mode.
_, err = conn.ExecContext(ctx, "SET SHOWPLAN_XML ON")
if err != nil {
return "", err
}
var planXML string
err = conn.QueryRowContext(ctx, query).Scan(&planXML)
if err != nil {
return "", err
}
// Disable SHOWPLAN_XML mode.
_, _ = conn.ExecContext(ctx, "SET SHOWPLAN_XML OFF")
return planXML, nil
}
Warning
SET SHOWPLAN_XML ON memengaruhi seluruh koneksi. Selalu gunakan db.Conn(ctx) untuk mengisolasi mode SHOWPLAN ke koneksi khusus.
Gunakan Query Store untuk menemukan kueri yang lambat
Tolok ukur Go dan waktu sisi klien memberi tahu Anda berapa lama waktu yang dibutuhkan kueri dari perspektif aplikasi Anda, tetapi angka tersebut menggabungkan latensi jaringan, waktu eksekusi server, dan pemrosesan klien. Query Store menangkap rencana eksekusi dan statistik runtime di server, sehingga Anda dapat melihat dengan tepat bagaimana SQL Server mengeksekusi setiap kueri, seberapa sering berjalan, dan bagaimana performanya berubah dari waktu ke waktu.
Query Store sangat berguna untuk mengidentifikasi pengendusan parameter, regresi rencana, dan kueri yang menghabiskan sumber daya server paling banyak. Aktifkan di database Anda jika belum diaktifkan:
ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;
Setelah diaktifkan, Anda dapat meninjau data performa dengan beberapa cara:
- SQL Server Management Studio (SSMS): Perluas database Anda di Object Explorer, buka folder Query Store, dan gunakan laporan bawaan seperti Kueri Konsumsi Sumber Daya Teratas, Kueri Regresi, dan Konsumsi Sumber Daya Keseluruhan.
- Portal Azure: Untuk Azure SQL Database, buka bilah Wawasan Performa Kueri untuk melihat kueri yang menghabiskan sumber daya teratas tanpa menginstal alat apa pun.
-
Transact-SQL (T-SQL): Kueri tampilan katalog
sys.query_store_runtime_statsdansys.query_store_plansecara langsung dari aplikasi Go Anda jika Anda memerlukan akses secara terprogram.
Tip
Query Store mempertahankan data di seluruh server yang dimulai ulang, sehingga Anda dapat menganalisis tren performa selama berhari-hari atau berminggu-minggu. Gunakan laporan Kueri Regresi untuk menemukan kueri yang menjadi lebih lambat dengan cepat setelah perubahan skema atau kode.
Menggunakan Dasbor Performa
Dasbor Performa adalah laporan SSMS bawaan yang memberikan gambaran umum real-time tentang kesehatan SQL Server. Klik kanan instans server di SSMS Object Explorer dan pilih Laporan>Laporan Standar>Dasbor Performa.
Dasbor menunjukkan:
- Waktu tunggu dan hambatan saat ini.
- Pertanyaan mahal baru-baru ini.
- Tren penggunaan CPU, I/O, dan memori.
- Permintaan pengguna aktif dan sesi yang diblokir.
Dasbor Performa berguna selama pengembangan dan pengujian beban untuk menemukan masalah dengan cepat tanpa menulis kueri diagnostik apa pun.
Memantau metrik sisi server dari Go
Jika Anda perlu mengekspos data performa SQL Server ke sistem pemantauan seperti Prometheus atau OpenTelemetry dari aplikasi Go Anda, kueri tampilan manajemen dinamis (DMV) secara langsung:
Kueri paling mahal
Ambil 10 kueri teratas yang diberi peringkat berdasarkan waktu berlalu rata-rata:
rows, err := db.QueryContext(ctx, `
SELECT TOP 10
qs.total_elapsed_time / qs.execution_count AS avg_elapsed_us,
qs.execution_count,
qs.total_logical_reads / qs.execution_count AS avg_reads,
SUBSTRING(st.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset)/2)+1) AS query_text
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
ORDER BY avg_elapsed_us DESC`)
Permintaan aktif saat ini
Cantumkan semua permintaan yang sedang dijalankan di server, tidak termasuk sesi Anda sendiri:
rows, err := db.QueryContext(ctx, `
SELECT
r.session_id,
r.status,
r.wait_type,
r.cpu_time,
r.logical_reads,
t.text AS query_text
FROM sys.dm_exec_requests AS r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t
WHERE r.session_id != @@SPID`)
Pola hemat memori untuk hasil yang besar
Streaming baris tanpa akumulasi
Proses baris satu per satu alih-alih memuat seluruh kumpulan hasil ke dalam slice:
rows, err := db.QueryContext(ctx, "SELECT Id, Data FROM BigTable")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var id int
var data string
if err := rows.Scan(&id, &data); err != nil {
return err
}
// Process immediately, don't append to a slice.
process(id, data)
}
return rows.Err()
Pemrosesan batch dengan paginasi keyset
Pecahkan pemindaian tabel besar menjadi potongan-potongan yang dapat dikelola untuk membatasi penggunaan memori dan menghindari menahan koneksi untuk waktu yang lama:
func processBatched(ctx context.Context, db *sql.DB, batchSize int) error {
var lastID int
for {
rows, err := db.QueryContext(ctx, `
SELECT TOP(@batch) Id, Data FROM BigTable
WHERE Id > @lastID ORDER BY Id`,
sql.Named("batch", batchSize),
sql.Named("lastID", lastID))
if err != nil {
return err
}
var count int
for rows.Next() {
var id int
var data string
if err := rows.Scan(&id, &data); err != nil {
return err
}
process(id, data)
lastID = id
count++
}
if err := rows.Err(); err != nil {
return err
}
rows.Close()
if count < batchSize {
break // No more rows.
}
}
return nil
}