使用 go-mssqldb 進行的批量運算

go-mssqldb驅動程式透過使用該mssql.CopyIn函式支援高效能的大量插入操作。 批量插入繞過了通常的逐列 INSERT 路徑,直接透過 TDS 批量複製協定將資料串流到伺服器。

選擇批量複製、TVP 或 JSON

當你需要將多列或複雜負載傳送到 SQL Server 時,請參考以下指南:

選擇...... 當它最適合時 權衡取捨
批量複製 mssql.CopyIn 你需要最快的方式將多列載入一個目標表。 吞吐量最佳,但它適用於資料表載入,而非預存程序的呼叫介面或混合結構的酬載。
一個 表值參數 你需要將一組強型別資料列傳遞至預存程序或參數化命令。 保留結構與程序邊界,但需要使用者自訂的資料表類型及匹配的欄位順序。
搭配 OPENJSONFOR JSON 的 JSON 你的應用程式已經交換 JSON,或者 payload 形狀是巢狀或彈性的。 對於應用程式程式碼來說,它更具可攜性,但通常比 TVP 或結構化插入的批量複製更慢且型別安全度較低。

如果您要將大量資料載入暫存資料表或目標資料表,請先使用大量複製。 如果你呼叫的是結構化列集的儲存程序,建議先從 TVP 開始。 如果你需要巢狀文件或鬆散的結構,建議先從 JSON 開始。

本文範例與 AdventureWorks2025 範例資料庫對比。 批次複製範例的目標為 HumanResources.DepartmentProduction.ProductCategory

基本的散裝內襯

使用 mssql.CopyIn 來建立大量複製陳述式,然後使用 Exec 來傳送資料列:

import (
    "database/sql"
    "log"

    "github.com/microsoft/go-mssqldb"
)

func bulkInsert(db *sql.DB) error {
    txn, err := db.Begin()
    if err != nil {
        return err
    }
    defer txn.Rollback()

    stmt, err := txn.Prepare(mssql.CopyIn("HumanResources.Department", mssql.BulkOptions{},
        "Name", "GroupName"))
    if err != nil {
        return err
    }

    // Add rows
    _, err = stmt.Exec("Data Science", "Research and Development")
    if err != nil {
        return err
    }
    _, err = stmt.Exec("Cloud Ops", "Information Technology")
    if err != nil {
        return err
    }
    _, err = stmt.Exec("Developer Relations", "Sales and Marketing")
    if err != nil {
        return err
    }

    // Flush and finalize the bulk copy
    result, err := stmt.Exec()
    if err != nil {
        return err
    }

    if err = stmt.Close(); err != nil {
        return err
    }

    rowsAffected, _ := result.RowsAffected()
    log.Printf("Bulk inserted %d rows\n", rowsAffected)

    return txn.Commit()
}

最後 stmt.Exec() 一次呼叫且無參數時會清除剩餘列,並完成批量複製操作。

批量選擇權

結構體 mssql.BulkOptions 配置批量複製行為:

Field 類型 Description
CheckConstraints bool 在批量插入時檢查限制。
FireTriggers bool 在目標資料表上觸發 INSERT 觸發程序。
KeepNulls bool 保留空值,而非插入預設值。
KilobytesPerBatch int 每批次的 KB 數。 0 使用伺服器預設值。
RowsPerBatch int 每批次的列數。 0 使用伺服器預設值。
Order []string ORDER 針對目標叢集索引的提示(例如, []string{"Id ASC"})。
Tablock bool 在大量複製期間取得資料表層級鎖定。

選項範例

傳遞 BulkOptions 以控制約束檢查、觸發程序和鎖定:

stmt, err := txn.Prepare(mssql.CopyIn("HumanResources.Department",
    mssql.BulkOptions{
        CheckConstraints: true,
        FireTriggers:     true,
        Tablock:          true,
        RowsPerBatch:     1000,
    },
    "Name", "GroupName"))

錯誤處理

若有任何一列失敗,整個批量複製操作即告失敗。 檢查來自每一列的 Exec 呼叫及最終清空 Exec 的錯誤:

for _, emp := range employees {
    _, err = stmt.Exec(emp.Name, emp.GroupName)
    if err != nil {
        txn.Rollback()
        return err
    }
}

// Final flush
_, err = stmt.Exec()
if err != nil {
    txn.Rollback()
    return err
}

偵測並記錄失敗的列

當批次複製操作失敗時,SQL Server 會顯示限制或資料問題,但不會標示具體哪一列。 要識別失敗的列,請使用批次處理方法:

func bulkInsertWithRowTracking(db *sql.DB, departments []Department) error {
    txn, err := db.Begin()
    if err != nil {
        return err
    }

    stmt, err := txn.Prepare(mssql.CopyIn("HumanResources.Department",
        mssql.BulkOptions{RowsPerBatch: 500}, "Name", "GroupName"))
    if err != nil {
        txn.Rollback()
        return err
    }

    for i, dept := range departments {
        _, err = stmt.Exec(dept.Name, dept.GroupName)
        if err != nil {
            txn.Rollback()
            log.Printf("Bulk copy failed at row %d (Name=%q): %v", i, dept.Name, err)
            return fmt.Errorf("bulk copy failed at row %d: %w", i, err)
        }
    }

    _, err = stmt.Exec()
    if err != nil {
        txn.Rollback()
        return fmt.Errorf("bulk copy flush failed: %w", err)
    }

    if err = stmt.Close(); err != nil {
        txn.Rollback()
        return err
    }

    return txn.Commit()
}

Tip

如果你需要略過錯誤資料列並繼續處理,可以使用個別的 INSERT 陳述式或暫存資料表模式:先大量複製到沒有條件約束的暫存資料表,然後使用含錯誤處理的 MERGEINSERT...SELECT,將資料移轉到目標資料表。

從 CSV 檔案串流

對於大型 CSV 檔案,直接將資料列串流到批量複製,而不必將整個檔案載入記憶體:

import (
    "encoding/csv"
    "io"
    "os"
)

func bulkInsertFromCSV(db *sql.DB, filePath string) error {
    f, err := os.Open(filePath)
    if err != nil {
        return err
    }
    defer f.Close()

    reader := csv.NewReader(f)

    // Skip the header row.
    _, err = reader.Read()
    if err != nil {
        return err
    }

    txn, err := db.Begin()
    if err != nil {
        return err
    }

    stmt, err := txn.Prepare(mssql.CopyIn("HumanResources.Department",
        mssql.BulkOptions{Tablock: true, RowsPerBatch: 5000},
        "Name", "GroupName"))
    if err != nil {
        txn.Rollback()
        return err
    }

    var rowCount int
    for {
        record, err := reader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            txn.Rollback()
            return fmt.Errorf("CSV read error at row %d: %w", rowCount+1, err)
        }

        _, err = stmt.Exec(record[0], record[1])
        if err != nil {
            txn.Rollback()
            return fmt.Errorf("row %d: %w", rowCount+1, err)
        }
        rowCount++
    }

    // Flush remaining rows.
    result, err := stmt.Exec()
    if err != nil {
        txn.Rollback()
        return err
    }
    if err = stmt.Close(); err != nil {
        txn.Rollback()
        return err
    }

    affected, _ := result.RowsAffected()
    log.Printf("Bulk inserted %d rows from CSV", affected)

    return txn.Commit()
}

表現比較

批量複製在大量資料負載下,比單一插入快得多。 下表顯示插入 100,000 列時的大致效能特性:

方法 相對速度 網絡往返列車 鎖定
個人 INSERT 最慢 (1倍) 100,000 每個插入點按行級計算。
批次處理 INSERT (每個語句 1,000 列) 中等(5-10倍) 100 每批次的行級。
不含 TABLOCK 的批量複製 快速(20-50倍) 這要看批次大小 排級體積。
使用 TABLOCK 進行大量複製 最快(50-100x) 這要看批次大小 桌上鎖,記錄最少。

Note

實際效能會根據網路延遲、伺服器設定、資料表索引,以及是否有最小的日誌功能而有所不同。 使用 testing.B 針對您的特定工作負載進行基準測試。 請參閱 效能調整

欄位排序與型態映射

欄位 mssql.CopyIn 必須符合目標資料表預期的順序與類型。 驅動程式不會執行欄位名稱匹配;它使用位置映射。

常見類型問題

Go 型別 SQL Server 欄位 Issue 解決方法
string varchar 隱含 nvarchar 轉換。 使用 mssql.VarChar 包裝器。
float64 decimal(18,4) 精度損失。 string身分通過。
time.Time datetime2 時區轉換。 使用UTC時間。
nil 任何可為 Null 的欄位 需要 KeepNulls: true KeepNulls 中設定 BulkOptions

明確型態的範例

當預設型別映射與你的架構不符時,明確指定欄位類型:

stmt, err := txn.Prepare(mssql.CopyIn("Production.ProductCategory",
    mssql.BulkOptions{KeepNulls: true},
    "Name"))
if err != nil {
    return err
}

for _, p := range categories {
    _, err = stmt.Exec(p.Name)
    if err != nil {
        return err
    }
}

效能祕訣

  • 使用 Tablock 對空白資料表進行大量插入。 此選項減少鎖爭用並減少日誌記錄。
  • 設定 RowsPerBatch,以控制驅動程式傳送資料的頻率。 較大批次減少往返次數,但使用更多記憶體。
  • 將連線字串中的 Increase packet size 提高至最高 32767,以減少網路額外負荷。
  • 將資料排序 為與目標資料表的叢集索引相符,並設定選項 Order 。 這種方法避免了伺服器端排序。
  • 在大量載入前丟棄非叢集索引,然後再重新建立。 批量插入時的索引維護會增加額外負擔。
  • 使用 CheckConstraints: false (預設值)來對受信任的資料在大量複製期間略過約束檢查。

Limitations

  • 批量複製不支援受 Always Encrypted 保護的欄位。 如需詳細資訊,請參閱限制
  • Azure SQL Database 不支援 TDS 層級的批量複製。 對於 Azure SQL Database,請改用批次 INSERT 陳述式或暫存資料表模式來代替。