本文提供如何優化使用 go-mssqldb SQL Server 驅動程式的 Go 應用程式效能的指引。
從影響最大的變更開始
大多數應用程式在第一天就不需要針對驅動程式做專屬調校。 在更改資料包大小、在各處加入預備語句或調整批量選項前,先從以下步驟開始:
- 設定一個與伺服器限制相符的連線池大小。
- 為 context 設定逾時,避免遭阻塞的 SQL 呼叫占住連線,導致應用程式在高負載時看似卡住。
- 修正 SQL Server 中緩慢查詢、缺少索引及不必要的往返問題。
- 在每次變更前後,先做工作負載的基準測試。
對許多服務而言,連線池設定與查詢形狀比封包大小或語句準備更重要。
連線池調校
database/sql連線池是最具影響力的效能槓桿。 連線池配置不足會使 goroutine 因等待連線而阻塞,而連線池配置過多則會浪費伺服器資源。
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
監控 db.Stats().WaitCount 和 db.Stats().WaitDuration,以偵測集區爭用。 如需更多詳細資訊,請參閱 連線共用池。
增加封包大小
預設的 TDS 封包大小為 4,096 位元組。 對於傳輸大量結果集或大量資料的工作負載,增加封包大小可減少網路往返次數。
sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&packet+size=16384
有效範圍:512至32,767。 8,192 或 16,384 的數值在高吞吐量情境中很常見。
除非基準數據顯示網路傳輸佔據了大部分工作負載,否則就保留預設封包大小。 對於查詢與列較小的 OLTP 流量,較大封包通常會增加複雜度,卻無法帶來實質的提升。
使用預先準備好的陳述
預先準備的語句避免了在伺服器上重複的查詢解析與編譯計畫。 當你多次執行相同查詢但參數不同時,請使用 db.PrepareContext :
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)
}
小提示
請使用 defer stmt.Close() 關閉已準備的陳述式,以避免耗用伺服器端預備陳述式控制代碼。
不要預設準備每一份陳述。 當相同的陳述式在熱點路徑中反覆執行時,它們最能發揮作用。 對於單次查詢,QueryContext 或 ExecContext 通常更直接,而且速度也夠快。
對於大量插入作業,請使用批次複製
單個 INSERT 語句對於大量資料負載來說速度較慢。 批量複製會直接將資料串流到伺服器,繞過查詢處理器:
stmt, err := txn.Prepare(mssql.CopyIn("MyTable", mssql.BulkOptions{
Tablock: true,
RowsPerBatch: 5000,
}, "Col1", "Col2"))
詳情請參見 「大宗作業」。
適當時使用varchar
預設情況下,程式碼會將 string 參數以 nvarchar(Unicode)傳送。 如果你的欄位使用 varchar,伺服器可能會進行隱含轉換並跳過索引。 使用 mssql.VarChar 傳送 varchar 參數。
db.QueryContext(ctx, "SELECT * FROM Production.Product WHERE ProductNumber = @p1",
mssql.VarChar("FR-R92B-58"))
使用上下文逾時
為每個查詢設定上下文截止日期,以防止被阻擋的 SQL 呼叫導致連線被釘釘或呼叫者停滯。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM LargeTable")
請及時關閉資源
打開 *sql.Rows、 *sql.Tx,物件 *sql.Conn 從池中釘住連接。 務必盡快關閉。
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
// process
}
return rows.Err()
減少往返行程
- 盡可能將批次查詢集中於單一呼叫中:
"SELECT ...; SELECT ...;"。 -
使用
OUTPUT子句,而不要分開呼叫SELECT SCOPE_IDENTITY()。 - 使用表格值參數 在單一呼叫中傳送多列,而非重複重複插入。
效能檢查清單
| Area | Recommendation |
|---|---|
| 游泳池 | 根據伺服器限制和工作負載的並行程度來設定 MaxOpenConns。 |
| 游泳池 | 將 MaxIdleConns 設定為至少是 MaxOpenConns 的一半。 |
| Network | 只有在對大型傳輸或大量負載做基準測試後才更改 packet size 。 |
| Queries | 僅在經常重複的陳述時使用預先準備的陳述。 |
| Queries | 對 varchar 欄位使用 mssql.VarChar,以避免隱式轉換。 |
| 大載荷 | 批次插入時使用批量副本(mssql.CopyIn)。 |
| Resources | 請及時關閉 Rows、Tx 和 Conn 物件。 |
| Timeouts | 為所有查詢和陳述設定上下文截止期限。 |
| 讀取密集型 | 使用 ApplicationIntent=ReadOnly 作為讀取複本。 |
| Benchmarks | 使用 testing.B 來衡量最佳化前後的差異。 |
| Monitoring | 啟用 查詢存放區,並使用 SSMS 報告或 Query Performance Insight。 |
| Monitoring | 匯出 db.Stats() 到 Prometheus 或 OpenTelemetry。 |
基準資料庫運作
使用 Go testing.B 來衡量資料庫操作的效能並驗證優化變更:
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)
}
}
}
使用以下項目執行基準測試:
go test -bench=BenchmarkInsert -benchmem -count=5
小提示
使用 -count=5 或更高版本,才能獲得具統計意義的結果。 使用 benchstat 比較變更前後的基準測試結果。
使用唯讀路由
如果您的 SQL Server 環境具有含可讀取次要複本的可用性群組,請在連線字串中設定 ApplicationIntent=ReadOnly,將唯讀查詢路由至次要複本:
sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly
為讀寫工作負載建立獨立 *sql.DB 實例:
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.
查詢計畫分析
用 SET SHOWPLAN_XML 來取得查詢的執行計畫,但不執行該查詢。 此方法有助於辨識資料表掃描、缺失的索引及昂貴的操作:
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 影響整個連線。 請一律使用 db.Conn(ctx),將 SHOWPLAN 模式限制在專用連線中。
使用 查詢存放區 來尋找緩慢的查詢
Go 基準測試和用戶端時序告訴你,從應用程式角度來看查詢所需時間,但這個數字會結合網路延遲、伺服器執行時間和用戶端處理。 查詢存放區 會擷取伺服器上的執行計畫和執行時統計,讓你能精確看到 SQL Server 如何執行每個查詢、執行頻率,以及其效能隨時間的變化。
查詢存放區 特別適合辨識參數嗅探、計畫迴歸,以及消耗最多伺服器資源的查詢。 如果你的資料庫尚未啟用此功能,請啟用:
ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;
啟用後,您可以透過多種方式檢視績效資料:
- SQL Server Management Studio (SSMS):在 物件總管 展開資料庫,開啟 查詢存放區 資料夾,使用內建報告如「頂尖資源消耗查詢」、「回歸查詢」及「整體資源消耗」。
- Azure 入口網站:對於 Azure SQL Database,開啟查詢效能洞察(Query Performance Insight)刀片,即可查看最耗資源的查詢,無需安裝任何工具。
-
Transact-SQL(T-SQL):如果你需要程式化存取,可以直接從 Go 應用程式查詢
sys.query_store_runtime_stats和sys.query_store_plan目錄視圖。
小提示
查詢存放區 會持續保存伺服器重啟時的資料,因此你可以分析數天或數週的效能趨勢。 使用 查詢回歸 報告,即可快速找出在結構描述或程式碼變更後變得更慢的查詢。
使用績效儀表板
效能儀表板是內建的 SSMS 報告,提供 SQL Server 健康狀況的即時概覽。 在 SSMS 物件總管 中右鍵點擊伺服器實例,並選擇「報告>標準報告>效能儀表板」。
儀表板顯示:
- 目前的等待與瓶頸。
- 最近昂貴的查詢。
- CPU、I/O 和記憶體使用趨勢。
- 主動使用者請求與被封鎖的會話。
效能儀表板在開發與負載測試中非常有用,能快速發現問題,無需撰寫任何診斷查詢。
監控 Go 的伺服器端指標
如果你需要將 SQL Server 效能資料暴露給像 Prometheus 或 OpenTelemetry 這類監控系統,從你的 Go 應用程式中,請直接查詢動態管理視圖(DMV):
最昂貴的查詢
檢索依平均經過時間排名前十的查詢:
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`)
目前有效申請
列出伺服器上目前執行的所有請求,不包括你自己的工作階段:
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`)
大型結果的記憶體效率模式
以串流方式傳送資料列,而不累積資料
逐列處理資料,而不是將整個結果集載入到 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()
使用鍵集分頁的批次處理
將大型資料表掃描拆解成可管理的區塊,以減少記憶體使用並避免長時間保留連線:
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
}