Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Dieser Artikel gibt Anleitungen zur Optimierung der Leistung von Go-Anwendungen, die den Treiber go-mssqldb mit SQL Server verwenden.
Beginnen Sie mit den Veränderungen mit der größten Wirkung
Die meisten Anwendungen benötigen von Anfang an keine treiberspezifische Optimierung. Beginnen Sie mit diesen Schritten, bevor Sie die Paketgröße ändern, überall vorbereitete Anweisungen hinzufügen oder Massenoptionen optimieren:
- Setze eine begrenzte Connection Pool-Größe, die deinen Serverlimits entspricht.
- Fügen Sie Kontext-Timeouts hinzu, damit blockierte SQL-Aufrufe Verbindungen nicht festnageln und die App unter Last eingefroren erscheinen lässt.
- Beheben Sie langsame Abfragen, fehlende Indizes und unnötige Roundtrips in SQL Server.
- Führe vor und nach jeder Änderung Benchmarks für den Workload durch.
Für viele Services sind Verbindungspooleinstellungen und Abfrageform wichtiger als Paketgröße oder Vorbereitung von Anweisungen.
Optimierung des Verbindungspools
Der database/sql Verbindungspool ist der wirkungsvollste Leistungshebel. Unterprovisionierte Pools führen dazu, dass goroutines das Warten auf Verbindungen blockieren, während überprovisionierte Pools Serverressourcen verschwenden.
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
Überwachen Sie db.Stats().WaitCount und db.Stats().WaitDuration, um Ressourcenkonflikte im Pool zu erkennen. Weitere Informationen finden Sie unter Verbindungspooling.
Paketgröße erhöhen
Die Standard-TDS-Paketgröße beträgt 4.096 Bytes. Bei Arbeitslasten, die große Ergebnissätze oder Massendaten übertragen, verringert eine Vergrößerung der Paketgröße die Anzahl der Netzwerk-Roundtrips.
sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&packet+size=16384
Gültiger Bereich: 512 bis 32.767. Werte von 8.192 oder 16.384 sind für Hochdurchsatzszenarien üblich.
Behalten Sie die Standardpaketgröße bei, es sei denn, Benchmarkdaten zeigen, dass die Netzwerkübertragung die Arbeitslast dominiert. Für OLTP-ähnlichen Verkehr mit kleinen Abfragen und Zeilen erhöhen größere Pakete oft die Komplexität ohne nennenswerten Gewinn.
Verwenden Sie vorbereitete Aussagen
Prepared Statements vermeiden wiederholtes Abfrage-Parsing und planen die Kompilierung auf dem Server. Verwenden db.PrepareContext , wenn du dieselbe Abfrage mehrfach mit unterschiedlichen Parametern ausführst:
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
Schließen Sie vorbereitete Anweisungen mit defer stmt.Close(), um zu vermeiden, dass Handles für serverseitige vorbereitete Anweisungen verbraucht werden.
Bereite nicht automatisch jede Aussage vor. Sie helfen am meisten, wenn dieselbe Aussage immer wieder auf einem heißen Pfad läuft. Für einzelne Abfragen ist QueryContext oder ExecContext in der Regel unkomplizierter und schnell genug.
Verwenden Sie Bulk-Copy für große Einsätze
Einzelne Anweisungen INSERT sind für große Datenmengen langsam. Bulk copy überträgt Daten direkt an den Server und umgeht den Abfrageprozessor:
stmt, err := txn.Prepare(mssql.CopyIn("MyTable", mssql.BulkOptions{
Tablock: true,
RowsPerBatch: 5000,
}, "Col1", "Col2"))
Details finden Sie unter Massenbetriebe.
Verwenden Sie Varchar, wenn es angemessen ist
Standardmäßig sendet der Code string-Parameter als nvarchar (Unicode). Wenn Ihre Spalten verwenden varchar, könnte der Server implizite Konvertierungen durchführen und Indizes überspringen. Verwenden Sie mssql.VarChar, um varchar-Parameter zu senden.
db.QueryContext(ctx, "SELECT * FROM Production.Product WHERE ProductNumber = @p1",
mssql.VarChar("FR-R92B-58"))
Verwenden Sie Kontext-Timeouts
Setzen Sie Kontextfristen für einzelne Abfragen, um zu verhindern, dass blockierte SQL-Aufrufe Verbindungen festnageln und Anrufer verzögern.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM LargeTable")
Ressourcen umgehend schließen
Offene *sql.Rows-, *sql.Tx- und *sql.Conn-Objekte belegen eine Verbindung im Pool. Schließe sie immer so schnell wie möglich.
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
// process
}
return rows.Err()
Reduziere Hin- und Rückflüge
-
Fassen Sie Abfragen nach Möglichkeit zu einem einzigen Aufruf zusammen:
"SELECT ...; SELECT ...;". -
Verwenden Sie
OUTPUTKlauseln statt separaterSELECT SCOPE_IDENTITY()Aufrufe. - Verwenden Sie tabellenwertige Parameter, um mehrere Zeilen in einem einzigen Aufruf zu senden, anstatt über einzelne Inserts zu schleifen.
Prüfliste für die Leistung
| Area | Recommendation |
|---|---|
| Swimmingpool | Legen Sie MaxOpenConns basierend auf Serverlimits und der Gleichzeitigkeit der Workloads fest. |
| Swimmingpool | Setze MaxIdleConns auf mindestens die Hälfte von MaxOpenConns. |
| Network | Ändere packet size erst nach dem Benchmarking großer Transfers oder Großladungen. |
| Queries | Verwenden Sie vorbereitete Aussagen nur für Aussagen, die sich häufig wiederholen. |
| Queries | Verwenden Sie mssql.VarChar für varchar-Spalten, um implizite Konvertierungen zu vermeiden. |
| Große Lasten | Verwenden Sie Massenkopien (mssql.CopyIn) für Batch-Einsätze. |
| Resources | Schließe Rows, Tx, und Conn Objekte prompt. |
| Timeouts | Setzen Sie Kontextfristen für alle Anfragen und Aussagen. |
| Leseintensiv | Verwendung ApplicationIntent=ReadOnly für Read-Replicas. |
| Vergleichstests | Verwenden Sie testing.B, um vor und nach der Optimierung zu messen. |
| Monitoring | Aktivieren Sie Abfragespeicher und verwenden Sie SSMS-Berichte oder Query Performance Insight. |
| Monitoring | Export db.Stats() zu Prometheus oder OpenTelemetry. |
Benchmark-Datenbankoperationen
Verwenden Sie Gos, testing.B um die Leistung von Datenbankoperationen zu messen und Optimierungsänderungen zu validieren:
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)
}
}
}
Führen Sie Benchmarks durch mit:
go test -bench=BenchmarkInsert -benchmem -count=5
Tip
Verwenden Sie -count=5 oder mehr, um statistisch aussagekräftige Ergebnisse zu erhalten. Verwenden Sie Benchstat , um Benchmark-Ergebnisse vor und nach einer Änderung zu vergleichen.
Verwenden Sie nur-lese-Routing
Wenn Ihre SQL Server-Umgebung über eine Verfügbarkeitsgruppe mit lesbaren sekundären Replikaten verfügt, leiten Sie schreibgeschützte Abfragen an das sekundäre Replikat weiter, indem Sie ApplicationIntent=ReadOnly in der Verbindungszeichenfolge festlegen:
sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly
Erstelle separate *sql.DB Instanzen für Lese- und Schreibworkloads:
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.
Abfrageplananalyse
Verwenden Sie, SET SHOWPLAN_XML um den Ausführungsplan für eine Abfrage abzurufen, ohne sie auszuführen. Diese Methode hilft Ihnen, Tabellenscans, fehlende Indizes und teure Operationen zu identifizieren:
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 beeinflusst die gesamte Verbindung. Verwenden Sie db.Conn(ctx) immer, um den SHOWPLAN-Modus auf eine dedizierte Verbindung zu beschränken.
Nutzen Sie Abfragespeicher, um langsame Anfragen zu finden
Go-Benchmarks und clientseitiges Timing zeigen dir an, wie lange eine Abfrage aus Sicht deiner Anwendung braucht, aber diese Zahl kombiniert Netzwerklatenz, Serverausführungszeit und Clientverarbeitung. Abfragespeicher erfasst Ausführungspläne und Laufzeitstatistiken auf dem Server, sodass Sie genau sehen können, wie SQL Server jede Abfrage ausgeführt hat, wie oft sie ausgeführt wurde und wie sich die Leistung im Laufe der Zeit verändert hat.
Abfragespeicher ist besonders nützlich, um Parameter-Sniffing, Planregressionen und Abfragen zu identifizieren, die die meisten Serverressourcen verbrauchen. Aktiviere es in deiner Datenbank, falls es noch nicht aktiviert ist:
ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;
Sobald aktiviert, können Sie Leistungsdaten auf verschiedene Weise überprüfen:
- SQL Server Management Studio (SSMS): Erweitern Sie Ihre Datenbank im Objekt-Explorer, öffnen Sie den Abfragespeicher-Ordner und verwenden Sie die integrierten Berichte wie Top Resource Consuming Queries, Regressed Queries und Overall Resource Consumption.
- Azure Portal: Für die Azure SQL-Datenbank öffnen Sie die Query Performance Insight Blade, um die wichtigsten ressourcenbeanspruchenden Abfragen zu sehen, ohne Werkzeuge installieren zu müssen.
-
Transact-SQL (T-SQL): Fragen Sie die
sys.query_store_runtime_statsundsys.query_store_planKatalogansichten direkt aus Ihrer Go-Anwendung ab, wenn Sie programmatischen Zugriff benötigen.
Tip
Abfragespeicher speichert Daten über Server-Neustarts hinweg, sodass Sie Leistungstrends über Tage oder Wochen hinweg analysieren können. Verwenden Sie den Regressed Queries-Bericht , um Abfragen schnell zu erkennen, die nach einer Schema- oder Codeänderung langsamer wurden.
Nutzen Sie das Performance Dashboard
Das Performance Dashboard ist ein integrierter SSMS-Bericht, der einen Echtzeitüberblick über die Gesundheit von SQL Server bietet. Klicken Sie mit der rechten Maustaste auf die Serverinstanz im SSMS Objekt-Explorer und wählen Sie Reports>Standard Reports>Performance Dashboard.
Das Dashboard zeigt Folgendes an:
- Aktuelle Wartezeiten und Engpässe.
- Aktuelle teure Anfragen.
- Trends im Bereich CPU-, I/O- und Speichernutzung.
- Aktive Benutzeranfragen und blockierte Sitzungen.
Das Performance Dashboard ist während der Entwicklung und Lasttests nützlich, um Probleme schnell zu erkennen, ohne diagnostische Abfragen zu erstellen.
Überwachen Sie serverseitige Metriken von Go
Wenn Sie Leistungsdaten des SQL Server einem Überwachungssystem wie Prometheus oder OpenTelemetry von Ihrer Go-Anwendung aus bereitstellen müssen, fragen Sie direkt dynamische Management Views (DMVs) ab:
Die teuersten Abfragen
Rufen Sie die Top 10 der nach durchschnittlicher verstrichener Zeit sortierten Abfragen ab:
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`)
Aktuelle aktive Anfragen
Liste alle aktuell ausgeführten Anfragen auf dem Server auf, mit Ausnahme deiner eigenen Sitzung:
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`)
Speichereffiziente Muster für große Ergebnisse
Bachreihen ohne Ansammlung
Verarbeiten Sie Zeilen einzeln, anstatt das gesamte Ergebnisset in einen Slice zu laden:
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()
Batch-Verarbeitung mit Keyset-Paginierung
Teile große Tabellenscans in handhabbare Abschnitte auf, um den Speicherbedarf zu begrenzen und zu vermeiden, dass eine Verbindung über längere Zeiträume gehalten wird:
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
}