📝 【踩雷筆記】第一印象害死人!pyodbc 的 fast_executemany 截斷悲劇(HY000 錯誤)

身為一個每天跟資料庫談戀愛的後端工程師,追求「快,還要更快」是我們的天性。當我們用 Python 的 pyodbc 連線到 SQL Server / Azure MSSQL 時,通常會興奮地開啟這個加速外掛:

Python

cursor.fast_executemany = True

「哇!批次寫入速度直線上傳,簡直起飛!」🚀

正當你準備提早下班、開心地去買杯珍奶時,資料庫突然對你吐了一口血:

pyodbc.Error: ('HY000', '[HY000] [Microsoft][ODBC Driver... String data, right truncation ...')

原本以為是完美的資料寫入,結果直接死在半路。這到底是怎麼回事?


🔍 案發現場:為什麼「快」反而出事?

這一切的罪魁禍首,居然是因為 fast_executemany 的「第一印象偏見」。

為了追求極致的效能,fast_executemany 在處理一大批資料時,只會偷偷看第一列(First Row)的資料長度,然後心裡就默默下了決定:

「嗯,第一列的這個字串長度是 10,那我就幫接下來的所有資料都準備 10 個字元的緩衝區(Buffer Size)吧!」

這時候,如果你的第 2 列、第 100 列、或是第 999 列資料裡,藏了一個長度是 25 的超級長字串……

蹦! 緩衝區塞不下了。

SQL Server 的 ODBC 驅動程式就會立刻翻臉,丟出 HY000 截斷錯誤(Truncation Error),然後整批資料就直接報銷。

這就像是搬家公司看了一眼你家門口的第一個小紙箱,就決定開一台發財車來,結果後面搬出來的其實是雙門大冰箱一樣荒謬。


🛠️ 絕妙解法:打不過就加入?不,我們可以「彈性裝死」!

既然這外掛這麼任性,我們該怎麼辦?難道要為了那幾顆老鼠屎,放棄整片 fast_executemany 的效能森林嗎?

在這次 Commit 中,展現了一個非常優雅又帶點「渣男哲學」的解法——自動倒退嚕(Fallback)機制

直接來看這段神奇的程式碼精髓:

Python

try:
    # 依然保持樂觀,先用快快的 fast_executemany 塞塞看
    cur.executemany(sql_insert, rows)
except pyodbc.Error as e:
    # 哎呀,被抓到有長字串、被嫌太長(HY000 截斷錯誤)了!
    if "HY000" in str(e) or "truncat" in str(e).lower():
        
        # 【精髓在此】秒關外掛,裝作什麼事都沒發生,用慢速但安全的模式重試這批資料
        cur.fast_executemany = False
        cur.executemany(sql_insert, rows)
        
        # 幫這批長字串擦完屁股後,下批資料我們繼續「開掛」
        cur.fast_executemany = True
    else:
        # 如果是別的錯誤(例如語法錯),那就真的沒救,直接噴錯誤
        raise

💡 運作邏輯簡單說:

  1. 先開掛衝一波: 預設繼續用 fast_executemany = True,畢竟 90% 的情況大家都很安全。
  2. 遇到挫折就認輸: 一旦遇到 HY000(字串太長裝不下),立刻把外掛關掉fast_executemany = False)。這時候 pyodbc 就會乖乖地為每一列資料重新計算正確的長度,確保安全寫入。
  3. 安全過關後再開掛: 幫這批比較特別的資料擦完屁股後,下一批資料進來時,再把外掛重新打開

🎯 總結

這個解法厲害的地方在於,你完全不用在寫入前花費 CPU 效能去檢查每一列字串到底有多長(這通常很慢),而是採取「做錯再修正」的樂觀策略。

既保住了大部份時間的極致高速,又完美的解決了偶發性的字串截斷地雷。

下次用 Python 倒資料到 MSSQL 遇到 HY000 嗎?不妨也試試看這種「彈性裝死」的 Fallback 機制吧!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *