我有一个使用 SQLAlchemy 将一些数据插入 MS SQL Server 数据库的 Python 进程。当 Python 进程运行时,它会在插入期间挂起。我打开 SQLAlchemy 日志记录来获取更多信息。我发现它挂在此时,SQLAlchemy 似乎正在请求有关整个数据库的表模式信息:
2020-10-30 08:12:07 [11444:6368] sqlalchemy.engine.base.Engine._execute_context(base.py:1235) INFO: SELECT [INFORMATION_SCHEMA].[TABLES].[TABLE_NAME]
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [INFORMATION_SCHEMA].[TABLES].[TABLE_SCHEMA] = CAST(? AS NVARCHAR(max)) AND [INFORMATION_SCHEMA].[TABLES].[TABLE_TYPE] = CAST(? AS NVARCHAR(max)) ORDER BY [INFORMATION_SCHEMA].[TABLES].[TABLE_NAME]
2020-10-30 08:12:07 [11444:6368] sqlalchemy.engine.base.Engine._execute_context(base.py:1240) INFO: ('dbo', 'BASE TABLE')
此时数据库中正在进行其他“事情”,包括一些未完成的事务,我的猜测是,无论出于何种原因查询
[INFORMATION_SCHEMA].[TABLES]
都会以某种方式创建一些死锁或阻塞。
我还读到(here)
[INFORMATION_SCHEMA].[TABLES]
是一种不会导致僵局的视图,这与我对导致此问题的原因的猜测相矛盾。
我的问题是:我可以更改 SQLAlchemy 的配置/设置,以便它一开始就不会进行此查询吗?
更新1: 插入的Python代码是这样的:
with sqlalchemy.create_engine("mssql+pyodbc:///?odbc_connect=%s" % params).connect() as connection:
# df is a Pandas DataFrame
df.to_sql(name=my_table, con=connection, if_exists='append', index=False)
请注意,当我在一天中没有进行其他数据库事务的其他时间运行 Python 脚本时,代码可以正常工作。在这些情况下,日志会立即像这样继续,列出数据库中的所有表:
2020-10-30 08:13:03 [11444:6368] sqlalchemy.engine.base.Engine._execute_context(base.py:1235) INFO: SELECT [INFORMATION_SCHEMA].[TABLES].[TABLE_NAME]
FROM [INFORMATION_SCHEMA].[TABLES]
WHERE [INFORMATION_SCHEMA].[TABLES].[TABLE_SCHEMA] = CAST(? AS NVARCHAR(max)) AND [INFORMATION_SCHEMA].[TABLES].[TABLE_TYPE] = CAST(? AS NVARCHAR(max)) ORDER BY [INFORMATION_SCHEMA].[TABLES].[TABLE_NAME]
2020-10-30 08:13:03 [11444:6368] sqlalchemy.engine.base.Engine._execute_context(base.py:1240) INFO: ('dbo', 'BASE TABLE')
2020-10-30 08:13:03 [11444:6368] sqlalchemy.engine.base.Engine._init_metadata(result.py:810) DEBUG: Col ('TABLE_NAME',)
2020-10-30 08:13:03 [11444:6368] sqlalchemy.engine.base.Engine.process_rows(result.py:1260) DEBUG: Row ('t_table1',)
2020-10-30 08:13:03 [11444:6368] sqlalchemy.engine.base.Engine.process_rows(result.py:1260) DEBUG: Row ('t_table2',)
...
更新2: 显然,当在开放事务中创建表或其他对象但尚未提交时,查询
[INFORMATION_SCHEMA].[TABLES]
将被阻止(source)。有熟悉 SQLAlchemy 内部结构的人建议如何阻止它首先进行此查询吗?
更新 3:在 SQLAlchemy github 上发布此问题后(问题链接),SQLAlchemy 开发人员确认 [INFORMATION_SCHEMA].[TABLES] 的查询实际上是由 Pandas function
to_sql()
引起的。
所以,我的新问题是有人知道如何在 Pandas
to_sql()
函数中禁用此行为吗?我查看了文档,但找不到任何有帮助的内容。
我对 SQLAlchemy 不是很熟悉,但我可以告诉你这个问题的 Pandas 方面。
如果表不存在,Pandas 会自动创建一个新表。它判断表是否存在的方法是调用 SQL Alchemy 中的
has_table()
。 has_table()
的工作方式是查询信息模式。 (至少,它在 MySQL 和 MSSQL 中是这样工作的。)
这是我在 Pandas 和 SQLAlchemy 中追踪逻辑的发现。我们从 pandas/io/sql.py 开始,里面
to_sql()
.
table = SQLTable(
name,
self,
frame=frame,
index=index,
if_exists=if_exists,
index_label=index_label,
schema=schema,
dtype=dtype,
)
table.create()
SQLTable.create() 定义在这里:
class SQLTable(PandasObject):
[...]
def create(self):
if self.exists():
if self.if_exists == "fail":
raise ValueError(f"Table '{self.name}' already exists.")
elif self.if_exists == "replace":
self.pd_sql.drop_table(self.name, self.schema)
self._execute_create()
elif self.if_exists == "append":
pass
else:
raise ValueError(f"'{self.if_exists}' is not valid for if_exists")
else:
self._execute_create()
请注意,它无条件调用
exists()
。在SQLTable.exists()
里面,你会发现这个:
def exists(self):
return self.pd_sql.has_table(self.name, self.schema)
这最终会在 SQLAlchemy 中调用
has_table()
:https://docs.sqlalchemy.org/en/13/core/internals.html#sqlalchemy.engine.default.DefaultDialect.has_table
对于 MSSQL,这是在 SQLAlchemy 中的 sqlalchemy/dialects/mssql/base.py 中实现的:
@_db_plus_owner
def has_table(self, connection, tablename, dbname, owner, schema):
if tablename.startswith("#"): # temporary table
[...]
else:
tables = ischema.tables
s = sql.select(tables.c.table_name).where(
sql.and_(
tables.c.table_type == "BASE TABLE",
tables.c.table_name == tablename,
)
)
if owner:
s = s.where(tables.c.table_schema == owner)
c = connection.execute(s)
return c.first() is not None
(
ischema
是information_schema的缩写,此代码正在该表上运行选择。)
我没有看到一个好的、简单的方法来解决这个问题。 Pandas 假设
has_table()
是一个廉价的操作。 MSSQL 不遵循这个假设。无论if_exists
设置为什么,Pandas都会在has_table()
期间调用to_sql()
。
不过,我可以想出一种巧妙的方法来做到这一点。如果你要 monkey-patch
pandas.io.sql.SQLTable.create()
使其成为无操作,那么你可以欺骗 Pandas 认为该表已经存在。这样做的缺点是 Pandas 不会自动创建表。
在调用to_sql/之前执行设置事务隔离级别读取未提交
我创建这个问题是为了跟踪
仅当表名称包含大写字母时才会发生这种情况。
MyTable
会卡住,而mytable
会成功完成。
这是 Pandas 需要纠正的问题吗?还是 SQLAchemy?
解决方法
有一个解决方法 - 在 SQLAchemy 引擎中将
Transaction Isolation Level
设置为 Read Uncommitted
。
https://docs.sqlalchemy.org/en/20/dialects/mssql.html#transaction-isolation-level
engine = sa.create_engine("mssql+pyodbc:///?odbc_connect={}".format(params)
, fast_executemany=True
, isolation_level="READ UNCOMMITTED"
)