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

Rufen Sie immer rows.Close() auf (in der Regel mit defer), und überprüfen Sie rows.Err() nach der Schleife. Wenn Zeilen nicht geschlossen werden, können Verbindungen aus dem Pool verloren gehen. 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)
}

Ausführen einer Anweisung

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 für dieselbe Verbindung erhalten bleibt.

Die gemeldete RowsAffected() Anzahl hängt davon ab, was die Zeilenzählungen hervorgebracht hat:

  • Gespeicherte Prozeduren: Wenn jede Anweisung innerhalb eines Verfahrens ausgeführt wird, das verwendet SET NOCOUNT ON, gibt 0 RowsAffected() zurück, da keine Zeilenzählung gesendet wird. Um eine Anzahl zu erhalten, entfernen Sie SET NOCOUNT ON, oder geben Sie die Anzahl über einen SELECT-Parameter oder eine OUTPUT-Anweisung zurück.
  • AFTER Trigger:RowsAffected() schließt Zeilen ein, die von AFTER-Triggern betroffen sind, die SET NOCOUNT ON nicht verwenden. Eine einzelne Zeile UPDATE in einer Tabelle, deren Auslöser zwei Audit-Zeilen schreibt, gibt 3 und nicht 1 zurück. Das Hinzufügen SET NOCOUNT ON zum Trigger-Körper schließt die Reihen des Triggers aus und gibt 1 zurück. Die Zählung der äußeren Aussage wird weiterhin gemeldet. Entferne SET NOCOUNT ON nicht von einem Trigger, um eine Zeilenzahl zu korrigieren; das Entfernen führt zu einer aufgeblähten Zahl.
  • INSTEAD OF Trigger: Die Anzahl umfasst die Anweisungen des Triggers sowie die ursprüngliche Anweisung, auch wenn die ursprüngliche Anweisung nicht ausgeführt wird. Eine Einzelzeile UPDATE , deren Trigger zwei Audit-Zeilen schreibt und das Update durchführt, gibt 4 zurück. Wenn der Trigger die Aktualisierung nicht durchführt, gibt der Aufruf trotzdem 3 zurück, und es werden keine Zeilen geändert, sodass eine von null verschiedene Anzahl nicht belegt, dass sich die Daten geändert haben. Abfragen Sie die betroffene Tabelle, wenn Sie das Ergebnis überprüfen müssen. Das Hinzufügen SET NOCOUNT ON zum Auslöserkörper ergibt in beiden Fällen 1.
  • Multi-Statement-Batches:ExecContext(ctx, "UPDATE ...; UPDATE ...") gibt die Summe aller gezählten Aussagen zurück, nicht die Zählung der letzten Aussage. Verwenden Sie separate ExecContext Anrufe, um eine Zahl pro Aussage zu erhalten.

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

Sie müssen rows.Next() für jedes Resultset vollständig ausschöpfen, bevor Sie rows.NextResultSet() aufrufen. 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.

Ausführen mehrerer Anweisungen im Batch

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 heftet eine Verbindung aus dem Pool an, bis rows.Close() aufgerufen wird. Erwägen Sie bei der Verarbeitung von Resultsets mit sehr langer Ausführungszeit, die Arbeit mithilfe der Keyset-Paginierung in Bereiche zu unterteilen, um zu vermeiden, dass eine Verbindung minutenlang gehalten wird.

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

Kontextabbruch

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.