Consultas e comandos com go-mssqldb

O go-mssqldb driver utiliza a interface padrão database/sql para executar consultas e instruções. Este artigo aborda padrões comuns de acesso a dados com o driver.

Executar uma consulta SELECT

Use QueryContext para executar uma consulta que retorna linhas:

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

Sempre chame rows.Close() (normalmente com defer) e verifique rows.Err() depois do loop. Não fechar as linhas pode causar vazamento de conexões do pool. rows.Close() também pode retornar um erro no servidor enquanto o driver consome os tokens restantes, então não o ignore quando o conjunto de resultados não tiver sido totalmente consumido.

Se você interromper a leitura antecipadamente, feche as linhas explicitamente e trate o erro de fechamento:

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

Os exemplos neste artigo usam o banco de dados de exemplo AdventureWorks2025. Exemplos orientados à leitura consultam objetos embutidos como Sales.vSalesPerson, Production.Product, e Sales.SalesOrderHeader. Os exemplos orientados para gravação têm como destino HumanResources.Department e Production.ProductInventory.

Consultar uma única linha

Use QueryRowContext quando você espera exatamente uma linha:

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

Executar uma instrução

Use ExecContext para INSERT, UPDATE, DELETE e comandos 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

O go-mssqldb driver não suporta LastInsertId(). Ao chamá-lo, ocorre um erro. Use uma OUTPUT cláusula ou uma consulta separada SELECT SCOPE_IDENTITY() para recuperar um valor de identidade inserido.

Se você usar SELECT SCOPE_IDENTITY(), execute-o no mesmo lote ou na mesma transação que INSERT, para que o escopo de identidade permaneça na mesma conexão.

A contagem que RowsAffected() reporta depende do que produziu as contagens de linhas:

  • Procedimentos armazenados: Quando toda instrução é executada dentro de um procedimento que usa SET NOCOUNT ON, RowsAffected() retorna 0 porque nenhuma contagem de linhas é enviada. Para obter uma contagem, remova SET NOCOUNT ON, ou retorne a contagem por meio de um OUTPUT parâmetro ou SELECT instrução.
  • AFTER Disparadores:RowsAffected() inclui linhas afetadas por disparadores AFTER que não usam SET NOCOUNT ON. Uma única linha UPDATE em uma tabela cujo gatilho grava duas linhas de auditoria retorna 3, não 1. Adicionar SET NOCOUNT ON ao corpo do gatilho exclui as linhas do gatilho e retorna 1. A contagem da declaração externa ainda é divulgada. Não remova SET NOCOUNT ON de um gatilho para corrigir a contagem de linhas; removê-lo faz com que o número fique inflado.
  • INSTEAD OF Gatilhos: A contagem inclui as instruções do gatilho e a instrução original, embora a instrução original não seja executada. Uma linha UPDATE única cujo gatilho grava duas linhas de auditoria e realiza a atualização retorna 4. Se o gatilho não fizer a atualização, a chamada ainda retorna 3 e nenhuma linha muda, então uma contagem diferente de zero não confirma que os dados mudaram. Consulte a tabela afetada quando precisar verificar o resultado. Adicionar SET NOCOUNT ON ao corpo do disparador retorna 1 em ambos os casos.
  • Lotes de múltiplas instruções:ExecContext(ctx, "UPDATE ...; UPDATE ...") retorna a soma de todas as afirmações contadas, não a contagem da última instrução. Use chamadas ExecContext separadas para obter uma contagem por instrução.

Consultas parametrizadas

Sempre use consultas parametrizadas para evitar injeção SQL. O driver dá suporte a parâmetros posicionais e nomeados.

Importante

O go-mssqldb driver usa @p1, @p2, e assim por diante para parâmetros posicionais e sql.Named() para parâmetros nomeados. A sintaxe de marcador ? que alguns outros drivers usam (como o go-sql-driver do MySQL) não funciona com o nome do driver sqlserver. Se você estiver migrando de outro banco de dados, substitua todos os placeholders no estilo ? ou $1 por placeholders no estilo @p1 ou por parâmetros nomeados.

Parâmetros posicionais

Utilize os marcadores @p1 e @p2 e passe os valores na ordem:

rows, err := db.QueryContext(ctx,
    "SELECT BusinessEntityID, FirstName, CountryRegionName FROM Sales.vSalesPerson WHERE FirstName = @p1 AND CountryRegionName = @p2",
    "Jared", "Australia")

Parâmetros nomeados

Use sql.Named() para vincular valores a marcadores nomeados:

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

Vários conjuntos de resultados

Use rows.NextResultSet() para iterar entre vários conjuntos de resultados retornados por um único lote ou procedimento armazenado.

Importante

Você deve percorrer completamente rows.Next() para cada conjunto de resultados antes de chamar rows.NextResultSet(). Chamar NextResultSet() antes de Next() retorna false e ignora silenciosamente as linhas restantes.

Use este padrão de loop para processar todos os conjuntos de resultados de forma confiável:

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

Use BeginTx para iniciar uma transação com um nível específico de isolamento. Para orientações completas sobre transações, incluindo níveis de isolamento, pontos de salvamento, tratamento de interbloqueios e padrões de nova tentativa, consulte Transações.

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

Obter valores de identidade inseridos

O go-mssqldb driver não suporta LastInsertId(). Use a cláusula OUTPUT para recuperar o valor de identidade na mesma instrução:

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 múltiplas linhas:

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 e FETCH NEXT para paginação do lado do servidor. Uma ORDER BY cláusula é necessária:

Paginação por offset

Passe o offset e o tamanho da 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()
}

Paginação por conjunto de chaves para tabelas grandes

A paginação por deslocamento fica lenta em tabelas grandes porque o servidor precisa pular linhas. A paginação por conjunto de chaves usa a última chave vista para buscar a próxima página de maneira 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()
}

Dica

A paginação por keyset é significativamente mais rápida do que OFFSET/FETCH para páginas profundas (página 1000+) porque usa uma busca de índice em vez de escanear e pular linhas.

Agrupar várias instruções em lote

Envie múltiplas instruções SQL em uma única chamada para reduzir viagens de ida e volta na rede.

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)

Processe conjuntos de resultados grandes de forma eficiente

Para consultas que retornam milhões de linhas, processe os resultados em fluxo contínuo. Não acumule todas as linhas na memória.

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

Cuidado

Um *sql.Rows aberto fixa uma conexão do pool até que rows.Close() seja chamado. Para o processamento de conjuntos de resultados de execução muito prolongada, considere dividir o trabalho em faixas usando paginação por conjunto de chaves para evitar manter uma conexão aberta por minutos.

Upsert com MERGE

O SQL Server usa a MERGE instrução para operações de inserção ou atualização (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))

Instruções preparadas

Use PrepareContext para criar uma declaração preparada reutilizável. Instruções preparadas podem melhorar o desempenho quando a mesma consulta é executada muitas vezes com 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)
}

Cancelamento de contexto

Todos database/sql os métodos aceitam um context.Context. Use-o para tempos limite e cancelamento.

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

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

Se o prazo de contexto expirar, o driver cancela a consulta no servidor e retorna um erro ao chamador.