簡體   English   中英

來自 Python 的多行 UPSERT(插入或更新)

[英]Multi-row UPSERT (INSERT or UPDATE) from Python

我目前正在使用 python 執行下面的簡單查詢,使用 pyodbc 在 SQL 服務器表中插入數據:

import pyodbc

table_name = 'my_table'
insert_values = [(1,2,3),(2,2,4),(3,4,5)]

cnxn = pyodbc.connect(...)
cursor = cnxn.cursor()
cursor.execute(
    ' '.join([
        'insert into',
        table_name,
        'values',
        ','.join(
            [str(i) for i in insert_values]
        )
    ])
)
cursor.commit()

只要沒有重復的鍵,這應該可以工作(假設第一列包含鍵)。 但是對於具有重復鍵的數據(表中已經存在的數據),它會引發錯誤。 我怎樣才能一次性使用 pyodbc 在 SQL Server 表中插入多行,以便簡單地更新具有重復鍵的數據。

注意:有針對單行數據提出的解決方案,但是,我想一次插入多行(避免循環)!

這可以使用MERGE來完成。 假設您有一個鍵列ID和兩列col_acol_b (您需要在更新語句中指定列名),那么該語句將如下所示:

MERGE INTO MyTable as Target
USING (SELECT * FROM 
       (VALUES (1, 2, 3), (2, 2, 4), (3, 4, 5)) 
       AS s (ID, col_a, col_b)
      ) AS Source
ON Target.ID=Source.ID
WHEN NOT MATCHED THEN
INSERT (ID, col_a, col_b) VALUES (Source.ID, Source.col_a, Source.col_b)
WHEN MATCHED THEN
UPDATE SET col_a=Source.col_a, col_b=Source.col_b;

您可以在reextester.com/IONFW62765 上嘗試一下

基本上,我正在使用要upsert的值列表“即時”創建Source表。 然后,當您將Source表與Target合並時,您可以在每一行上測試MATCHED條件( Target.ID=Source.ID )(而當僅使用簡單的IF <exists> INSERT (...) ELSE UPDATE (...)條件)。

在帶有pyodbc python 中,它可能看起來像這樣:

import pyodbc

insert_values = [(1, 2, 3), (2, 2, 4), (3, 4, 5)]
table_name = 'my_table'
key_col = 'ID'
col_a = 'col_a'
col_b = 'col_b'

cnxn = pyodbc.connect(...)
cursor = cnxn.cursor()
cursor.execute(('MERGE INTO {table_name} as Target '
                'USING (SELECT * FROM '
                '(VALUES {vals}) '
                'AS s ({k}, {a}, {b}) '
                ') AS Source '
                'ON Target.ID=Source.ID '
                'WHEN NOT MATCHED THEN '
                'INSERT ({k}, {a}, {b}) VALUES (Source.{k}, Source.{a}, Source.{b}) '
                'WHEN MATCHED THEN '
                'UPDATE SET {k}=Source.{a}, col_b=Source.{b};'
                .format(table_name=table_name,
                        vals=','.join([str(i) for i in insert_values]),
                        k=key_col,
                        a=col_a,
                        b=col_b)))
cursor.commit()

您可以在SQL Server 文檔中閱讀有關MERGE更多信息。

給定一個數據幀(df),我使用 ksbg 中的代碼將其插入到表中。 請注意,我在兩列(日期和車站代碼)上尋找匹配項,您可以使用其中一列。 代碼生成給定任何 df 的查詢。

def append(df, c):


    table_name = 'ddf.ddf_actuals'


    columns_list = df.columns.tolist()
    columns_list_query = f'({(",".join(columns_list))})'
    sr_columns_list = [f'Source.{i}' for i in columns_list]
    sr_columns_list_query = f'({(",".join(sr_columns_list))})'
    up_columns_list = [f'{i}=Source.{i}' for i in columns_list]
    up_columns_list_query = f'{",".join(up_columns_list)}'

    rows_to_insert = [row.tolist() for idx, row in final_list.iterrows()]
    rows_to_insert = str(rows_to_insert).replace('[', '(').replace(']', ')')[1:][:-1]


    query = f"MERGE INTO {table_name} as Target \
USING (SELECT * FROM \
(VALUES {rows_to_insert}) \
AS s {columns_list_query}\
) AS Source \
ON Target.stationcode=Source.stationcode AND Target.date=Source.date \
WHEN NOT MATCHED THEN \
INSERT {columns_list_query} VALUES {sr_columns_list_query} \
WHEN MATCHED THEN \
UPDATE SET {up_columns_list_query};"
    c.execute(query)

    c.commit()

跟進這里的現有答案,因為它們可能容易受到注入攻擊,最好使用參數化查詢(對於 mssql/pyodbc,這些是“?”占位符)。 我稍微調整了 Alexander Novas 的代碼,以在帶有 sqlalchemy 的查詢的參數化版本中使用數據幀行:

# assuming you already have a dataframe "df" and sqlalchemy engine called "engine"
# also assumes your dataframe columns have all the same names as the existing table

table_name_to_update = 'update_table'
table_name_to_transfer = 'placeholder_table'

# the dataframe and existing table should both have a column to use as the primary key
primary_key_col = 'id'

# replace the placeholder table with the dataframe
df.to_sql(table_name_to_transfer, engine, if_exists='replace', index=False)

# building the command terms
cols_list = df.columns.tolist()
cols_list_query = f'({(", ".join(cols_list))})'
sr_cols_list = [f'Source.{i}' for i in cols_list]
sr_cols_list_query = f'({(", ".join(sr_cols_list))})'
up_cols_list = [f'{i}=Source.{i}' for i in cols_list]
up_cols_list_query = f'{", ".join(up_cols_list)}'
    
# fill values that should be interpreted as "NULL" with None
def fill_null(vals: list) -> list:
    def bad(val):
        if isinstance(val, type(pd.NA)):
            return True
        # the list of values you want to interpret as 'NULL' should be 
        # tweaked to your needs
        return val in ['NULL', np.nan, 'nan', '', '', '-', '?']
    return tuple(i if not bad(i) else None for i in vals)

# create the list of parameter indicators (?, ?, ?, etc...)
# and the parameters, which are the values to be inserted
params = [fill_null(row.tolist()) for _, row in df.iterrows()]
param_slots = '('+', '.join(['?']*len(df.columns))+')'
    
cmd = f'''
       MERGE INTO {table_name_to_update} as Target
       USING (SELECT * FROM
       (VALUES {param_slots})
       AS s {cols_list_query}
       ) AS Source
       ON Target.{primary_key_col}=Source.{primary_key_col}
       WHEN NOT MATCHED THEN
       INSERT {cols_list_query} VALUES {sr_cols_list_query} 
       WHEN MATCHED THEN
       UPDATE SET {up_cols_list_query};
       '''

# execute the command to merge tables
with engine.begin() as conn:
    conn.execute(cmd, params)

如果您插入的字符串包含與 SQL 插入文本不兼容的字符(例如使插入語句混亂的撇號),則此方法也更好,因為它讓連接引擎處理參數化值(這也使其更安全地對抗 SQL注入攻擊)。

作為參考,我正在使用此代碼創建引擎連接 - 您顯然需要使其適應您的服務器/數據庫/環境以及是否需要fast_executemany

import urllib
import pyodbc
pyodbc.pooling = False
import sqlalchemy

terms = urllib.parse.quote_plus(
            'DRIVER={SQL Server Native Client 11.0};'
            'SERVER=<your server>;'
            'DATABASE=<your database>;'
            'Trusted_Connection=yes;' # to logon using Windows credentials

url = f'mssql+pyodbc:///?odbc_connect={terms}'
engine = sqlalchemy.create_engine(url, fast_executemany=True)

編輯:我意識到這段代碼實際上根本沒有使用“占位符”表,只是通過參數化命令直接從數據幀行復制值。

暫無
暫無

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

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