Consultas e instruções 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 devolve 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

Chame sempre rows.Close() (normalmente com defer) e verifique rows.Err() depois do ciclo. Não fechar as filas pode infiltrar as ligações da piscina. rows.Close() Também pode devolver um erro do lado do servidor enquanto o driver drena os tokens restantes, por isso não o ignore quando o conjunto de resultados ainda não estiver totalmente consumido.

Se parar de ler cedo, feche as linhas explicitamente e trate do erro de encerramento:

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 deste artigo são executados na base de dados de exemplo AdventureWorks2025. Exemplos orientados à leitura consultam objetos incorporados como Sales.vSalesPerson, Production.Product, e Sales.SalesOrderHeader. Exemplos orientados para escrita têm como alvo HumanResources.Department e Production.ProductInventory.

Consulta a uma única linha

Use QueryRowContext quando 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

Utilize ExecContext para INSERT, UPDATE, DELETE e instruções 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á-la, ocorre um erro. Use uma OUTPUT cláusula ou uma consulta separada SELECT SCOPE_IDENTITY() para recuperar um valor de identidade inserido.

Se usares SELECT SCOPE_IDENTITY(), executa-o no mesmo lote ou transação que o INSERT para que o âmbito de identidade fique na mesma ligação.

Se um procedimento armazenado ou gatilho usar SET NOCOUNT ON, RowsAffected() retorna 0 porque o SQL Server suprime a mensagem de contagem de linhas. Se precisar da contagem efetiva, remova SET NOCOUNT ON do procedimento ou devolva a contagem explicitamente através de um parâmetro de saída ou de uma instrução SELECT.

Consultas parametrizadas

Use sempre consultas parametrizadas para evitar a injeção SQL. O controlador suporta tanto parâmetros posicionais como nomeados.

Importante

O go-mssqldb driver usa @p1, @p2, e assim sucessivamente para parâmetros posicionais e sql.Named() para parâmetros nomeados. A sintaxe de marcador de posição ? que alguns outros controladores utilizam (como a do MySQL go-sql-driver) não funciona com o nome do controlador sqlserver. Se estiveres a migrar de outra base de dados, substitui todos os marcadores de posição do tipo ? ou $1 por parâmetros do tipo @p1 ou parâmetros nomeados.

Parâmetros posicionais

Utilize os marcadores de posição @p1, @p2 e passe os valores pela ordem:

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

Parâmetros nomeados

Utilize sql.Named() para associar valores a marcadores de posição 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 percorrer vários conjuntos de resultados devolvidos por um único lote ou procedimento armazenado.

Importante

Tem de esgotar completamente rows.Next() para cada conjunto de resultados antes de chamar rows.NextResultSet(). Chamar NextResultSet() antes de Next() devolve false e ignora silenciosamente as restantes linhas.

Use este padrão de loop para processar todos os conjuntos de resultados de forma fiá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 de isolamento específico. Para orientações completas sobre transações, incluindo níveis de isolamento, pontos de restauro, tratamento de impasses e padrões de repetição, 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. É necessária uma ORDER BY cláusula:

Paginação baseada em 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 offset torna-se lenta em tabelas grandes porque o servidor tem de saltar linhas. A paginação por keyset utiliza a última chave vista para obter a página seguinte 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

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

Agrupar várias declarações

Envie múltiplas instruções SQL numa única chamada para reduzir as 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)

Processar conjuntos de resultados grandes de forma eficiente

Para consultas que devolvem milhões de linhas, processe os resultados em fluxo. 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()
}

Caution

Uma abertura *sql.Rows fixa uma ligação da piscina até rows.Close() ser chamada. 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 ligação aberta durante minutos.

Upsert com MERGE

O SQL Server utiliza 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))

Declarações preparadas

Use PrepareContext para criar uma declaração preparada reutilizável. Instruções preparadas podem melhorar o desempenho quando a mesma consulta é executada várias 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 os database/sql métodos aceitam um context.Context. Utilize-o para limites de tempo 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 devolve um erro ao chamador.