Prestandajustering med go-mssqldb

Den här artikeln ger vägledning för hur man optimerar prestandan hos Go-applikationer som använder drivrutinen go-mssqldb med SQL Server.

Börja med de förändringar med störst påverkan

De flesta applikationer behöver inte drivrutinsspecifik justering redan från dag ett. Börja med dessa steg innan du ändrar paketstorlek, lägger till förberedda satser överallt eller justerar bulkalternativ:

  1. Sätt en begränsad anslutningspoolstorlek som matchar dina servergränser.
  2. Lägg till kontexttidsavbrott så att blockerade SQL-anrop inte låser anslutningar och gör att appen ser frusen ut under belastning.
  3. Åtgärda långsamma frågor, saknade index och onödiga rundturer i SQL Server.
  4. Benchmarka arbetsbelastningen före och efter varje ändring.

För många tjänster spelar anslutningspoolinställningar och frågeform större roll än paketstorlek eller formulering av satser.

Justering av anslutningspool

database/sql anslutningspoolen är den faktor som påverkar prestandan mest. Underprovisionerade pooler gör att goroutines blockerar väntan på anslutningar, medan överprovisionerade pooler slösar serverresurser.

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

Övervaka db.Stats().WaitCount och db.Stats().WaitDuration för att upptäcka konkurrens i poolen. För mer information, se Connection pooling.

Öka paketstorleken

Standard TDS-paketstorlek är 4 096 byte. För arbetsbelastningar som överför stora resultatuppsättningar eller bulkdata minskar en ökning av paketstorleken antalet nätverksrundturer.

sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&packet+size=16384

Giltigt intervall: 512 till 32 767. Värden på 8 192 eller 16 384 är vanliga för högkapacitetsscenarier.

Behåll standardpaketstorleken om inte benchmarkdata visar att nätverksöverföring dominerar arbetsbelastningen. För OLTP-liknande trafik med små frågor och rader tillför större paket ofta komplexitet utan någon meningsfull vinst.

Använd förberedda uttalanden

Förberedda satser undviker upprepad frågeparsning och planerar kompilering på servern. Använd db.PrepareContext när du kör samma fråga många gånger med olika parametrar:

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

Stäng förberedda instruktioner med defer stmt.Close() för att undvika att förbruka handtag för förberedda instruktioner på serversidan.

Förbered inte varje uttalande som standard. De hjälper mest när samma påstående upprepas i en het väg. För enstaka frågor är QueryContext eller ExecContext oftast enklare och snabbt nog.

Använd masskopiering för stora infogningar

Enskilda INSERT uttalanden är långsamma för stora datamängder. Masskopiering strömmar data direkt till servern och kringgår frågeprocessorn:

stmt, err := txn.Prepare(mssql.CopyIn("MyTable", mssql.BulkOptions{
    Tablock:      true,
    RowsPerBatch: 5000,
}, "Col1", "Col2"))

Mer information finns i Massåtgärder.

Använd varchar när det är lämpligt

Som standard skickar string koden parametrar som nvarchar (Unicode). Om dina kolumner använder varchar, kan servern utföra implicit konvertering och hoppa över index. Använd mssql.VarChar för att skicka varchar parametrar.

db.QueryContext(ctx, "SELECT * FROM Production.Product WHERE ProductNumber = @p1",
    mssql.VarChar("FR-R92B-58"))

Använd tidsgränser för kontext

Ange tidsgränser för kontexten för enskilda frågor för att förhindra att blockerade SQL-anrop binder upp anslutningar och fördröjer anropande processer.

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT * FROM LargeTable")

Stäng resurser snabbt

Objekt av typen *sql.Rows, *sql.Tx och *sql.Conn som är öppna binder upp en anslutning från poolen. Stäng dem alltid så snart som möjligt.

rows, err := db.QueryContext(ctx, query)
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    // process
}
return rows.Err()

Minska tur- och returresor

  • Batcha förfrågningar i ett enda anrop när det är möjligt: "SELECT ...; SELECT ...;"
  • Använd OUTPUT satser istället för separata SELECT SCOPE_IDENTITY() anrop.
  • Använd tabellvärda parametrar för att skicka flera rader i ett enda samtal istället för att loopa över enskilda inserts.

Checklista för prestanda

Area Recommendation
Bassäng Ställ in MaxOpenConns utifrån serverns begränsningar och hur många arbetsbelastningar som körs samtidigt.
Bassäng Ställ MaxIdleConns in på minst hälften av MaxOpenConns.
Network Byt packet size först efter att ha prestandatestat stora överföringar eller massinläsningar.
Queries Använd förberedda uttalanden endast för uttalanden som upprepas ofta.
Queries Använd mssql.VarChar för varchar kolumner för att undvika implicita konverteringar.
Stora laster Använd bulkkopiering (mssql.CopyIn) för batchinsatser.
Resources Stäng Rows, Tx, och Conn objekt omedelbart.
Timeouts Sätt kontextdeadlines för alla frågor och uttalanden.
Lästung krävande Använd ApplicationIntent=ReadOnly för läsrepliker.
Prestandajämförelser Använd testing.B för att mäta före och efter optimering.
Monitoring Aktivera Query Store och använd SSMS-rapporter eller Query Performance Insight.
Monitoring Exportera db.Stats() till Prometheus eller OpenTelemetry.

Prestandatest av databasoperationer

Använd Gos testing.B för att mäta prestandan i databasoperationer och validera optimeringsändringar:

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)
        }
    }
}

Kör benchmarks med:

go test -bench=BenchmarkInsert -benchmem -count=5

Tip

Använd -count=5 eller högre för att få statistiskt meningsfulla resultat. Använd benchstat för att jämföra benchmarkresultat före och efter en förändring.

Använd skrivskyddad routing

Om din SQL Server-miljö har en tillgänglighetsgrupp med läsbara sekundärfiler, dirivar read-only-frågor till den sekundära repliken genom att ange ApplicationIntent=ReadOnly i reťazec pripojenia:

sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly

Skapa separata *sql.DB instanser för läs- och skrivarbetsbelastningar:

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.

Analys av frågeplan

Använd SET SHOWPLAN_XML för att hämta exekveringsplanen för en fråga utan att köra den. Denna metod hjälper dig att identifiera tabellskanningar, saknade index och dyra operationer:

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
}

Varning

SET SHOWPLAN_XML ON påverkar hela anslutningen. Använd alltid db.Conn(ctx) för att isolera SHOWPLAN-läget till en dedikerad anslutning.

Använd Query Store för att hitta långsamma frågor

Go-benchmarks och klienttidtagning visar hur lång tid en fråga tar ur applikationens perspektiv, men det antalet kombinerar nätverkslatens, serverexekveringstid och klientbearbetning. Query Store fångar exekveringsplaner och körningsstatistik på servern, så du kan se exakt hur SQL Server körde varje fråga, hur ofta den kördes och hur dess prestanda förändrades över tid.

Query Store är särskilt användbart för att identifiera parametersniffing, planregressioner och frågor som förbrukar mest serverresurser. Aktivera det i din databas om det inte redan är aktiverat:

ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;

När det är aktiverat kan du granska prestandadata på flera sätt:

  • SQL Server Management Studio (SSMS): Expandera din databas i Object Explorer, öppna mappen Query Store och använd inbyggda rapporter som Top Resource Consuming Queries, Regressed Queries och Overall Resource Consumption.
  • Azure-portalen: För Azure SQL Database, öppna bladet Query Performance Insight för att se de mest resurskrävande frågorna utan att installera några verktyg.
  • Transact-SQL (T-SQL): Fråga sys.query_store_runtime_stats- och sys.query_store_plan-katalogvyerna direkt från din Go-applikation om du behöver programmatisk åtkomst.

Tip

Query Store lagrar data över serveromstarter, så du kan analysera prestandatrender över dagar eller veckor. Använd rapporten Regressed Queries för att snabbt upptäcka frågor som blev långsammare efter en schema- eller kodändring.

Använd prestandainstrumentpanelen

Performance Dashboard är en inbyggd SSMS-rapport som ger en realtidsöversikt över SQL Server:s hälsa. Högerklicka på serverinstansen i SSMS Object Explorer och välj Reports>Standard Reports>Performance Dashboard.

Instrumentpanelen visar:

  • Nuvarande väntetider och flaskhalsar.
  • Senaste kostsamma frågorna
  • CPU-, I/O- och minnesanvändningstrender.
  • Aktiva användarförfrågningar och blockerade sessioner.

Prestandainstrumentpanelen är användbar under utveckling och belastningstestning för att snabbt upptäcka problem utan att behöva skriva några diagnostiska frågor.

Övervaka server-side-metrik från Go

Om du behöver göra prestandadata från SQL Server tillgängliga för ett övervakningssystem som Prometheus eller OpenTelemetry från din Go-applikation, fråga dynamiska hanteringsvyer (DMV:er) direkt:

De dyraste frågorna

Hämta de 10 vanligaste frågorna rankade efter genomsnittlig förfluten tid:

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`)

Nuvarande aktiva förfrågningar

Lista alla för närvarande körande förfrågningar på servern, exklusive din egen session:

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`)

Minneseffektiva mönster för stora resultat

Strömma rader utan att ackumulera

Bearbeta rader en i taget i stället för att läsa in hela resultatuppsättningen i en 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()

Batchbearbetning med tangentsättspaginering

Dela upp stora tabellsskanningar i hanterbara delar för att begränsa minnesanvändningen och undvika att hålla en anslutning under längre perioder:

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
}