Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
El go-mssqldb controlador utiliza la interfaz estándar database/sql para ejecutar consultas y sentencias. Este artículo aborda patrones comunes para el acceso a datos con el controlador.
Ejecución de una consulta SELECT
Úsase QueryContext para ejecutar una consulta que devuelva filas:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName FROM Sales.vSalesPerson WHERE CountryRegionName = @p1",
sql.Named("p1", "Australia"))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int
var name, location string
if err := rows.Scan(&id, &name, &location); err != nil {
log.Fatal(err)
}
fmt.Printf("%d: %s (%s)\n", id, name, location)
}
if err = rows.Err(); err != nil {
log.Fatal(err)
}
Importante
Llama siempre a rows.Close() (normalmente con defer) y comprueba rows.Err() después del bucle. No cerrar los rows puede provocar fugas de conexiones del pool.
rows.Close() también puede devolver un error del lado del servidor mientras el controlador vacía los tokens restantes, así que no lo ignores cuando el conjunto de resultados no se haya consumido por completo.
Si dejas de leer antes de terminar, cierra las filas explícitamente y gestiona el error al cerrarlas:
rows, err := db.QueryContext(ctx,
"SELECT TOP (100) ProductID, Name FROM Production.Product ORDER BY ProductID")
if err != nil {
log.Fatal(err)
}
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
_ = rows.Close()
log.Fatal(err)
}
fmt.Printf("%d %s\n", id, name)
break // Stop early for demonstration.
}
if err := rows.Close(); err != nil {
log.Fatal(err)
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
Los ejemplos de este artículo se aplican a la base de datos de ejemplo AdventureWorks2025 . Ejemplos orientados a lectura consultan objetos integrados como Sales.vSalesPerson, Production.Product, y Sales.SalesOrderHeader. Los ejemplos orientados a la escritura están dirigidos a HumanResources.Department y Production.ProductInventory.
Consulta una sola fila
Usa QueryRowContext cuando esperes exactamente una fila:
var id int
var name string
err := db.QueryRowContext(ctx,
"SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name FROM Sales.vSalesPerson WHERE BusinessEntityID = @p1",
sql.Named("p1", 280)).Scan(&id, &name)
if err == sql.ErrNoRows {
fmt.Println("No employee found.")
} else if err != nil {
log.Fatal(err)
} else {
fmt.Printf("Employee %d: %s\n", id, name)
}
Ejecutar una sentencia
Usa ExecContext para INSERT, UPDATE, DELETE, y las sentencias DDL:
result, err := db.ExecContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) VALUES (@p1, @p2)",
sql.Named("p1", "Data Science"),
sql.Named("p2", "Research and Development"))
if err != nil {
log.Fatal(err)
}
rowsAffected, _ := result.RowsAffected()
fmt.Printf("Rows affected: %d\n", rowsAffected)
Importante
El go-mssqldb controlador no soporta LastInsertId(). Llamarlo devuelve un error. Utiliza una OUTPUT cláusula o una consulta separada SELECT SCOPE_IDENTITY() para recuperar un valor de identidad insertado.
Si usas SELECT SCOPE_IDENTITY(), ejecútalo en el mismo lote o en la misma transacción que INSERT, para que el ámbito de identidad permanezca en la misma conexión.
Si un procedimiento almacenado o disparador usa SET NOCOUNT ON, RowsAffected() devuelve 0 porque SQL Server suprime el mensaje de recuento de filas. Si necesitas el recuento real, o bien quita SET NOCOUNT ON del procedimiento, o bien devuelve el recuento explícitamente mediante un parámetro de salida o una instrucción SELECT.
Consultas parametrizadas
Utiliza siempre consultas parametrizadas para evitar la inyección SQL. El controlador admite tanto parámetros posicionales como parámetros con nombre.
Importante
El go-mssqldb controlador utiliza @p1, @p2, y así sucesivamente para parámetros posicionales y sql.Named() para parámetros nombrados. La sintaxis de marcador ? que utilizan algunos otros controladores (como go-sql-driver la de MySQL) no funciona con el nombre de controlador sqlserver. Si estás migrando de otra base de datos, sustituye todos los marcadores de posición de tipo ? o $1 por marcadores de posición de tipo @p1 o parámetros con nombre.
Parámetros posicionales
Usa los marcadores @p1, @p2 y pasa los valores en orden:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
"Jared", "Australia")
Parámetros con nombre
Usar sql.Named() para vincular valores a marcadores de posición nombrados:
rows, err := db.QueryContext(ctx,
"SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @name AND CountryRegionName = @location",
sql.Named("name", "Jared"),
sql.Named("location", "Australia"))
Varios conjuntos de resultados
Use rows.NextResultSet() para recorrer varios conjuntos de resultados que devuelve un único lote o procedimiento almacenado.
Importante
Debes agotar rows.Next() completamente para cada conjunto de resultados antes de llamar rows.NextResultSet(). Si se llama a NextResultSet() antes de Next(), se devuelve false y se omiten las filas restantes de forma silenciosa.
Utiliza este patrón de bucle para procesar todos los conjuntos de resultados de forma fiable:
rows, err := db.QueryContext(ctx,
`SELECT TOP (3) ProductID, Name
FROM Production.Product
ORDER BY ProductID;
SELECT TOP (3) SalesOrderID, CONVERT(NVARCHAR(10), OrderDate, 23) AS OrderDate
FROM Sales.SalesOrderHeader
ORDER BY SalesOrderID DESC;`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
setIndex := 0
for {
switch setIndex {
case 0:
for rows.Next() {
var productID int
var productName string
if err := rows.Scan(&productID, &productName); err != nil {
log.Fatal(err)
}
fmt.Printf("Product %d: %s\n", productID, productName)
}
case 1:
for rows.Next() {
var salesOrderID int
var orderDate string
if err := rows.Scan(&salesOrderID, &orderDate); err != nil {
log.Fatal(err)
}
fmt.Printf("Order %d: %s\n", salesOrderID, orderDate)
}
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
if !rows.NextResultSet() {
break
}
setIndex++
}
Transactions
Úsalo BeginTx para iniciar una transacción con un nivel de aislamiento específico. Para una guía completa sobre transacciones, incluyendo niveles de aislamiento, puntos de guardado, gestión de bloqueos y patrones de reintentos, consulte Transacciones.
tx, err := db.BeginTx(ctx, &sql.TxOptions{
Isolation: sql.LevelSerializable,
})
if err != nil {
log.Fatal(err)
}
defer tx.Rollback()
// Subtract from source location.
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity - @p1 WHERE ProductID = @p2 AND LocationID = 1",
sql.Named("p1", 5),
sql.Named("p2", 1))
if err != nil {
log.Fatal(err)
}
// Add to destination location.
_, err = tx.ExecContext(ctx,
"UPDATE Production.ProductInventory SET Quantity = Quantity + @p1 WHERE ProductID = @p2 AND LocationID = 6",
sql.Named("p1", 5),
sql.Named("p2", 1))
if err != nil {
log.Fatal(err)
}
if err = tx.Commit(); err != nil {
log.Fatal(err)
}
Inserta valores de identidad
El go-mssqldb controlador no soporta LastInsertId(). Utiliza la cláusula OUTPUT para recuperar el valor de identidad en la misma sentencia:
var newID int64
err := db.QueryRowContext(ctx,
"INSERT INTO HumanResources.Department (Name, GroupName) OUTPUT INSERTED.DepartmentID VALUES (@name, @grp)",
sql.Named("name", "Data Science"),
sql.Named("grp", "Research and Development")).Scan(&newID)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted department with ID: %d\n", newID)
Para varias filas:
rows, err := db.QueryContext(ctx, `
INSERT INTO HumanResources.Department (Name, GroupName)
OUTPUT INSERTED.DepartmentID, INSERTED.Name
VALUES (@n1, @g1), (@n2, @g2)`,
sql.Named("n1", "Data Science"), sql.Named("g1", "Research and Development"),
sql.Named("n2", "Cloud Ops"), sql.Named("g2", "Information Technology"))
if err != nil {
log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
var id int64
var name string
if err := rows.Scan(&id, &name); err != nil {
log.Fatal(err)
}
fmt.Printf("Inserted: %d - %s\n", id, name)
}
Pagination
Uso OFFSET y FETCH NEXT para paginación en servidor. Se requiere una ORDER BY cláusula:
Paginación basada en desplazamiento
Pasa el desplazamiento y el tamaño de página como parámetros:
func getEmployeesPage(ctx context.Context, db *sql.DB, page, pageSize int) ([]Employee, error) {
offset := (page - 1) * pageSize
rows, err := db.QueryContext(ctx, `
SELECT BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location
FROM Sales.vSalesPerson
ORDER BY BusinessEntityID
OFFSET @offset ROWS
FETCH NEXT @pageSize ROWS ONLY`,
sql.Named("offset", offset),
sql.Named("pageSize", pageSize))
if err != nil {
return nil, err
}
defer rows.Close()
var employees []Employee
for rows.Next() {
var e Employee
if err := rows.Scan(&e.Id, &e.Name, &e.Location); err != nil {
return nil, err
}
employees = append(employees, e)
}
return employees, rows.Err()
}
Paginación por conjunto de claves para tablas grandes
La paginación con desplazamiento resulta lenta en tablas grandes porque el servidor debe omitir filas. La paginación por conjunto de teclas utiliza la última clave vista para obtener la siguiente página de forma eficiente:
func getNextPage(ctx context.Context, db *sql.DB, lastID int, pageSize int) ([]Employee, error) {
rows, err := db.QueryContext(ctx, `
SELECT TOP(@pageSize) BusinessEntityID, FirstName + ' ' + LastName AS Name, CountryRegionName AS Location
FROM Sales.vSalesPerson
WHERE BusinessEntityID > @lastID
ORDER BY BusinessEntityID`,
sql.Named("pageSize", pageSize),
sql.Named("lastID", lastID))
if err != nil {
return nil, err
}
defer rows.Close()
var employees []Employee
for rows.Next() {
var e Employee
if err := rows.Scan(&e.Id, &e.Name, &e.Location); err != nil {
return nil, err
}
employees = append(employees, e)
}
return employees, rows.Err()
}
Tip
La paginación por conjunto de teclas es significativamente más rápida que OFFSET/FETCH para páginas profundas (página 1000+) porque utiliza una búsqueda de índice en lugar de escanear y saltar filas.
Agrupar varias sentencias en lote
Envía varias sentencias SQL en una sola llamada para reducir los viajes de ida y vuelta de red.
rows, err := db.QueryContext(ctx, `
SELECT COUNT(*) FROM HumanResources.Employee;
SELECT COUNT(*) FROM Sales.SalesOrderHeader;
SELECT COUNT(*) FROM Production.Product;`)
if err != nil {
log.Fatal(err)
}
defer rows.Close()
var empCount, orderCount, productCount int
if rows.Next() {
if err := rows.Scan(&empCount); err != nil {
log.Fatal(err)
}
}
if rows.NextResultSet() && rows.Next() {
if err := rows.Scan(&orderCount); err != nil {
log.Fatal(err)
}
}
if rows.NextResultSet() && rows.Next() {
if err := rows.Scan(&productCount); err != nil {
log.Fatal(err)
}
}
if err := rows.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("Employees: %d, Orders: %d, Products: %d\n",
empCount, orderCount, productCount)
Procesar conjuntos de resultados grandes de manera eficiente
Para consultas que devuelven millones de filas, procese los resultados en modo streaming. No acumules todas las filas en memoria.
func processLargeTable(ctx context.Context, db *sql.DB) error {
rows, err := db.QueryContext(ctx, "SELECT TransactionID, CONVERT(NVARCHAR(30), TransactionDate, 126) FROM Production.TransactionHistory")
if err != nil {
return err
}
defer rows.Close()
var processed int
for rows.Next() {
var id int
var data string
if err := rows.Scan(&id, &data); err != nil {
return err
}
// Process each row without accumulating.
if err := handleRow(id, data); err != nil {
return err
}
processed++
if processed%10000 == 0 {
log.Printf("Processed %d rows", processed)
}
}
return rows.Err()
}
Precaución
Un open *sql.Rows fija una conexión desde la piscina hasta que rows.Close() se llama. Para el procesamiento muy prolongado de conjuntos de resultados, considera dividir el trabajo en intervalos mediante paginación por claves para no mantener una conexión abierta durante minutos.
Upsert con MERGE
SQL Server utiliza la MERGE instrucción para operaciones de inserción o actualización (upsert).
_, err := db.ExecContext(ctx, `
MERGE HumanResources.Department AS target
USING (SELECT @id AS DepartmentID, @name AS Name, @grp AS GroupName) AS source
ON target.DepartmentID = source.DepartmentID
WHEN MATCHED THEN
UPDATE SET Name = source.Name, GroupName = source.GroupName
WHEN NOT MATCHED THEN
INSERT (Name, GroupName)
VALUES (source.Name, source.GroupName);`,
sql.Named("id", dept.Id),
sql.Named("name", dept.Name),
sql.Named("grp", dept.GroupName))
Instrucciones preparadas
Úsalo PrepareContext para crear una declaración preparada reutilizable. Las sentencias preparadas pueden mejorar el rendimiento cuando la misma consulta se ejecuta 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 _, location := range []string{"Australia", "India", "Germany"} {
var name string
err := stmt.QueryRowContext(ctx, location).Scan(&name)
if err != nil {
log.Println(location, err)
continue
}
fmt.Printf("%s: %s\n", location, name)
}
Cancelación del contexto
Todos los database/sql métodos aceptan un context.Context. Úsalo para tiempos muertos y cancelaciones.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM Production.TransactionHistory")
Si expira el plazo de contexto, el controlador cancela la consulta en el servidor y devuelve un error al llamante.