Ajuste de rendimiento con go-mssqldb

Este artículo ofrece orientación para optimizar el rendimiento de las aplicaciones Go que utilizan el go-mssqldb controlador con SQL Server.

Empieza con los cambios de mayor impacto

La mayoría de las aplicaciones no necesitan ajustes específicos del controlador el primer día. Empieza con estos pasos antes de cambiar el tamaño del paquete, añadir declaraciones preparadas por todas partes o ajustar las opciones en bloque:

  1. Establece un tamaño de pool de conexión acotado que coincida con los límites de tu servidor.
  2. Añade tiempos de espera de contexto para que las llamadas SQL bloqueadas no mantengan ocupadas las conexiones y la aplicación parezca congelada cuando hay carga.
  3. Soluciona consultas lentas, índices ausentes y viajes de ida y vuelta innecesarios en SQL Server.
  4. Haz benchmarks de la carga de trabajo antes y después de cada cambio.

Para muchos servicios, la configuración del pool de conexiones y la forma de la consulta importan más que el tamaño del paquete o la preparación de la sentencia.

Ajuste del grupo de conexiones

El database/sql grupo de conexiones es el factor de rendimiento más importante. Los grupos infraaprovisionados hacen que las goroutines se bloqueen mientras esperan conexiones, mientras que los grupos sobreaprovisionados desperdician recursos del servidor.

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

Supervise db.Stats().WaitCount y db.Stats().WaitDuration para detectar la contención del pool. Para más detalles, véase Agrupación de conexiones.

Aumentar el tamaño del paquete

El tamaño predeterminado del paquete TDS es de 4.096 bytes. Para cargas de trabajo que transfieren grandes conjuntos de resultados o datos masivos, aumentar el tamaño del paquete reduce el número de viajes de ida y vuelta de red.

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

Rango válido: 512 a 32.767. Valores de 8.192 o 16.384 son comunes en escenarios de alto rendimiento.

Mantén el tamaño de paquete por defecto a menos que los datos de referencia demuestren que la transferencia de red domina la carga de trabajo. Para tráfico tipo OLTP con consultas y filas pequeñas, los paquetes más grandes suelen añadir complejidad sin una ganancia significativa.

Utiliza declaraciones preparadas

Las sentencias preparadas evitan el análisis repetido de consultas y la compilación de planes en el servidor. Úsalo db.PrepareContext cuando ejecutas la misma consulta muchas veces con parámetros diferentes:

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

Cierra las sentencias preparadas usando defer stmt.Close() para evitar consumir las gestiones de sentencias preparadas del lado del servidor.

No prepares cada declaración por defecto. Son más útiles cuando la misma instrucción se ejecuta repetidamente en una ruta crítica. Para consultas puntuales, QueryContext o ExecContext suele ser más directo y rápido.

Utiliza la copia en bloque para inserciones grandes

Las sentencias individuales INSERT son lentas para cargas de datos grandes. La copia masiva transmite datos directamente al servidor, evitando el procesador de consultas:

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

Para obtener más información, consulta Operaciones masivas.

Usa varchar cuando sea apropiado

Por defecto, el código envía string parámetros como nvarchar (Unicode). Si tus columnas usan varchar, el servidor podría realizar conversión implícita y saltar índices. Usa mssql.VarChar para enviar parámetros varchar.

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

Usa los tiempos de espera de contexto

Establece plazos contextuales para consultas individuales para evitar que las llamadas SQL bloqueadas bloqueen conexiones y retrasen a los llamantes.

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

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

Cerrar los recursos con urgencia

Abre *sql.Rows, *sql.Tx, y *sql.Conn los objetos fijan una conexión desde el pool. Ciérralos siempre lo antes posible.

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

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

Reducir los viajes de ida y vuelta

  • Agrupa las consultas en una sola llamada cuando sea posible: "SELECT ...; SELECT ...;".
  • Usa OUTPUT cláusulas en lugar de SELECT SCOPE_IDENTITY() llamadas separadas.
  • Utiliza parámetros con valores de tabla para enviar varias filas en una sola llamada en lugar de bucle sobre insertos individuales.

Lista de comprobación de rendimiento

Area Recommendation
piscina Establezca MaxOpenConns según los límites de servidores y la concurrencia de carga de trabajo.
piscina Configure MaxIdleConns como mínimo en la mitad de MaxOpenConns.
Network Cambie packet size solo después de realizar pruebas de rendimiento de transferencias de gran tamaño o cargas masivas.
Queries Usa declaraciones preparadas solo para afirmaciones que se repiten con frecuencia.
Queries Use mssql.VarChar para las columnas varchar para evitar conversiones implícitas.
Grandes cargas Usa copia masiva (mssql.CopyIn) para insertos por lotes.
Resources Cierra Rows, Tx, y Conn objetos rápidamente.
Tiempos de expiración Establece plazos contextuales para todas las consultas y sentencias.
Lectura intensa Usa ApplicationIntent=ReadOnly para réplicas de lectura.
Benchmarks Úsalo testing.B para medir antes y después de la optimización.
Monitorización Activa Almacén de consultas y utiliza informes SSMS o Query Performance Insight.
Monitorización Exportar db.Stats() a Prometheus o OpenTelemetry.

Operaciones de bases de datos de referencia

Utiliza testing.B de Go para medir el rendimiento de las operaciones en la base de datos y validar los cambios de optimización:

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

Ejecuta pruebas de rendimiento con:

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

Tip

Úsalo -count=5 o más para obtener resultados estadísticamente significativos. Usa benchstat para comparar los resultados de benchmarks antes y después de un cambio.

Utilizar enrutamiento de solo lectura

Si el entorno de SQL Server tiene un grupo de disponibilidad con secundarios legibles, dirija las consultas de solo lectura a la réplica secundaria estableciendo ApplicationIntent=ReadOnly en la cadena de conexión:

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

Crea instancias separadas *sql.DB para cargas de trabajo de lectura y escritura:

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.

Análisis del plan de consulta

Úsalo SET SHOWPLAN_XML para recuperar el plan de ejecución de una consulta sin ejecutarla. Este método te ayuda a identificar escaneos de tablas, índices ausentes y operaciones costosas:

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 afecta a toda la conexión. Use siempre db.Conn(ctx) para limitar el modo SHOWPLAN a una conexión dedicada.

Usa Almacén de consultas para encontrar consultas lentas

Los benchmarks de Go y el timing del lado del cliente te indican cuánto tarda una consulta desde la perspectiva de tu aplicación, pero ese número combina latencia de red, tiempo de ejecución del servidor y procesamiento del cliente. Almacén de consultas captura planes de ejecución y estadísticas de ejecución en el servidor, para que puedas ver exactamente cómo SQL Server ejecutó cada consulta, con qué frecuencia se ejecutó y cómo cambió su rendimiento con el tiempo.

Almacén de consultas es especialmente útil para identificar la captura de parámetros, las regresiones en los planes de ejecución y las consultas que más recursos del servidor consumen. Actívalo en tu base de datos si aún no está activado:

ALTER DATABASE [AdventureWorks2025] SET QUERY_STORE = ON;

Una vez habilitados, puedes revisar los datos de rendimiento de varias maneras:

  • SQL Server Management Studio (SSMS): Amplía tu base de datos en Explorador de objetos, abre la carpeta Almacén de consultas y utiliza los informes integrados como Consultas que consumen más recursos, Consultas regresadas y Consumo General de Recursos.
  • Portal Azure: Para Azure SQL Database, abre la hoja Query Performance Insight para ver las consultas que más recursos consumen sin instalar ninguna herramienta.
  • Transact-SQL (T-SQL): Consulta directamente las vistas de catálogo sys.query_store_runtime_stats y sys.query_store_plan desde tu aplicación Go si necesitas acceso programático.

Tip

Almacén de consultas mantiene los datos entre los reinicios del servidor, así que puedes analizar tendencias de rendimiento durante días o semanas. Utiliza el informe de Consultas Regresadas para detectar rápidamente consultas que se han vuelto más lentas tras un cambio de esquema o código.

Utiliza el Panel de Rendimiento

El Panel de Rendimiento es un informe SSMS integrado que ofrece una visión general en tiempo real del estado de SQL Server. Haz clic con el botón derecho en la instancia del servidor en el Explorador de objetos de SSMS y selecciona Informes>Informes estándar>Performance Dashboard.

El panel muestra lo siguiente:

  • Esperas actuales y cuellos de botella.
  • Consultas recientes y caras.
  • Tendencias de uso de CPU, E/S y memoria.
  • Solicitudes de usuarios activos y sesiones bloqueadas.

El Panel de Rendimiento es útil durante el desarrollo y las pruebas de carga para detectar rápidamente problemas sin necesidad de escribir consultas diagnósticas.

Monitorizar las métricas del lado del servidor desde Go

Si necesitas exponer datos de rendimiento de SQL Server a un sistema de monitorización como Prometheus u OpenTelemetry desde tu aplicación Go, consulta directamente las vistas de gestión dinámica (DMV):

Consultas más caras

Consulta las 10 consultas principales ordenadas por tiempo medio transcurrido:

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

Solicitudes activas actuales

Enumera todas las solicitudes que se están ejecutando actualmente en el servidor, excluyendo tu propia sesión:

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

Patrones eficientes en memoria para resultados grandes

Transmitir filas sin acumular

Procesa las filas una a una en lugar de cargar todo el conjunto de resultados en una porción:

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

Procesamiento por lotes con paginación por keyset

Divide los escaneos de tablas grandes en bloques manejables para limitar el uso de memoria y evitar mantener una conexión durante períodos prolongados:

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
}