【问题标题】:A Simple (but long) SQLAlchemy filter() runs extremely slow (SQL Server)一个简单(但很长)的 SQLAlchemy filter() 运行速度极慢(SQL Server)
【发布时间】:2021-06-16 05:24:16
【问题描述】:

总结:我正在运行一个简单(但很长)的查询,在该查询中,我正在获取与 source_id/owner_id 对列表中的条目匹配的所有行。没有连接,没有什么复杂的。 SQLAlchemy 生成的 SQL 语句直接运行大约需要 200ms,但通过 SQLAlchemy 运行需要 2 分钟以上。

我的模型课:

class MyModel(BaseModel):
    __table_args__ = {"schema": "my_database.dbo"}
    __tablename__ = "tblMyModel"

    source_id = Column("SourceID", INT, primary_key=True)
    owner_id = Column("OwnerID", INT, primary_key=True)
    part_number = Column("PartNumber", NVARCHAR(64), nullable=False)

查询:

class ModelKey(NamedTuple):
    source_id: int
    owner_id: int

def get_by_ids(
    session: Session, id_keys: Set[ModelKey]
) -> List[MyModel]:
    filters = [
        and_(
            MyModel.source_id == key.source_id,
            MyModel.owner_id == key.owner_id,
        )
        for key in id_keys
    ]
    query = session.query(MyModel).filter(or_(*filters))
    result = query.all()
    return result

(我们将会话传递给此方法,因为我们正在跨多个查询和插入进行会话管理。在调用 get_by_ids() 之前,还有一些其他 SQL 查询被调用,但没有插入操作。)

id_keys 中最多可包含 400 个条目。我检查了 SQLAlchemy 生成的 SQL 语句:

SELECT 
    my_database.dbo.tblMyModel.SourceID,
    my_database.dbo.tblMyModel.OwnerID,
    my_database.dbo.tblMyModel.PartNumber 
FROM my_database.dbo.tblMyModel 
WHERE 
     my_database.dbo.tblMyModel.SourceID = 18396 AND my_database.dbo.tblMyModel.OwnerID = 99312703
     OR my_database.dbo.tblMyModel.SourceID = 18396 AND my_database.dbo.tblMyModel.OwnerID = 99255569
     OR my_database.dbo.tblMyModel.SourceID = 34512 AND my_database.dbo.tblMyModel.OwnerID = 8675309
...
     OR my_database.dbo.tblMyModel.SourceID = 36785 AND my_database.dbo.tblMyModel.OwnerID = 1234567

这是一个很长的查询(464 个OR 子句),但是如果我使用 SQLAlchemy 生成的 SQL 语句并直接在 DBeaver 中运行它,它会在大约 200 毫秒内执行,而对同一个数据库运行相同查询的 SQLAlchemy 会接管2 分钟。

我使用SQLAlchemy suggetions 分析了我的代码并发现了以下内容:

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1  113.133  113.133  113.133  113.133 {method 'execute' of 'pyodbc.Cursor' objects}
        1    3.578    3.578    3.578    3.578 {method 'fetchall' of 'pyodbc.Cursor' objects}
        1    0.033    0.033    0.035    0.035 selectable.py:3144(_froms)
    18348    0.006    0.000    0.006    0.000 {built-in method builtins.isinstance}
        1    0.005    0.005    0.005    0.005 {method 'close' of 'pyodbc.Cursor' objects}
      930    0.005    0.000    0.014    0.000 elements.py:965(__init__)
      930    0.005    0.000    0.008    0.000 elements.py:3251(__init__)
      933    0.005    0.000    0.013    0.000 compiler.py:866(visit_column)
      930    0.004    0.000    0.037    0.000 compiler.py:1361(_generate_generic_binary)
   3264/1    0.004    0.000    0.086    0.086 visitors.py:87(_compiler_dispatch)

根据 SQLAlchemy 的文档,“这表明数据库需要很长时间才能开始返回结果,这意味着应该通过添加索引或重组查询和/或底层架构来优化您的查询。”但正如我所说,查询本身在 SQLAlchemy 之外运行时运行得非常快。

感谢任何帮助!

【问题讨论】:

  • 那么多OR 子句总是很困难。可能优化器能够将查询的一个版本转换为VALUES 表上的连接,而另一个版本不能。看起来您应该使用两列表值参数或临时表,然后加入它。
  • 我想知道它是否在运行时将值内联到查询中与发送带有占位符的查询,以及这是否会影响查询计划。
  • Ilja 这是一个非常好的问题,查看我们的 DataDog 仪表板中的查询显示 WHERE my_database.dbo. [ tblMyModel ] . [ SourceID ] = ? AND my_database.dbo. [ tblMyModel ] . [ OwnerID ] = ? OR ... 但我不知道这是否是 DataDog 格式化查询。当我将 sqlalchemy 查询编译成 SQL 时,我调用了 query.statement.compile(compile_kwargs={"literal_binds": True})),这可能与实际查询不同,因为我将 literal_binds 设置为 true。

标签: python sql sql-server sqlalchemy


【解决方案1】:

我可以通过使用 OPENJSON 来解决这个问题:

class ModelKey(NamedTuple):
    source_id: int
    owner_id: int

def get_by_ids(
    session: Session, id_keys: Set[ModelKey]
) -> List[MyModel]:
        key_dicts = [
            {"source_id": key.source_id, "owner_id": key.owner_id}
            for key in id_keys
        ]

        json_params = json.dumps(list(key_dicts))

        sql = text(
            """
            SELECT tmm.SourceID , tmm.OwnerID, tmm.PartNumber
            FROM OPENJSON( :json) WITH (
                source_id INT '$.source_id',
                owner_id INT '$.owner_id'
            ) as json
            INNER JOIN my_database.dbo.tblMyModel tmm
            ON json.source_id = tmm.SourceID AND json.owner_id = tmm.OwnerID
            """
        ).params(json=json_params)
        result = session.execute(sql).fetchall()

        models = []
        for row in result:
            row_dict = dict(row.items())
            m = MyModel(
                source_id=row_dict["SourceID"],
                owner_id=row_dict["OwnerID"],
                part_number=row_dict["PartNumber"],
            )
            models.append(m)

        return models

这个版本的查询(在 SQLAlchemy 中执行)大约需要 5 毫秒,这比原来大约需要两分钟的版本有了很大的改进。 (这是在相同的硬件上使用相同的 id_keys 运行的。)

我仍然不知道为什么原来的版本这么慢,如果有人有任何见解,我想这样做。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-06-25
    • 1970-01-01
    • 2016-02-29
    • 1970-01-01
    • 2015-09-21
    • 1970-01-01
    • 1970-01-01
    • 2021-05-02
    相关资源
    最近更新 更多