Leistungsoptimierung mit go-mssqldb

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:

  1. Setze eine begrenzte Connection Pool-Größe, die deinen Serverlimits entspricht.
  2. Fügen Sie Kontext-Timeouts hinzu, damit blockierte SQL-Aufrufe Verbindungen nicht festnageln und die App unter Last eingefroren erscheinen lässt.
  3. Beheben Sie langsame Abfragen, fehlende Indizes und unnötige Roundtrips in SQL Server.
  4. 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 OUTPUT Klauseln statt separater SELECT 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_stats und sys.query_store_plan Katalogansichten 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
}