【问题标题】:SQLAlchemy hangs during insert while querying [INFORMATION_SCHEMA].[TABLES]查询 [INFORMATION_SCHEMA].[TABLES] 时,SQLAlchemy 在插入期间挂起
【发布时间】:2021-02-13 00:39:40
【问题描述】:

我有一个 Python 进程,它使用 SQLAlchemy 将一些数据插入 MS SQL Server DB。当 Python 进程运行时,它会在插入期间挂起。我打开了 SQLAlchemy 日志以获取更多信息。我发现它在 SQLAlchemy 似乎正在请求有关整个 DB 的表模式信息时挂起:

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 (issue link) SQLAlchemy 开发人员发布此问题后,确认 [INFORMATION_SCHEMA].[TABLES] 的查询实际上是由 Pandas @987654324 引起的@to_sql().

所以,我的新问题是,有人知道如何在 Pandas to_sql() 函数中禁用此行为吗?我查看了文档,找不到任何似乎有帮助的内容。

【问题讨论】:

  • 在 GitHub 上讨论过 here.
  • @GordThompson SQLAlchemy 开发人员提到使用ALTER DATABASE MyDatabase SET ALLOW_SNAPSHOT_ISOLATION ONALTER DATABASE MyDatabase SET READ_COMMITTED_SNAPSHOT ON 不会通过打开快照隔离来积极锁定。我担心在数据库级别进行这些更改。您是否知道 SQL Server 中有一个设置,当存在已在该数据库中创建新表的打开事务时,对 [INFORMATION_SCHEMA].[TABLES] 进行查询不会阻塞?
  • 你可以试试create_engine(connection_uri, isolation_level="READ_UNCOMMITTED") 看看是否能防止挂起。

标签: python sql-server pandas sqlalchemy pyodbc


【解决方案1】:

我对 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 的缩写,这段代码在那个表上运行 select。)

如何解决这个问题

我没有看到一个好的、简单的方法来解决这个问题。 Pandas 假设has_table() 是一种廉价操作。 MSSQL 不遵循该假设。无论if_exists 设置为什么,Pandas 都会在to_sql() 期间调用has_table()

不过,我可以想出一个很老套的方法。如果您要 monkey-patch pandas.io.sql.SQLTable.create() 使其成为空操作,那么您可以欺骗 Pandas 认为该表已经存在。这样做的缺点是 Pandas 不会自动创建表。

【讨论】:

  • 我认为你基本上是在正确的轨道上,但问题出现在插入之后。当 Pandas to_sql 检查表是否可能存在区分大小写问题(在 to_sql 中查找:# check for potentially case sensitivity issues (GH7815))时,它最终会在 SQLAlchemy 中调用函数 get_table_names,这最终是导致阻塞的原因。
猜你喜欢
  • 2017-09-21
  • 2021-03-13
  • 1970-01-01
  • 1970-01-01
  • 2015-08-10
  • 1970-01-01
  • 2017-04-01
  • 1970-01-01
  • 2013-05-05
相关资源
最近更新 更多