Abfragen und Anweisungen mit go-mssqldb

Der Treiber go-mssqldb verwendet die Standardschnittstelle database/sql zum Ausführen von Abfragen und zur Ausführung von Anweisungen. Dieser Artikel behandelt gängige Muster für den Datenzugriff mit dem Treiber.

Ausführen einer SELECT-Abfrage

Verwenden Sie QueryContext, um eine Abfrage auszuführen, die Zeilen zurückgibt:

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

Important

rows.Close() immer aufrufen (typischerweise mit defer) und rows.Err() nach der Schleife überprüfen. Wenn Zeilen nicht geschlossen werden, kann dies zum Verlust von Verbindungen aus dem Verbindungspool führen. rows.Close() kann auch einen serverseitigen Fehler zurückgeben, während der Treiber verbleibende Tokens verbraucht, also ignorieren Sie ihn nicht, wenn die Ergebnismenge nicht vollständig verbraucht ist.

Wenn du früh aufhörst zu lesen, schließe die Zeilen explizit und behandle den Schlussfehler:

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

Beispiele in diesem Artikel laufen gegen die AdventureWorks2025-Beispieldatenbank . Leseorientierte Beispiele fragen eingebaute Objekte wie Sales.vSalesPerson, Production.Product, und Sales.SalesOrderHeaderab. Schreiborientierte Beispiele richten sich an HumanResources.Department und Production.ProductInventory.

Eine einzelne Zeile abfragen

Verwenden Sie QueryRowContext, wenn Sie genau eine Zeile erwarten:

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

Führe eine Anweisung aus

Verwenden Sie ExecContext für INSERT, UPDATE, DELETE und DDL-Anweisungen:

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)

Important

Der Treiber go-mssqldb unterstützt LastInsertId()nicht . Das Aufrufen liefert einen Fehler. Verwenden Sie eine OUTPUT Klausel oder eine separate SELECT SCOPE_IDENTITY() Abfrage, um einen eingefügten Identitätswert abzurufen.

Wenn Sie SELECT SCOPE_IDENTITY() verwenden, führen Sie es im selben Batch oder in derselben Transaktion wie INSERT aus, damit der Identitätsbereich auf derselben Verbindung bleibt.

Wenn eine gespeicherte Prozedur oder ein Trigger SET NOCOUNT ON verwendet, gibt RowsAffected() 0 zurück, weil SQL Server die Zeilenanzahlmeldung unterdrückt. Wenn Sie die tatsächliche Anzahl benötigen, entfernen Sie entweder SET NOCOUNT ON aus der Prozedur oder geben Sie die Anzahl explizit über einen Ausgabeparameter oder eine SELECT-Anweisung zurück.

Parametrisierte Abfragen

Verwenden Sie immer parametrisierte Abfragen, um SQL-Injection zu vermeiden. Der Treiber unterstützt sowohl positionsbezogene als auch benannte Parameter.

Important

Der Treiber go-mssqldb verwendet @p1, @p2, und so weiter für Positionsparameter und sql.Named() für benannte Parameter. Die ? Platzhaltersyntax, die einige andere Treiber verwenden (wie die go-sql-drivervon MySQL), funktioniert nicht mit dem Treibernamen sqlserver . Wenn Sie von einer anderen Datenbank migrieren, ersetzen Sie alle ? oder $1-Stil Platzhalter durch @p1-Stil oder benannte Parameter.

Positionsparameter

Verwenden Sie @p1, @p2 Platzhalter und Passwerte in der Reihenfolge:

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

Benannte Parameter

Verwenden Sie sql.Named(), um Werte an benannte Platzhalter zu binden:

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

Mehrere Ergebnismengen

rows.NextResultSet() verwenden, um mehrere Ergebnissätze zu durchlaufen, die von einem einzelnen Batch oder einer Stored Procedure zurückgegeben werden.

Important

Du musst rows.Next() für jede Ergebnismenge vollständig verarbeiten, bevor du rows.NextResultSet() aufrufst. Der Aufruf von NextResultSet() vor Next() gibt false zurück und überspringt die verbleibenden Zeilen stillschweigend.

Verwenden Sie dieses Schleifenmuster, um alle Ergebnismengen zuverlässig zu verarbeiten:

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

Transaktionen

Verwenden Sie BeginTx, um eine Transaktion mit einer bestimmten Isolationsstufe zu starten. Für umfassende Transaktionshinweise, einschließlich Isolationsstufen, Speicherpunkte, Deadlock-Handling und Wiederholungsmustern, siehe Transaktionen.

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

Eingefügte Identitätswerte abrufen

Der Treiber go-mssqldb unterstützt LastInsertId()nicht . Verwenden Sie die Klausel OUTPUT, um den Identitätswert in derselben Anweisung abzurufen:

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)

Für mehrere Reihen:

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

Paginierung

Verwendung OFFSET und FETCH NEXT für serverseitige Paginierung. Eine Klausel ORDER BY ist erforderlich:

Offset-basierte Paginierung

Übergeben Sie den Offset und die Seitengröße als Parameter:

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

Keyset-Paginierung für große Tabellen

Die Offset-Paginierung wird bei großen Tabellen langsam, weil der Server Zeilen überspringen muss. Die Keyset-Paginierung verwendet die zuletzt gesehene Taste, um die nächste Seite effizient abzurufen:

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

Keyset-Paginierung ist deutlich schneller als OFFSET/FETCH bei tiefen Seiten (Seite 1000+), da sie eine Indexsuche anstelle von Scannen und Überspringen von Zeilen verwendet.

Mehrere Anweisungen stapelweise verarbeiten

Sende mehrere SQL-Anweisungen in einem einzigen Aufruf, um Netzwerk-Roundtrips zu reduzieren.

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)

Verarbeiten Sie große Ergebnismengen effizient

Bei Abfragen, die Millionen von Zeilen zurückgeben, sollten Ergebnisse im Streamingverfahren verarbeitet werden. Sammle nicht alle Zeilen im Gedächtnis.

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

Achtung

Ein offenes *sql.Rows Signal verbindet eine Verbindung vom Pool, bis rows.Close() aufgerufen wird. Für eine sehr langlaufende Verarbeitung von Ergebnismengen sollten Sie die Arbeit in Bereiche mit Keyset-Pagination unterteilen, um eine Verbindung nicht minutenlang zu halten.

Upsert mit MERGE

SQL Server verwendet die MERGE Anweisung für Insert-or-Update-(Upsert)-Operationen.

_, 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))

Vorbereitete Anweisungen

Verwenden Sie PrepareContext , um eine wiederverwendbare vorbereitete Erklärung zu erstellen. Prepared Statements können die Leistung verbessern, wenn dieselbe Abfrage mehrfach mit unterschiedlichen Parametern ausgeführt wird.

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

Kontextaufhebung

Alle database/sql Methoden akzeptieren ein context.Context. Nutze es für Auszeiten und Absagen.

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

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

Wenn die Kontextfrist abläuft, bricht der Treiber die Abfrage auf dem Server ab und gibt dem Aufrufer einen Fehler zurück.