Pandas to_sql() 在一个 DataFrame 上慢,但在其他 DataFrame 上快
Pandas to_sql() slow on one DataFrame but fast on others
目标
我正在尝试使用 pandas DataFrame.to_sql()
将大型 DataFrame(>1M 行)发送到 MS SQL 服务器数据库。
问题
该命令在一个特定的 DataFrame 上明显较慢,发送 10,000 行大约需要 130 秒。相比之下,类似的 DataFrame 仅需 7 秒 即可发送相同数量的行。后者的 DataFrame 实际上有更多的列,以及更多的数据 df.memory_usage(deep=True)
.
详情
SQLAlchemy 引擎是通过
创建的
engine = create_engine('mssql+pyodbc://@<server>/<db>?driver=ODBC+Driver+17+for+SQL+Server', fast_executemany=True)
to_sql()
调用如下:
df[i:i+chunksize].to_sql(table, conn, index=False, if_exists='replace')
其中 chunksize = 10000
.
我试图通过 cProfile
定位瓶颈,但这只表明几乎所有时间都花在了 pyodbc.Cursor.executemany
。
如有任何调试提示,我们将不胜感激!
原因
性能差异是由于 pyodbc
中的 issue 在使用 [=14] 时将 None
值传递给 SQL 服务器 INSERT
语句=] 选项会导致速度下降。
解决方案
我们可以将值打包为 JSON
并使用 OPENJSON
(SQL Server 2016+ 支持)而不是 fast_executemany
。该解决方案使我的应用程序性能提高了 30 倍!这是一个 self-contained 示例,基于文档 here,但适用于 pandas
用户。
import pandas as pd
from sqlalchemy import create_engine
df = pd.DataFrame({'First Name': ['Homer', 'Ned'], 'Last Name': ['Simpson', 'Flanders']})
rows_as_json = df.to_json(orient='records')
server = '<server name>'
db = '<database name>'
table = '<table name>'
engine = create_engine(f'mssql+pyodbc://<user>:<password>@{server}/{db}')
sql = f'''
INSERT INTO {table} ([First Name], [Last Name])
SELECT [First Name], [Last Name] FROM
OPENJSON(?)
WITH (
[First Name] nvarchar(50) '$."First Name"',
[Last Name] nvarchar(50) '$."Last Name"'
)
'''
cursor = engine.raw_connection().cursor()
cursor.execute(sql, rows_as_json)
cursor.commit()
替代解决方法
- 将数据导出为 CSV 并使用外部工具完成传输(例如 bcp utility)。
- 将转换为
None
的值人工替换为 non-empty 填充值,添加辅助列以指示更改了哪些行,通过 to_sql()
正常完成操作,然后通过基于辅助列的单独查询将填充值重置为 Null
。
致谢
非常感谢@GordThompson 指出我的解决方案!
目标
我正在尝试使用 pandas DataFrame.to_sql()
将大型 DataFrame(>1M 行)发送到 MS SQL 服务器数据库。
问题
该命令在一个特定的 DataFrame 上明显较慢,发送 10,000 行大约需要 130 秒。相比之下,类似的 DataFrame 仅需 7 秒 即可发送相同数量的行。后者的 DataFrame 实际上有更多的列,以及更多的数据 df.memory_usage(deep=True)
.
详情
SQLAlchemy 引擎是通过
创建的engine = create_engine('mssql+pyodbc://@<server>/<db>?driver=ODBC+Driver+17+for+SQL+Server', fast_executemany=True)
to_sql()
调用如下:
df[i:i+chunksize].to_sql(table, conn, index=False, if_exists='replace')
其中 chunksize = 10000
.
我试图通过 cProfile
定位瓶颈,但这只表明几乎所有时间都花在了 pyodbc.Cursor.executemany
。
如有任何调试提示,我们将不胜感激!
原因
性能差异是由于 pyodbc
中的 issue 在使用 [=14] 时将 None
值传递给 SQL 服务器 INSERT
语句=] 选项会导致速度下降。
解决方案
我们可以将值打包为 JSON
并使用 OPENJSON
(SQL Server 2016+ 支持)而不是 fast_executemany
。该解决方案使我的应用程序性能提高了 30 倍!这是一个 self-contained 示例,基于文档 here,但适用于 pandas
用户。
import pandas as pd
from sqlalchemy import create_engine
df = pd.DataFrame({'First Name': ['Homer', 'Ned'], 'Last Name': ['Simpson', 'Flanders']})
rows_as_json = df.to_json(orient='records')
server = '<server name>'
db = '<database name>'
table = '<table name>'
engine = create_engine(f'mssql+pyodbc://<user>:<password>@{server}/{db}')
sql = f'''
INSERT INTO {table} ([First Name], [Last Name])
SELECT [First Name], [Last Name] FROM
OPENJSON(?)
WITH (
[First Name] nvarchar(50) '$."First Name"',
[Last Name] nvarchar(50) '$."Last Name"'
)
'''
cursor = engine.raw_connection().cursor()
cursor.execute(sql, rows_as_json)
cursor.commit()
替代解决方法
- 将数据导出为 CSV 并使用外部工具完成传输(例如 bcp utility)。
- 将转换为
None
的值人工替换为 non-empty 填充值,添加辅助列以指示更改了哪些行,通过to_sql()
正常完成操作,然后通过基于辅助列的单独查询将填充值重置为Null
。
致谢
非常感谢@GordThompson 指出我的解决方案!