使用 mssql-python 進行大量複製

mssql-python 驅動程式包含批量複製功能,能有效將大量資料插入 SQL Server、Azure SQL Database、Azure SQL 受控執行個體 及 Microsoft Fabric 中的 SQL 資料庫。

cursor.bulkcopy() 方法提供了載入大型資料集的高效能路徑:

  • 減少網路往返次數。
  • 可選擇性地在載入時繞過限制檢查。
  • 使用最佳化的 TDS 大量插入通訊協定。
  • 可達成與 bcp.exeSqlBulkCopy相當的吞吐量。

基於 Rust mssql_py_core 的原生擴充功能驅動了批量複製功能。 它在一般游標 execute() 管線之外運作。

基本使用方式

呼叫 bulkcopy() 游標,傳遞目標資料表名稱及一組列元組或 Row 物件的迭代:

Important

如果您在同一工作階段中建立或修改目標資料表,請先呼叫 conn.commit(),再呼叫 bulkcopy()。 批量複製協定使用獨立的內部通道來讀取資料表中繼資料,因此未提交的 DDL 變更可能導致死結或逾時。

import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

# Create a temp table for the demo
cursor.execute("""
    CREATE TABLE ##BulkDemo (
        ID INT,
        Name NVARCHAR(50),
        Amount MONEY
    )
""")
conn.commit()

data = [
    (1, "Alice", 50000.00),
    (2, "Bob", 60000.00),
    (3, "Carol", 55000.00),
]

result = cursor.bulkcopy("##BulkDemo", data)
print(f"Copied {result['rows_copied']} rows in {result['batch_count']} batch(es)")
print(f"Elapsed: {result['elapsed_time']}")

返回值

bulkcopy() 返回字典:

Key 類型 描述
rows_copied int 成功複製的列數。
batch_count int 處理的批次數量。
elapsed_time float 操作所需時間(秒)

方法簽章

cursor.bulkcopy(
    table_name,                    # str – target table (can include schema, e.g. "dbo.MyTable")
    data,                          # Iterable[Tuple | Row] – rows to insert
    batch_size=0,                  # int – rows per batch; 0 = server optimal
    timeout=30,                    # int – operation timeout in seconds
    column_mappings=None,          # List[str] | List[Tuple[int,str]] | None
    keep_identity=False,           # bool – preserve identity values from source
    check_constraints=False,       # bool – check constraints during load
    table_lock=False,              # bool – use table-level lock
    keep_nulls=False,              # bool – preserve NULLs instead of defaults
    fire_triggers=False,           # bool – fire INSERT triggers on target
    use_internal_transaction=False, # bool – use internal transaction per batch
)

欄位映射

預設情況下,bulkcopy() 會依欄位的序數位置對應欄位。 每個資料欄位對應到同一索引的表格欄位。 使用 column_mappings 參數來覆寫此行為。

欄位名稱清單

列表中的每個位置對應來源資料索引:

result = cursor.bulkcopy(
    "##BulkDemo",
    data,
    column_mappings=["ID", "Name", "Amount"],
)

進階格式:明確索引映射

每個元組都採用 (source_index, target_column_name) 的形式。 使用此格式跳過或重新排序欄位:

result = cursor.bulkcopy(
    "##BulkDemo",
    data,
    column_mappings=[(0, "ID"), (1, "Name"), (2, "Amount")],
)

從檔案載入

你可以透過將產生器傳入 bulkcopy(),從 CSV 檔案及其他檔案格式載入資料。

CSV 檔案

import csv
import io
import mssql_python

# In production, replace io.StringIO with open("data.csv", "r", ...)
csv_data = """ID,Name,Value
1,Widget,9.99
2,Gadget,24.50
3,Gizmo,4.75
"""

def csv_row_generator(file_obj):
    """Generator that yields tuples from a CSV file object."""
    reader = csv.reader(file_obj)
    next(reader)  # Skip header
    for row in reader:
        if row:  # skip blank lines
            yield (
                int(row[0]),      # ID
                row[1],           # Name
                float(row[2]),    # Value
            )

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE ##CSVImport (ID INT, Name NVARCHAR(100), Value FLOAT)
""")
conn.commit()
result = cursor.bulkcopy("##CSVImport", csv_row_generator(io.StringIO(csv_data)))
print(f"Imported {result['rows_copied']} rows from CSV")

帶有批次處理的大型檔案

設定參數 batch_size 控制驅動程式每批傳送多少列。 這種方法對於大型檔案效果良好:

import csv
import io
import mssql_python

# In production, replace io.StringIO with open("large_file.csv", "r", ...)
csv_data = "\n".join(
    ["ID,Name,Value"] + [f"{i},Item {i},{i * 1.5}" for i in range(1, 201)]
)

def csv_rows(file_obj):
    reader = csv.reader(file_obj)
    next(reader)  # Skip header
    for row in reader:
        if row:
            yield (int(row[0]), row[1], float(row[2]))

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE ##LargeCSV (ID INT, Name NVARCHAR(100), Value FLOAT)
""")
conn.commit()
result = cursor.bulkcopy(
    "##LargeCSV",
    csv_rows(io.StringIO(csv_data)),
    batch_size=50,
)
print(f"Imported {result['rows_copied']} rows in {result['batch_count']} batches")

載入 pandas 資料框

將 pandas DataFrame 轉換成元組清單,然後再傳給 bulkcopy()

import pandas as pd
import mssql_python

df = pd.DataFrame({
    'ID': [1, 2, 3],
    'Name': ['Alice', 'Bob', 'Carol'],
    'Amount': [50000.0, 60000.0, 55000.0],
})

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE ##PandasDemo (ID INT, Name NVARCHAR(50), Amount MONEY)
""")
conn.commit()

data = [tuple(row) for row in df.itertuples(index=False, name=None)]
result = cursor.bulkcopy("##PandasDemo", data)

處理 NULL 值

在任何欄位位置傳入 None,以插入 SQL NULL 值:

cursor.execute("""
    CREATE TABLE ##NullDemo (ID INT, Name NVARCHAR(50), Amount MONEY)
""")
conn.commit()

data = [
    (1, "Alice", 50000.00),
    (2, "Bob", None),       # NULL Amount
    (3, None, 55000.00),    # NULL Name
]

cursor.bulkcopy("##NullDemo", data)

識別欄位

要插入明確的單位值,請設定 keep_identity=True

cursor.execute("""
    CREATE TABLE ##IdentDemo (ID INT, Name NVARCHAR(50), Amount MONEY)
""")
conn.commit()

data = [
    (100, "Alice", 50000.00),
    (200, "Bob", 60000.00),
]

cursor.bulkcopy("##IdentDemo", data, keep_identity=True)

keep_identity=False (預設值)時,從資料中省略身份欄位,並用來 column_mappings 鎖定非身份欄位。

批量複製選項

參數 預設值 描述
batch_size 0 每批次的列數。 0 讓伺服器選擇最佳大小。
timeout 30 操作逾時時間(以秒為單位)。
keep_identity False 保留來源資料中的識別值。
check_constraints False 載入時檢查資料表的限制。
table_lock False 取得桌級鎖而非列級鎖。
keep_nulls False 保留 NULL 值,而非插入欄位預設值。
fire_triggers False 在目標資料表上觸發 INSERT 觸發程序。
use_internal_transaction False 將每個批次包裝成內部交易。

處理錯誤

bulkcopy() 如果載入失敗,會產生例外,因此會將呼叫包裹在 try/except 區塊中以捕捉錯誤。 請記住,bulkcopy() 會使用自己的內部連線執行,並且會獨立提交複製的資料列,因此你在主要連線上執行的 conn.rollback() 無法復原這些資料列。 若要讓批次具有原子性,請設定 use_internal_transaction=True,此設定會讓每個批次各自在自己的交易中執行,並在批次失敗時自動回滾:

import mssql_python

conn = mssql_python.connect(connection_string)
cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE ##ImportDemo (ID INT, Name NVARCHAR(50), Value FLOAT)
""")
conn.commit()

data = [
    (1, "Alice", 50000.00),
    (2, "Bob", 60000.00),
    (3, "Carol", 55000.00),
]

try:
    result = cursor.bulkcopy("##ImportDemo", data, use_internal_transaction=True)
    print(f"Successfully copied {result['rows_copied']} rows")
except (mssql_python.DatabaseError, ValueError) as e:
    # bulkcopy() commits on its own connection, so there's nothing to roll back
    # here. With use_internal_transaction=True, a failed batch is already rolled
    # back on the bulk copy connection.
    print(f"Bulk copy failed: {e}")

若要讓載入作業先經過您自己的驗證邏輯把關,請先大量複製到暫存資料表,然後在主要連線的交易中使用 INSERT ... SELECT,將這些資料列移轉到目標資料表。 INSERT 會在您的連線上執行,因此如果驗證失敗,conn.rollback() 會將其復原。

Authentication

批量複製使用獨立的內部通道,該通道需要自己的令牌。 驅動程式會自動處理支援的認證方式的令牌取得。

受控識別(ActiveDirectoryMSI)

對於系統指派或使用者指派的受控識別,請使用 Authentication=ActiveDirectoryMSI。 此認證方法建議用於 Azure 託管服務,如 Azure VMS、App Service、Functions 及 AKS。

import mssql_python

# System-assigned managed identity
conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryMSI;"
    "Encrypt=yes"
)
cursor = conn.cursor()

cursor.execute("CREATE TABLE ##MsiDemo (ID INT, Name NVARCHAR(50))")
conn.commit()

result = cursor.bulkcopy("##MsiDemo", [(1, "Alice"), (2, "Bob")])
print(f"Copied {result['rows_copied']} rows")

對於使用者指定的管理身份,請在 連接字串 中傳遞用戶端 ID:

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryMSI;"
    "UID=<client-id>;"
    "Encrypt=yes"
)

Service principal (ActiveDirectoryServicePrincipal)

使用 Authentication=ActiveDirectoryServicePrincipal 進行服務主體(用戶端憑證)驗證。

conn = mssql_python.connect(
    "Server=<server>.database.windows.net;"
    "Database=<database>;"
    "Authentication=ActiveDirectoryServicePrincipal;"
    "UID=<application-client-id>;"
    "PWD=<client-secret>;"
    "Encrypt=yes"
)
cursor = conn.cursor()

cursor.execute("CREATE TABLE ##SpDemo (ID INT, Value FLOAT)")
conn.commit()

result = cursor.bulkcopy("##SpDemo", [(1, 1.5), (2, 2.5)])
print(f"Copied {result['rows_copied']} rows")

預設憑證鏈(ActiveDirectoryDefault)

ActiveDirectoryDefault 依序嘗試多個憑證提供者,例如環境變數、工作負載身份、受管理身份等。 它適用於本地開發和 Azure 托管服務,無需修改程式碼。

欲了解更多關於認證的資訊,請參閱 Microsoft Entra 認證

效能祕訣

以下技巧幫助您最大化批量複製的吞吐量。

使用產生器處理大型資料集

產生器能最小化記憶體使用,因為 bulkcopy() 接受任意可迭代:

def data_generator(count):
    """Generate rows without loading all into memory."""
    for i in range(count):
        yield (i, f"Item {i}", i * 1.5)

cursor = conn.cursor()
cursor.execute("""
    CREATE TABLE ##LargeDemo (ID INT, Name NVARCHAR(50), Value FLOAT)
""")
conn.commit()
result = cursor.bulkcopy("##LargeDemo", data_generator(1000))

使用桌鎖來加快載入速度

當你沒有同時讀取器時,設定 table_lock=True 以減少大容量初期負載的鎖定負擔。

result = cursor.bulkcopy(
    "##LargeDemo",
    data,
    table_lock=True,
    batch_size=100000,
)

載入時停用索引

在大量載入前暫時停用非叢集索引,之後再重建以提升效能:

cursor = conn.cursor()

cursor.execute("""
    CREATE TABLE ##IndexDemo (ID INT, Name NVARCHAR(50), Value FLOAT)
""")
cursor.execute("CREATE NONCLUSTERED INDEX IX_Name ON ##IndexDemo(Name)")
conn.commit()

cursor.execute("ALTER INDEX IX_Name ON ##IndexDemo DISABLE")
conn.commit()

result = cursor.bulkcopy("##IndexDemo", data)
conn.commit()

cursor.execute("ALTER INDEX IX_Name ON ##IndexDemo REBUILD")
conn.commit()

並行載入資料表

為每個資料表開一個獨立連線,並同時執行載入。

import concurrent.futures

def load_table(table_name, rows):
    conn = mssql_python.connect(connection_string)
    cursor = conn.cursor()
    cursor.execute(f"CREATE TABLE {table_name} (ID INT, Name NVARCHAR(50), Value FLOAT)")
    conn.commit()
    result = cursor.bulkcopy(table_name, rows)
    conn.commit()
    conn.close()
    return result["rows_copied"]

data = [(i, f"Item {i}", i * 1.5) for i in range(100)]

with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
    futures = [
        executor.submit(load_table, "##Load1", data),
        executor.submit(load_table, "##Load2", data),
        executor.submit(load_table, "##Load3", data),
    ]
    for future in concurrent.futures.as_completed(futures):
        print(f"Loaded {future.result()} rows")

與替代方案的比較

下表比較了批量複製與其他資料插入方法。

方法 應用案例 績效
cursor.bulkcopy() 大型資料集(超過 1,000 筆資料列)。 最快
cursor.executemany() 含參數的中型資料集 Moderate
cursor.execute() 在迴圈中 小資料集,邏輯簡單明瞭。 最慢