簡體   English   中英

如何使用 pyodbc 加速批量插入 MS SQL Server

[英]How to speed up bulk insert to MS SQL Server using pyodbc

下面是我需要一些幫助的代碼。 我必須運行它超過 1,300,000 行,這意味着插入大約 300,000 行需要40 分鍾

我認為批量插入是加快速度的途徑嗎? 還是因為我正在通過for data in reader:部分迭代行?

#Opens the prepped csv file
with open (os.path.join(newpath,outfile), 'r') as f:
    #hooks csv reader to file
    reader = csv.reader(f)
    #pulls out the columns (which match the SQL table)
    columns = next(reader)
    #trims any extra spaces
    columns = [x.strip(' ') for x in columns]
    #starts SQL statement
    query = 'bulk insert into SpikeData123({0}) values ({1})'
    #puts column names in SQL query 'query'
    query = query.format(','.join(columns), ','.join('?' * len(columns)))

    print 'Query is: %s' % query
    #starts curser from cnxn (which works)
    cursor = cnxn.cursor()
    #uploads everything by row
    for data in reader:
        cursor.execute(query, data)
        cursor.commit()

我是故意動態選擇我的列標題(因為我想創建盡可能多的 Pythonic 代碼)。

SpikeData123 是表名。

正如對另一個答案的評論中所述,T-SQL BULK INSERT命令僅在要導入的文件與 SQL Server 實例位於同一台計算機上或位於 SQL Server 實例可以的 SMB/CIFS 網絡位置時才有效讀。 因此,它可能不適用於源文件位於遠程客戶端上的情況。

pyodbc 4.0.19 添加了Cursor#fast_executemany功能,在這種情況下可能會有所幫助。 fast_executemany默認為“關閉”,以下測試代碼...

cnxn = pyodbc.connect(conn_str, autocommit=True)
crsr = cnxn.cursor()
crsr.execute("TRUNCATE TABLE fast_executemany_test")

sql = "INSERT INTO fast_executemany_test (txtcol) VALUES (?)"
params = [(f'txt{i:06d}',) for i in range(1000)]
t0 = time.time()
crsr.executemany(sql, params)
print(f'{time.time() - t0:.1f} seconds')

...在我的測試機器上執行大約需要 22 秒。 只需添加crsr.fast_executemany = True ...

cnxn = pyodbc.connect(conn_str, autocommit=True)
crsr = cnxn.cursor()
crsr.execute("TRUNCATE TABLE fast_executemany_test")

crsr.fast_executemany = True  # new in pyodbc 4.0.19

sql = "INSERT INTO fast_executemany_test (txtcol) VALUES (?)"
params = [(f'txt{i:06d}',) for i in range(1000)]
t0 = time.time()
crsr.executemany(sql, params)
print(f'{time.time() - t0:.1f} seconds')

... 將執行時間減少到 1 秒多一點。

更新 - 2022 年 5 月: bcpandasbcpyaz是 Microsoft 的bcp實用程序的包裝器。


更新 - 2019 年 4 月:正如 @SimonLang 的評論中所述,SQL Server 2017 及更高版本下的BULK INSERT顯然確實支持 CSV 文件中的文本限定符(參考: here )。


BULK INSERT 幾乎肯定會比逐行讀取源文件並為每一行執行常規 INSERT快得多 但是,BULK INSERT 和 BCP 都對 CSV 文件有很大的限制,因為它們不能處理文本限定符(參考: here )。 也就是說,如果您的 CSV 文件中沒有合格的文本字符串...

1,Gord Thompson,2015-04-15
2,Bob Loblaw,2015-04-07

...然后您可以 BULK INSERT 它,但如果它包含文本限定符(因為某些文本值包含逗號)...

1,"Thompson, Gord",2015-04-15
2,"Loblaw, Bob",2015-04-07

...然后 BULK INSERT 無法處理它。 盡管如此,總體而言,將這樣的 CSV 文件預處理為管道分隔文件可能會更快......

1|Thompson, Gord|2015-04-15
2|Loblaw, Bob|2015-04-07

...或制表符分隔的文件(其中代表制表符)...

1→Thompson, Gord→2015-04-15
2→Loblaw, Bob→2015-04-07

...然后批量插入該文件。 對於后一個(制表符分隔的)文件,BULK INSERT 代碼看起來像這樣:

import pypyodbc
conn_str = "DSN=myDb_SQLEXPRESS;"
cnxn = pypyodbc.connect(conn_str)
crsr = cnxn.cursor()
sql = """
BULK INSERT myDb.dbo.SpikeData123
FROM 'C:\\__tmp\\biTest.txt' WITH (
    FIELDTERMINATOR='\\t',
    ROWTERMINATOR='\\n'
    );
"""
crsr.execute(sql)
cnxn.commit()
crsr.close()
cnxn.close()

注意:正如評論中提到的,執行BULK INSERT語句僅適用於 SQL Server 實例可以直接讀取源文件的情況。 對於源文件位於遠程客戶端的情況,請參閱此答案

是的批量插入是將大文件加載到數據庫的正確路徑。 乍一看,我會說它需要這么長時間的原因是正如您提到的那樣,您正在遍歷文件中的每一行數據,這實際上意味着正在消除使用批量插入的好處並使其像普通插入一樣。 請記住,顧名思義,它用於插入數據塊。 我會刪除循環並重試。

另外,我會仔細檢查您的批量插入語法,因為它對我來說看起來不正確。 檢查由 pyodbc 生成的 sql,因為我感覺它可能只是在執行正常的插入

或者,如果它仍然很慢,我會嘗試直接從 sql 使用批量插入,或者使用批量插入將整個文件加載到臨時表中,然后將相關列插入到正確的表中。 或混合使用批量插入和 bcp 來插入特定列或 OPENROWSET。

這個問題讓我很沮喪,在我在 SO 上找到這篇文章之前,我並沒有看到使用fast_executemany有多大改進。 具體來說,Bryan Bailliache 關於 max varchar 的評論。 我一直在使用 SQLAlchemy,甚至確保更好的數據類型參數並沒有為我解決問題; 但是,切換到 pyodbc 確實如此。 我還接受了 Michael Moura 關於使用臨時表的建議,發現它節省了更多時間。 我寫了一個函數,以防有人發現它有用。 我寫它是為了插入一個列表或列表列表。 我使用 SQLAlchemy 和 Pandas to_sql插入相同的數據從有時需要 40 分鍾以上縮短到不到 4 秒。 不過,我可能一直在濫用我以前的方法。

聯系

def mssql_conn():
    conn = pyodbc.connect(driver='{ODBC Driver 17 for SQL Server}',
                          server=os.environ.get('MS_SQL_SERVER'),
                          database='EHT',
                          uid=os.environ.get('MS_SQL_UN'),
                          pwd=os.environ.get('MS_SQL_PW'),
                          autocommit=True)
    return conn

插入功能

def mssql_insert(table,val_lst,truncate=False,temp_table=False):
    '''Use as direct connection to database to insert data, especially for
       large inserts. Takes either a single list (for one row),
       or list of list (for multiple rows). Can either append to table
       (default) or if truncate=True, replace existing.'''
    conn = mssql_conn()
    cursor = conn.cursor()
    cursor.fast_executemany = True
    tt = False
    qm = '?,'
    if isinstance(val_lst[0],list):
        rows = len(val_lst)
        params = qm * len(val_lst[0])
    else:
        rows = 1
        params = qm * len(val_lst)
        val_lst = [val_lst]
    params = params[:-1]
    if truncate:
        cursor.execute(f"TRUNCATE TABLE {table}")
    if temp_table:
        #create a temp table with same schema
        start_time = time.time()
        cursor.execute(f"SELECT * INTO ##{table} FROM {table} WHERE 1=0")
        table = f"##{table}"
        #set flag to indicate temp table was used
        tt = True
    else:
        start_time = time.time()
    #insert into either existing table or newly created temp table
    stmt = f"INSERT INTO {table} VALUES ({params})"
    cursor.executemany(stmt,val_lst)
    if tt:
        #remove temp moniker and insert from temp table
        dest_table = table[2:]
        cursor.execute(f"INSERT INTO {dest_table} SELECT * FROM {table}")
        print('Temp table used!')
        print(f'{rows} rows inserted into the {dest_table} table in {time.time() - 
              start_time} seconds')
    else:
        print('No temp table used!')
        print(f'{rows} rows inserted into the {table} table in {time.time() - 
              start_time} seconds')
    cursor.close()
    conn.close()

我的控制台結果首先使用臨時表,然后不使用臨時表(在這兩種情況下,表都包含執行時的數據並且 Truncate=True):

No temp table used!
18204 rows inserted into the CUCMDeviceScrape_WithForwards table in 10.595500707626343 
seconds

Temp table used!
18204 rows inserted into the CUCMDeviceScrape_WithForwards table in 3.810380458831787 
seconds

FWIW,我給出了一些插入 SQL Server 的方法,我自己進行了一些測試。 通過使用 SQL Server Batches 和使用 pyodbcCursor.execute 語句,我實際上能夠獲得最快的結果。 我沒有測試保存到 csv 和 BULK INSERT,我想知道它是如何比較的。

這是我關於測試的博客:http: //jonmorisissqlblog.blogspot.com/2021/05/python-pyodbc-and-batch-inserts-to-sql.html

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM