Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Ten artykuł zawiera wskazówki dotyczące optymalizacji wydajności aplikacji Go, które korzystają ze sterownika go-mssqldb wraz z SQL Server.
Zacznij od zmian o największym wpływie
Większość aplikacji nie wymaga tuningu specyficznego dla sterownika od pierwszego dnia. Zacznij od wykonania tych kroków, zanim zmienisz rozmiar pakietu, dodasz wszędzie instrukcje przygotowane lub dostosujesz opcje operacji zbiorczych:
- Ustaw ograniczony rozmiar puli połączeń, który odpowiada limitom twojego serwera.
- Dodaj limity czasu kontekstu, aby zablokowane wywołania SQL nie blokowały połączeń i nie sprawiały, że aplikacja wygląda na zawieszoną pod obciążeniem.
- Usuń wolne zapytania, brak indeksów i zbędne połączenia zwrotne w SQL Server.
- Porównaj obciążenie pracą przed i po każdej zmianie.
Dla wielu usług ustawienia puli połączeń i kształt zapytania mają większe znaczenie niż wielkość pakietu czy przygotowanie instrukcji.
Strojenie puli połączeń
Pula database/sql połączeń jest najbardziej wpływową dźwignią wydajności. Zbyt małe pule powodują blokowanie gorutin podczas oczekiwania na połączenia, podczas gdy zbyt duże pule marnują zasoby serwera.
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
Monitoruj db.Stats().WaitCount i db.Stats().WaitDuration, aby wykrywać rywalizację o pulę. Więcej informacji znajdziesz w sekcji Pula połączeń.
Zwiększenie rozmiaru pakietu
Domyślny rozmiar pakietu TDS wynosi 4 096 bajtów. W przypadku obciążeń przesyłających duże zbiory wyników lub dane masowe, zwiększenie rozmiaru pakietu zmniejsza liczbę przejściów w sieci.
sqlserver://<user>:<password>@<server>?database=AdventureWorks2025&packet+size=16384
Ważny zakres: od 512 do 32 767. Wartości 8 192 lub 16 384 są powszechne dla scenariuszy o wysokiej przepustowości.
Zachowaj domyślny rozmiar pakietu, chyba że dane benchmarkowe pokażą, że transfer sieciowy dominuje w obciążeniu. W przypadku obciążenia typu OLTP, z małymi zapytaniami i wierszami, większe pakiety często zwiększają złożoność bez istotnych korzyści.
Używaj przygotowanych stwierdzeń
Instrukcje przygotowane eliminują konieczność ponownego parsowania zapytań i kompilacji planu wykonania na serwerze. Używaj, db.PrepareContext gdy wykonujesz to samo zapytanie wiele razy z różnymi parametrami:
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)
}
Wskazówka
Zamykaj przygotowane instrukcje za pomocą defer stmt.Close(), aby uniknąć zużywania uchwytów przygotowanych instrukcji po stronie serwera.
Nie przygotowuj każdego wyciągu domyślnie. Najbardziej pomagają, gdy to samo zdanie powtarza się na gorącej ścieżce. Na jednorazowe zapytania QueryContext lub ExecContext zwykle jest to bardziej proste i wystarczająco szybkie.
Używaj kopiowania zbiorczego do dużych operacji wstawiania
Poszczególne INSERT instrukcje są wolne przy dużych ilościach danych. Kopiowanie masowe przesyła dane bezpośrednio do serwera, omijając procesor zapytań:
stmt, err := txn.Prepare(mssql.CopyIn("MyTable", mssql.BulkOptions{
Tablock: true,
RowsPerBatch: 5000,
}, "Col1", "Col2"))
Szczegóły można znaleźć w sekcji Operacje masowe.
Używaj varchar w odpowiednich przypadkach
Domyślnie kod wysyła string parametry jako nvarchar (Unicode). Jeśli twoje kolumny używają varchar, serwer może wykonać konwersję niejawną i pominąć indeksy. Użyj mssql.VarChar, aby wysłać parametry varchar.
db.QueryContext(ctx, "SELECT * FROM Production.Product WHERE ProductNumber = @p1",
mssql.VarChar("FR-R92B-58"))
Używaj czasów kontekstowych
Ustaw limity czasu kontekstu dla poszczególnych zapytań, aby zapobiec blokowaniu połączeń przez zablokowane wywołania SQL i wstrzymywaniu kodu wywołującego.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM LargeTable")
Szybko zamknij zasoby
Otwórz *sql.Rows, *sql.Tx, a obiekty przypinają *sql.Conn połączenie z pulą. Zawsze zamykaj je jak najszybciej.
rows, err := db.QueryContext(ctx, query)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
// process
}
return rows.Err()
Zmniejsz liczbę podróży w obie strony
-
Łącz wiele zapytań w jedno wywołanie, gdy to możliwe:
"SELECT ...; SELECT ...;". -
Używaj
OUTPUTklauzul zamiast oddzielnychSELECT SCOPE_IDENTITY()wywołań. - Używaj parametrów o wartościach tabelarycznych, aby przekazać wiele wierszy w jednym wywołaniu zamiast wykonywać w pętli pojedyncze operacje wstawiania.
Lista kontrolna wydajności
| Area | Zalecenie |
|---|---|
| Pula | Ustaw MaxOpenConns na podstawie limitów serwera i równoległości obciążenia. |
| Pula | Ustaw MaxIdleConns na co najmniej połowę wartości MaxOpenConns. |
| Network | Zmiana packet size tylko po benchmarkowaniu dużych transferów lub ładunków masowych. |
| Queries | Używaj przygotowanych zdań tylko wtedy, gdy powtarzają się często. |
| Queries | Użyj mssql.VarChar dla kolumn varchar, aby uniknąć niejawnych konwersji. |
| Duże obciążenia | Używaj kopiowania zbiorczego (mssql.CopyIn) do wstawiania wsadowego. |
| Resources | Zamknij niezwłocznie obiekty Rows, Tx i Conn. |
| Timeouts | Ustal terminy kontekstowe dla wszystkich zapytań i wypowiedzi. |
| Dużo czytania | Zastosowanie ApplicationIntent=ReadOnly do odczytu replik. |
| Benchmarks | Używaj testing.B do pomiaru przed i po optymalizacji. |
| Nadzorowanie | Włącz Query Store i korzystaj z raportów SSMS lub Query Performance Insight. |
| Nadzorowanie | Eksportuj db.Stats() do Prometheus lub OpenTelemetry. |
Operacje bazy danych benchmark
Użyj Go's testing.B do mierzenia wydajności operacji bazy danych i weryfikacji zmian w optymalizacji:
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)
}
}
}
Uruchom benchmarki z:
go test -bench=BenchmarkInsert -benchmem -count=5
Wskazówka
Użyj -count=5 lub więcej, aby uzyskać statystycznie istotne wyniki. Użyj benchstat , aby porównać wyniki benchmarków przed i po zmianie.
Używaj routingu tylko do odczytu
Jeśli środowisko SQL Server zawiera grupę dostępności z czytelnymi replikami pomocniczymi, kieruj zapytania tylko do odczytu do repliki pomocniczej, ustawiając parametr ApplicationIntent=ReadOnly w parametrach połączenia:
sqlserver://<user>:<password>@mylistener?database=AdventureWorks2025&ApplicationIntent=ReadOnly
Stwórz osobne *sql.DB instancje dla obciążeń odczytu i zapisu:
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.
Analiza planu zapytań
Użyj SET SHOWPLAN_XML do pobrania planu wykonania zapytania bez jego uruchomienia. Ta metoda pomaga zidentyfikować skanowanie tabel, brakujące indeksy oraz kosztowne operacje:
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 Wpływa na całe połączenie. Zawsze używaj db.Conn(ctx) do izolowania trybu SHOWPLAN do dedykowanego połączenia.
Użyj Query Store, aby znaleźć powolne zapytania
Benchmarki Go i pomiar czasu po stronie klienta mówią, jak długo trwa zapytanie z perspektywy aplikacji, ale ta wartość obejmuje opóźnienie sieci, czas wykonywania po stronie serwera i przetwarzanie po stronie klienta. Query Store rejestruje plany wykonania i statystyki wykonawcze na serwerze, dzięki czemu możesz dokładnie zobaczyć, jak SQL Server wykonywał każde zapytanie, jak często się uruchamiało i jak zmieniała się jego wydajność w czasie.
Query Store jest szczególnie przydatny do identyfikacji parametrów sniffingu, regresji planowych oraz zapytań zużywających najwięcej zasobów serwera. Włącz go w swojej bazie danych, jeśli nie jest jeszcze włączony:
ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;
Po włączeniu możesz przeglądać dane wydajności na kilka sposobów:
- SQL Server Management Studio (SSMS): Rozwiń swoją bazę danych w Eksplorator obiektów, otwórz folder Query Store i korzystaj z wbudowanych raportów, takich jak Top Resource Consuming Queries, Regressed Queries oraz Overall Resource Consumption.
- Portal Azure: Dla Azure SQL Database otwórz blade Query Performance Insight, aby zobaczyć najczęściej zużywające zasoby zapytania bez konieczności instalowania narzędzi.
-
Transact-SQL (T-SQL): W razie potrzeby dostępu programowego możesz wykonywać zapytania bezpośrednio do widoków katalogowych
sys.query_store_runtime_statsisys.query_store_planz poziomu aplikacji Go.
Wskazówka
Query Store przechowuje dane przy restartach serwera, dzięki czemu możesz analizować trendy wydajności na przestrzeni dni lub tygodni. Użyj raportu Regressed Queries , aby szybko wykrywać zapytania, które zwolniły po zmianie schematu lub kodu.
Korzystaj z panelu wydajności
Performance Dashboard to wbudowany raport SSMS, który daje przegląd stanu SQL Server w czasie rzeczywistym. Kliknij prawym przyciskiem myszy instancję serwera w SSMS Eksplorator obiektów i wybierz Reports> StandardReports>Performance Dashboard.
Na pulpicie nawigacyjnym przedstawiono następujące elementy:
- Obecne oczekiwania i wąskie gardła.
- Ostatnie kosztowne zapytania.
- Trendy w wykorzystaniu CPU, I/O i pamięci.
- Aktywne żądania użytkowników i zablokowane sesje.
Panel wydajności jest przydatny podczas rozwoju i testów obciążeniowych, aby szybko wykrywać problemy bez konieczności pisania zapytań diagnostycznych.
Monitorowanie metryk po stronie serwera z Go
Jeśli chcesz udostępnić dane o wydajności SQL Server w systemie monitoringu, takim jak Prometheus lub OpenTelemetry, z poziomu aplikacji Go, odpytywaj bezpośrednio dynamiczne widoki zarządzania (DMV):
Najdroższe zapytania
Pobierz 10 najczęściej zapytań uszeregowanych według średniego upływu czasu:
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`)
Aktualne aktywne żądania
Wypisz wszystkie obecnie wykonywane żądania na serwerze, z wyjątkiem własnej sesji:
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`)
Wzorce efektywne pod względem pamięci dla dużych wyników
Rzędy strumieni bez akumulacji
Przetwarzaj wiersze po jednym zamiast ładować cały zestaw wyników do 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()
Przetwarzanie wsadowe z paginacją opartą na kluczach
Podziel duże skany tabel na zarządzalne fragmenty, aby ograniczyć zużycie pamięci i uniknąć utrzymywania połączenia przez dłuższy czas:
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
}