2025, Oct 30 16:33
Pandas से SQL Server बैच इंसर्ट में NaN/pd.NA के कारण pyodbc fast_executemany त्रुटियाँ: समाधान
Pandas से SQL Server में fast_executemany पर बैच इंसर्ट के दौरान NaN या pd.NA से होने वाली float त्रुटियाँ रोकें: सभी missing मान Python None में normalize करें.
Pandas से SQL Server में बैच इंसर्ट बड़े डेटा सेट को तेजी से भेजने का एक बढ़िया तरीका है—जब तक कि अचानक यह काम करना बंद न कर दे। दिक्कत तब आती है जब संख्यात्मक कॉलम में missing मान मौजूद हों: कई पंक्तियों वाला बैच फेल हो जाता है, जबकि बैच का आकार 1 हो तो रिकॉर्ड निकल जाता है। त्रुटियाँ बताती हैं कि float मान अमान्य हैं, मानो ड्राइवर जरूरत से ज्यादा सख्त हो। असल में समस्या यह है कि Python की तरफ missing मान pyodbc तक पहुँचने से पहले किस रूप में पेश किए जा रहे हैं।
संक्षेप में पुनरुत्पादन
नीचे दिया गया छोटा-सा डेटा सेट यह व्यवहार दिखाता है। पूर्णांक जैसे कॉलम को Pandas के Int64 dtype से nullable बनाया गया है, जबकि बाकी संख्यात्मक कॉलम float हैं और उनमें missing मान np.nan के रूप में दर्शाए गए हैं।
import pandas as pd
import numpy as np
sample = {
    "unique_id": [
        "String_3", "String_5", "String_10", "String_9", "String_4",
        "String_7", "String_2", "String_6", "String_1", "String_8"
    ],
    "entity_name": [
        "Alice", None, "Eve", "Alice", "Bob",
        "Alice", "Alice", None, "Charlie", "Eve"
    ],
    "entity_address": [
        "456 Elm St", "789 Oak St", "123 Main St", "456 Elm St", None,
        "456 Elm St", "123 Main St", None, "789 Oak St", "789 Oak St"
    ],
    "entity_age_code": [
        763.0, 349.0, np.nan, np.nan, 888.0,
        999.0, 711.0, 574.0, 963.0, 300.0
    ],
    "entity_height": [
        93.357616, 48.408745, 79.978718, 94.953377, 11.094891,
        np.nan, 33.282917, np.nan, 82.714043, np.nan
    ],
    "entity_weight": [
        19.158688, np.nan, 73.853124, 54.005774, 70.846664,
        70.996657, np.nan, 26.325328, 11.360588, 51.324372
    ]
}
frame = pd.DataFrame(sample)
frame["entity_age_code"] = cast_series_type(frame["entity_age_code"], "Int64")
इंसर्ट का रास्ता एक पैरामीटराइज़्ड स्टेटमेंट बनाता है और fast_executemany के साथ pyodbc का उपयोग करता है। दिक्कत वाली जगह है कि हर बैच के लिए मान कैसे तैयार किए जा रहे हैं:
import pyodbc
pk_cols = ["unique_id"]
target_tbl = "DscEntityTable"
chunk_len = 3
cn_str = "xxxxxxx"
cnx = pyodbc.connect(cn_str)
cur = cnx.cursor()
frame = frame.drop_duplicates()
sql_types = deduce_mssql_types(frame)
cur.execute(f"SELECT OBJECT_ID('{target_tbl}', 'U')")
exists = cur.fetchone()[0] is not None
if exists:
    key_pairs = list(frame[pk_cols].dropna().itertuples(index=False, name=None))
    prior = fetch_existing_keys_chunked(cur, target_tbl, pk_cols, key_pairs)
    prior = pd.DataFrame(prior, columns=pk_cols)
    for k in pk_cols:
        kind = str(frame[k].dtype)
        prior[k] = cast_series_type(prior[k], kind)
    frame = pd.merge(frame, prior, how="outer", indicator=True)
    frame = frame[frame["_merge"] == "left_only"].drop(columns="_merge")
    if frame.empty:
        sys.exit("No new records to insert.")
else:
    cols_sql = ", ".join([
        f"[{c}] {sql_types[c]} COLLATE Latin1_General_100_CI_AS_SC_UTF8" if sql_types[c].startswith("VARCHAR") else f"[{c}] {sql_types[c]}"
        for c in frame.columns
    ])
    pk_sql = ", ".join(f"[{c}]" for c in pk_cols)
    create_stmt = f"CREATE TABLE {target_tbl} ({cols_sql}, PRIMARY KEY ({pk_sql}))"
    cur.execute(create_stmt)
    cnx.commit()
cols = list(frame.columns)
placeholders = ", ".join(["?"] * len(cols))
insert_stmt = f"INSERT INTO {target_tbl} ({', '.join(cols)}) VALUES ({placeholders})"
for idx, start in enumerate(range(0, len(frame), chunk_len)):
    block = frame.iloc[start:start + chunk_len]
    cur.fast_executemany = True
    # समस्या वाली रूपांतरण: कभी-कभी payload में np.nan छूट जाता है
    payload = block.map(lambda v: None if (pd.isna(v)) else v).values.tolist()
    try:
        cur.executemany(insert_stmt, payload)
        cnx.commit()
    except Exception as exc:
        pass
cur.close()
cnx.close()
जब chunk_len एक से अधिक होता है, तो कुछ बैच ऐसी त्रुटियों के साथ फेल होते हैं:
The incoming tabular data stream (TDS) remote procedure call (RPC) protocol stream is incorrect. Parameter N: The supplied value is not a valid instance of data type float.
या:
Fractional truncation (0)
असल समस्या क्या है
इस विफलता के पीछे दो बातें हैं। पहली, pyodbc का fast_executemany SQL NULL को Python के None के रूप में अपेक्षित करता है; वह np.nan को NULL नहीं मानता। दूसरी, pd.isna() भले ही np.nan को सही ढंग से पहचान लेता है, लेकिन मिश्रित dtypes पर DataFrame.map और उसके बाद .values.tolist() का संयोजन हर np.nan की जगह हमेशा None नहीं देता। नतीजा यह कि कुछ बैच SQL Server को भेजते समय float कॉलम के लिए np.nan बनाए रखते हैं, और ड्राइवर उन्हें “not a valid instance of data type float” कह कर ठुकरा देता है।
nullable integers में एक और बारीकी है: Pandas का Int64 dtype missing मानों को np.nan नहीं, बल्कि pd.NA से दर्शाता है। इसलिए आपके DataFrame में एक ही समय पर missing के दोनों placeholders दिख सकते हैं, और pyodbc को बैच देने से पहले इन्हें सामान्य (normalize) करना जरूरी है।
जैसा कि एक पर्यवेक्षक ने कहा, बात किसी रहस्यमयी batch size संवेदनशीलता की नहीं, बल्कि उन पंक्तियों के अंदर मौजूद मानों की है। एकल-पंक्ति इंसर्ट इसलिए निकल सकता है क्योंकि मूल्यांकन के समय उसमें कोई समस्याग्रस्त np.nan नहीं होता, जबकि np.nan शामिल करने वाला बहु-पंक्ति बैच float पैरामीटरों पर फेल हो जाएगा।
उपाय: सभी missing मानों को Python None में सामान्य करें
सबसे सरल और भरोसेमंद तरीका यह है कि लिस्ट-ऑफ-लिस्ट में बदलने से पहले बैच DataFrame पर ही missing मार्करों को बदल दें। np.nan और pd.NA दोनों को स्पष्ट रूप से None पर मैप करें, ताकि pyodbc सही SQL NULL भेज सके:
records = block.replace({np.nan: None, pd.NA: None}).values.tolist()
यह उपाय float वाले np.nan मानों (जैसे entity_height और entity_weight में) और nullable integer या object dtypes द्वारा उपयोग किए गए pd.NA मानों (जैसे Int64 में बदले जाने के बाद entity_age_code) दोनों को कवर करता है। DataFrame स्तर पर यह प्रतिस्थापन करने से विषम कॉलमों पर map के साथ दिखने वाले असंगत नतीजों से बचा जा सकता है।
सुधारा गया बैच इंसर्ट लूप
नीचे वही लूप है, जिसमें पंक्तियों को तैयार करने के तरीके में बस एक बदलाव जोड़ा गया है:
cols = list(frame.columns)
placeholders = ", ".join(["?"] * len(cols))
insert_stmt = f"INSERT INTO {target_tbl} ({', '.join(cols)}) VALUES ({placeholders})"
for idx, start in enumerate(range(0, len(frame), chunk_len)):
    block = frame.iloc[start:start + chunk_len]
    cur.fast_executemany = True
    # executemany से पहले सभी missing मानों को Python None में सामान्य करें
    records = block.replace({np.nan: None, pd.NA: None}).values.tolist()
    try:
        cur.executemany(insert_stmt, records)
        cnx.commit()
    except Exception as exc:
        pass
यह जानना क्यों जरूरी है
जब आप लाखों पंक्तियाँ भेजते हैं, तो missing डेटा के निरूपण में छोटी-सी असंगति भी बड़ा असर डाल सकती है। fast_executemany के साथ ड्राइवर प्रकारों और nulls के बारे में सख्त होता है; float पैरामीटरों में np.nan का पहुँच जाना सख्त विफलताओं का कारण बनता है, जो बैच आकार बदलने पर बीच-बीच में होने वाली लगती हैं। DataFrame स्तर पर None में normalization करने से यह अनिश्चितता खत्म होती है और इंसर्ट अनुमानित व तेज हो जाते हैं।
निष्कर्ष
यदि आपके बैच इंसर्ट “invalid float values” या “fractional truncation” जैसी त्रुटियों के साथ फेल हो रहे हैं और डेटा में missing मान हैं, तो executemany चलाने से पहले हर NaN और NA को Python None में बदलना सुनिश्चित करें। बैच पर Pandas की replace के साथ {np.nan: None, pd.NA: None} लगाना इसे करने का संक्षिप्त तरीका है। nullable Int64 कॉलम्स को Int64 ही रखें, पर याद रखें कि वे अंदरूनी तौर पर pd.NA का उपयोग करते हैं, इसलिए np.nan के साथ उसका मैपिंग भी शामिल करें। एक बार सभी nulls None में normalize हो जाएँ, तो तेज और भरोसेमंद bulk inserts मिलेंगे।
यह लेख StackOverflow पर एक प्रश्न (लेखक: Abhishek Sourabh) और jei के उत्तर पर आधारित है।