go-mssqldb ile performans ayarı

Bu makale, sürücüyü SQL Server ile kullanan go-mssqldb Go uygulamalarının performansını optimize etmek için rehberlik sağlar.

En yüksek etkili değişikliklerle başlayın

Çoğu uygulama, ilk günden sürücüye özgü ayar gerektirmez. Paket boyutunu değiştirmeden önce şu adımlarla başlayın, her yere hazırlanmış ifadeler ekleyin veya toplu seçenekleri ayarlayın:

  1. Sunucu sınırlarınıza uygun bir bağlantı havuzu boyutu belirleyin.
  2. Bağlam zaman aşımları ekleyin ki engellenmiş SQL çağrıları bağlantıları sabitlemesin ve uygulama yük altında donmuş gibi görünsün.
  3. SQL Server'da yavaş sorguları, eksik indeksleri ve gereksiz gidiş-dönüşleri düzeltin.
  4. Her değişiklikten önce ve sonra iş yükünü kıyaslayın.

Birçok hizmet için, bağlantı havuzu ayarları ve sorgu şekli, paket boyutu veya ifade hazırlamadan daha önemlidir.

Bağlantı havuzu ayarı

Bağlantı database/sql havuzu en etkili performans kaldıraçıdır. Yetersiz sağlanan havuzlar, goroutine'ların bağlantı beklemesini engellemesine neden olurken, aşırı sağlanan havuzlar sunucu kaynaklarını boşa harcatır.

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

Havuz çekişmesini algılamak için db.Stats().WaitCount ve db.Stats().WaitDuration öğelerini izleyin. Daha fazla bilgi için bkz. Bağlantı havuzlama.

Paket boyutunu artırın

Varsayılan TDS paket boyutu 4.096 bayttır. Büyük sonuç kümeleri veya toplu veri aktaran iş yüklerinde, paket boyutunun artırılması ağ gidiş-dönüş sayısını azaltır.

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

Geçerli aralık: 512 ila 32.767. Yüksek veri kapasitesine sahip senaryolar için 8.192 veya 16.384 değerleri yaygındır.

Kıyaslama verileri ağ transferinin iş yükünü kontrol ettiğini göstermedikçe varsayılan paket boyutunu koruyun. Küçük sorgular ve satırlarla OLTP tarzı trafik için, daha büyük paketler genellikle anlamlı bir kazanç olmadan karmaşıklık katıyor.

Hazırlanmış ifadeler kullanın

Hazırlanmış deyimler, sunucuda sorguların tekrar tekrar ayrıştırılmasını ve yürütme planının derlenmesini önler. Aynı sorguyu farklı parametrelerle birçok kez çalıştırdığınızda kullanın 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)
}

Tavsiye

Sunucu tarafındaki hazırlanmış deyim tutamaçlarının tüketilmesini önlemek için, defer stmt.Close() kullanarak hazırlanmış deyimleri kapatın.

Her ifadeyi varsayılan olarak hazırlamayın. Aynı ifade tekrar tekrar sıcak bir yolda ilerlediğinde en çok yardımcı olurlar. Tek seferlik sorgular için QueryContext veya ExecContext genellikle daha pratik ve yeterince hızlıdır.

Büyük eklemeler için toplu kopya kullanın

Bireysel ifadeler INSERT büyük veri yükleri için yavaştır. Toplu kopyalama, verileri doğrudan sunucuya aktarır ve sorgu işlemcisini atlar:

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

Detaylar için bkz. Toplu operasyonlar.

Uygun olduğunda varchar kullanın

Varsayılan olarak kod, string parametrelerini nvarchar (Unicode) olarak gönderir. Sütunlarınız varchar kullanıyorsa, sunucu örtük dönüşüm gerçekleştirebilir ve indeksleri kullanmayabilir. Parametreleri göndermek için mssql.VarCharvarchar kullanın.

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

Bağlam zaman aşımlarını kullanın

Bloklanan SQL çağrılarının bağlantıları sabitlemesini ve arayanları durdurmasını önlemek için bireysel sorgularda bağlam son tarihleri belirleyin.

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

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

Kaynakları derhal kapatın

Açık *sql.Rows, *sql.Tx, ve *sql.Conn nesneler havuzdan bir bağlantı sabitler. Her zaman mümkün olan en kısa sürede kapatın.

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

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

Gidiş-dönüş sayısını azaltın

  • Mümkün olduğunda toplu sorguları tek bir çağrıda kullanır: "SELECT ...; SELECT ...;".
  • Ayrı OUTPUT çağrılar yerine SELECT SCOPE_IDENTITY().
  • Tek tek ekleme işlemleri için döngü kullanmak yerine, birden fazla satırı tek bir çağrıda göndermek için tablo değerli parametreleri kullanın.

Performans denetim listesi

Area Recommendation
Havuz Sunucu sınırları ve iş yükü eşzamanlılığına göre ayarlanır MaxOpenConns .
Havuz MaxIdleConns değerini MaxOpenConns değerinin en az yarısına ayarlayın.
Network packet size Değişim sadece büyük transferler veya toplu yükler karşılaştırıldığında yapılır.
Queries Sadece sık tekrarlanan ifadeler için hazırlanmış ifadeler kullanın.
Queries Örtük dönüşümlerden kaçınmak için varchar sütunlar için mssql.VarChar kullanın.
Büyük yükler Toplu eklemeler için toplu kopyalama (mssql.CopyIn) kullanın.
Resources Rows, Tx ve Conn nesnelerini derhal kapatın.
Timeouts Tüm sorgular ve ifadeler için bağlam son tarihleri belirleyin.
Okuma ağırlıklı Okuma replikaları için ApplicationIntent=ReadOnly kullanın.
Benchmarks Optimizasyondan önce ve sonra ölçmek için kullanılır testing.B .
İzleme Query Store'u etkinleştirin ve SSMS raporlarını veya Query Performance Insight kullanın.
İzleme db.Stats() öğesini Prometheus veya OpenTelemetry'ye dışa aktarın.

Benchmark veritabanı işlemleri

Go'ları veritabanı işlemlerinin performansını ölçmek ve optimizasyon değişikliklerini doğrulamak için kullanın 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)
        }
    }
}

Kıyaslamaları şu şekilde çalıştırın:

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

Tavsiye

İstatistiksel olarak anlamlı sonuçlar elde etmek için -count=5 veya üzerini kullanın. Benchstat kullanarak bir değişiklikten önce ve sonra kıyaslama sonuçlarını karşılaştırın.

Sadece okunabilir yönlendirme kullanın

SQL Server ortamınızda okunabilir ikincil çoğaltmalar içeren bir kullanılabilirlik grubu varsa, bağlantı dizesinde ApplicationIntent=ReadOnly ayarını yaparak salt okunur sorguları ikincil çoğaltmaya yönlendirin:

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

Okuma ve yazma iş yükleri için ayrı *sql.DB örnekler oluşturun:

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.

Sorgu planı analizi

SET SHOWPLAN_XML öğesini, bir sorguyu çalıştırmadan yürütme planını almak için kullanın. Bu yöntem, tablo taramalarını, eksik indeksleri ve pahalı işlemleri belirlemenize yardımcı olur:

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 tüm bağlantıyı etkiler. Her zaman SHOWPLAN modunu özel bir bağlantıya izole etmek için kullanın db.Conn(ctx) .

Yavaş sorguları bulmak için Query Store'u kullanın

Go benchmark'lar ve istemci tarafı zamanlama, uygulamanızın bakış açısından sorgu süresinin ne kadar sürdüğünü gösterir, ancak bu sayı ağ gecikmesi, sunucu çalışma süresi ve istemci işlemesini birleştirir. Query Store, sunucudaki yürütme planlarını ve çalışma zamanı istatistiklerini yakalar, böylece SQL Server'ın her sorguyu nasıl çalıştırdığını, ne sıklıkla çalıştığını ve performansının zamanla nasıl değiştiğini tam olarak görebilirsiniz.

Query Store, özellikle parametre koklama, plan regresyonları ve en çok sunucu kaynaklarını tüketen sorguları tespit etmek için faydalıdır. Eğer zaten etkin değilse, veritabanınızda etkinleştirin:

ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;

Etkinleştirildikten sonra, performans verilerini birkaç şekilde inceleyebilirsiniz:

  • SQL Server Management Studio (SSMS): Nesne Gezgini'da veritabanınızı genişletin, Query Store klasörünü açın ve En İyi Kaynak Tüketen Sorgular, Regressli Sorgular ve Genel Kaynak Tüketimi gibi yerleşik raporları kullanın.
  • Azure portalı: Azure SQL Veritabanı için, herhangi bir araç yüklemeden en çok kaynak tüketen sorguları görmek için Query Performance Insight blade'ini açın.
  • Transact-SQL (T-SQL): Programatik erişime ihtiyacınız varsa Go uygulamanızdan doğrudan katalog görünümlerini sorgulayın sys.query_store_runtime_statssys.query_store_plan .

Tavsiye

Query Store, sunucu yeniden başlatmaları boyunca verileri tutar, böylece performans trendlerini günler veya haftalar boyunca analiz edebilirsiniz. Bir şema veya kod değişikliğinden sonra yavaşlayan sorguları hızlıca tespit etmek için Regressli Sorgular raporunu kullanın.

Performans Panelini Kullanın

Performans Paneli, SQL Server sağlığına gerçek zamanlı bir genel bakış sunan yerleşik bir SSMS raporudur. SSMS Nesne Gezgini'da sunucu örneğine sağ tıklayın ve Reports>Standard Reports>Performance Dashboard'u seçin.

Pano şunları gösterir:

  • Mevcut beklemeler ve darboğazlar.
  • Son zamanlarda pahalı sorular.
  • CPU, I/O ve bellek kullanım trendleri.
  • Aktif kullanıcı talepleri ve engellenmiş oturumlar.

Performance Dashboard, geliştirme ve yük testleri sırasında herhangi bir tanı sorgusu yazmadan sorunları hızlıca tespit etmek için faydalıdır.

Go'dan sunucu tarafı metriklerini izleyin

Go uygulamanızdan Prometheus veya OpenTelemetry gibi bir izleme sistemine SQL Server performans verilerini maruz bırakmanız gerekiyorsa, dinamik yönetim görünümlerini (DMV'ler) doğrudan sorgulayın:

En pahalı sorular

Ortalama geçen süreye göre sıralanmış en iyi 10 soruyu alın:

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

Mevcut aktif talepler

Sunucuda şu anda yürüten tüm istekleri listeleyin, kendi oturumunuz hariç:

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

Büyük sonuçlar için bellek verimli desenler

Satırları biriktirmeden işle

Tüm sonuç setini bir dilime yüklemek yerine satırları teker teker işleyin:

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

Anahtar kümesiyle sayfalama ile toplu işleme

Büyük tablo taramalarını yönetilebilir parçalara ayırarak bellek kullanımını sınırlayın ve bağlantıyı uzun süre tutturulmasını önleyin:

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
}