可以让SQLAlchemy在不重复原始查询的情况下执行子查询预加载吗?

12 投票
4 回答
13938 浏览
提问于 2025-05-01 04:54

假设我们有一个原始生成的查询,像这样:

SELECT company.x AS company_x, ...
FROM company
LEFT OUTER JOIN acc ON acc.id = company.acc
LEFT OUTER JOIN usercomp_links ON company.id = usercomp_links.pid
LEFT OUTER JOIN usergro_links ON acc.id = usergro_links.pid
WHERE usergro_links.eid = %s OR usercomp_links.eid = %s

如果我们在这个查询后面加上 .options(subqueryload(Company.childs)),结果就会变成:

SELECT company.x AS company_x, ..., anon_1.company_id AS anon_1_company_id
FROM (
    SELECT company.id AS company_id
    FROM company
    LEFT OUTER JOIN acc ON acc.id = company.acc
    LEFT OUTER JOIN usercomp_links ON company.id = usercomp_links.pid
    LEFT OUTER JOIN usergro_links ON acc.id = usergro_links.pid
    WHERE usergro_links.eid = %s OR usercomp_links.eid = %s) AS anon_1
INNER JOIN acel_links AS acel_links_1 ON anon_1.company_id = acel_links_1.eid
INNER JOIN company ON company.id = acel_links_1.pid ORDER BY anon_1.company_id

但是这样会非常慢。如果我从第一个查询中获取公司ID,然后手动加载所有子公司,速度会快得多,和这种情况比起来简直是飞快。

我看过文档,也看了代码,但我不知道怎么告诉sqlalchemy只从第一个查询的结果中获取ID,然后用一个相对简单的查询来加载子公司。

我不只是依赖这个例子,我遇到过更复杂的情况,sqlalchemy根本无法加载构建好的查询。那为什么要重复做第一次查询的工作呢?

所以,有人知道怎么在不使用自动构建的“连接再连接”的方式下进行急切加载吗?

暂无标签

4 个回答

0

我在SQLAlchemy的邮件列表上发了个帖子,内容可以在这里找到:https://groups.google.com/d/msg/sqlalchemy/8-kHuliJpr8/PHUZLLtMEQAJ

Boris Serebrov提到的“in”加载方式默认似乎只支持单向操作。如果你不是在使用急切加载(eager loading),那么当你从“一”方访问“一对多”关系时,它仍然会执行查询。

我最后找到了解决办法,可以查看这里:https://gist.github.com/pawl/df5ba8923d9929dd1f4fc4e683eced40

0

你可以选择使用抽象的 ORM 层,这样的话,你需要把孩子属性建模为与 ORM 的 关系,大概是这样的:

from sqlalchemy.orm import relationship

children = relationship("<name of the acl_links class>", lazy="joined")

使用 lazy="joined" 会导致急切加载,这正是你想要的(这和 @vsminkov 提到的 joinedload 是一样的),文档中提到:

任何 relationship() 的默认加载策略都是通过 lazy 这个参数来配置的……在这里我们把它设置为 joined,这样孩子的关系就会通过 JOIN 被急切加载。

在定义关系时,你可以进行很多调整,所以可以查看文档,充分利用这些功能。

或者你也可以选择使用 查询 API,根据自己的需求来组合查询,比如,做一个简单的第二个 where-in 查询,例如:

list_of_ids_previously_loaded_companies = <information from your previous query>

the_session.query(<name of the acl_links class>).filter(<name of the acl_links class>.eid.in_(list_of_ids_previously_loaded_companies)

你甚至可以更深入地使用 表达式语言,像这样:

q = select([acl_links]).where(acl_links.c.eid.in_(list_of_ids_previously_loaded_companies))
the_session.execute(q).fetchall()

作为最后的选择,你可以完全使用 原始 SQL

from sqlalchemy import text

children_results = a_db_connection.execute(text(<SQL STATEMENT STRING>).fetchall()

选择最适合你需求的方式。 请注意,你仍然需要正确建模你的数据库结构,并放置合适的 索引外键 来优化性能。

17

更新:现在在SQLAlchemy中已经实现了“select in”策略(自版本1.2起):请查看文档中的Select IN加载

简而言之:

我认为在可能的情况下应该使用joinedload策略,因为它比其他策略更高效,包括问题中提到的使用“IN”语句加载相关数据的策略。

实际上,“IN”策略可以很容易地在SQLAlchemy之外实现(见下面的代码),而且将其作为新的加载策略实现应该也不复杂(因为它在逻辑上与现有的subqueryload策略类似)。

详细版本:

我开始进行一个简单的实验,看看不同策略产生的查询。

实验的完整源代码在Github上

我的模型看起来是这样的:

class Author(ModelBase):
    __tablename__ = 'authors'
    id = Column(Integer, primary_key=True, nullable=False)
    name = Column(String(255))


class Book(ModelBase):
    __tablename__ = 'books'
    id = Column(Integer, primary_key=True)
    name = Column(String)
    author_id = Column(Integer, ForeignKey('authors.id'))
    author = relationship(
        'Author', backref=backref('books'))

现在,进行测试,首先是懒加载

books = session.query(Book).all()
print books[0].author.name
session.commit()

输出(已清理):

-------------Lazy--------------
sqlalchemy.engine.base.Engine:
SELECT
  books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books

SELECT
  authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id = ?
INFO:sqlalchemy.engine.base.Engine:(1,)
author1

如预期的那样,懒加载会运行一个查询来获取书籍,每次访问作者时又会运行一个查询。

子查询加载:

books = session.query(Book).options(subqueryload(Book.author)).all()
print books[0].author.name
session.commit()

-------------Subquery----------
SELECT
  books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books

SELECT
  authors.id AS authors_id, authors.name AS authors_name,
  anon_1.books_author_id AS anon_1_books_author_id
FROM (
  SELECT DISTINCT books.author_id AS books_author_id
  FROM books) AS anon_1
JOIN authors
  ON authors.id = anon_1.books_author_id
ORDER BY anon_1.books_author_id
author1

对于子查询,我们有两个查询,第一个获取书籍,第二个通过子查询获取作者。

连接加载:

books = session.query(Book).options(joinedload(Book.author)).all()
print books[0].author.name
session.commit()

-------------Joined------------
SELECT
  books.id AS books_id, books.name AS books_name,
  books.author_id AS books_author_id,
  authors_1.id AS authors_1_id, authors_1.name AS authors_1_name
FROM books
LEFT OUTER JOIN authors AS authors_1 ON authors_1.id = books.author_id
author1

连接策略只运行一个查询来获取书籍和作者。

立即加载:

books = session.query(Book).options(immediateload(Book.author)).all()
print books[0].author.name
session.commit()

-------------Immediate---------
SELECT
   books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books

SELECT
  authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id = ?
INFO:sqlalchemy.engine.base.Engine:(1,)

SELECT authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id = ?
INFO:sqlalchemy.engine.base.Engine:(2,)

author1

immediate策略会在第一次查询中加载书籍,然后当我们尝试访问关系时,会为每个相关记录分别运行查询来获取所有相关数据。

看起来在大多数情况下,“joinedload()”应该是最有效的(而且比“IN”策略更高效)——我们只需通过一个查询就能获取所有数据。

现在,让我们尝试在SQLAlchemy之外实现IN策略:

print '-------------IN----------------'
books = session.query(Book).all()
ids = set()
for b in books:
    ids.add(b.author_id)
authors = session.query(Author).filter(Author.id.in_(ids)).all()
print books[0].author.name
print books[1].author.name
print books[2].author.name
print books[3].author.name

输出:

-------------IN----------------
SELECT
  books.id AS books_id, books.name AS books_name, books.author_id AS books_author_id
FROM books

SELECT authors.id AS authors_id, authors.name AS authors_name
FROM authors
WHERE authors.id IN (?, ?)
INFO:sqlalchemy.engine.base.Engine:(1, 2)

author1
author1
author2
author2

如我们所见,它运行了两个查询,然后我们可以访问所有作者。

请注意,我们并没有明确将作者与书籍连接,但当我们尝试通过书籍访问作者时,它仍然有效,因为SQLAlchemy在内部身份映射中找到了作者记录,并没有运行额外的数据库查询。

与上述“IN”策略代码类似的内容可以被概括为一个函数,可以与任何模型/关系一起使用。而且,可能将“IN”策略作为新的SQLAlchemy策略实现相对简单,它与现有的subqueryloading类似——它也应该运行第二个查询来获取相关数据。

3

这是关于SQLAlchemy的内容,SQLAlchemy是一个用来处理数据库的工具。最近,它增加了一个新的功能,你现在可以使用一种叫做 selectinload 的方法来加载数据。

撰写回答