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

Posted

技术标签:

【中文标题】我可以让 SQLAlchemy 在不重复完整原始查询的情况下进行子查询预加载吗?【英文标题】:Can I have SQLAlchemy do subquery eager loading without repeating the full original query? 【发布时间】:2014-11-02 20:33:43 【问题描述】:

假设我们有这样的原始生成查询:

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 无法加载构造查询时,我遇到了更困难的情况。为什么还要从第一次查询开始做所有这些工作?

那么有谁知道如何在没有自动构建的“join from join”样式的情况下进行预加载?

【问题讨论】:

举个例子,来自 Laravel(一个 php 框架)的 Eloquent 就是 eager loading by merely loading the related records directly by ID。例如。获取书籍:select * from books,获取书籍作者:select * from authors where id in (1, 2, 3, 4, 5, ...)(书籍查询已经给了我们作者 ID!) @Agop:您使用的是哪个 RDBMS? @Agop:你有和 OP 类似的查询吗?具体来说,您那里有OR 声明吗?或者您是否真的在寻找急切加载技术的另一种实现方式? @van:Postgres,虽然引擎不重要。 SQLAlchemy 将原始查询嵌入为即时加载的子查询。不管OR 语句如何,这都是极其低效的,尤其是当原始查询本身有点慢时。 @Agop 也许我误解了一个问题,但为什么joinedload 不是解决方案? 【参考方案1】:

更新:“select in”策略现在在 SQLAlchemy 中实现(自 v 1.2 起):请参阅文档中的 Select IN loading。

TLDR:

我认为应该尽可能使用joinedload 策略,因为它比其他策略更有效,包括问题策略中建议使用“IN”语句加载相关数据的策略。

“IN”策略可以很容易地在 SQLAlchemy 的“外部”实现(参见下面的代码),并且将其作为新的加载策略实现可能并不复杂(逻辑上它类似于现有的 @987654325 @ 策略)。

完整版:

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

实验的完整源代码是on 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'))

现在,测试,首先是 lazy 加载:

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”策略更有效)——我们只需通过单个查询获取所有数据。

现在,让我们尝试在 SQL alchemy 之外实现 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 - 它也应该运行第二个查询以获取相关数据。

【讨论】:

很好的答案。我想知道这种自动链接相关对象的sqlalchemy方式是否适用于所有情况,而无需进入数据库。 @van 是的,在您关闭会话之前,SQLAlchemy 会维护所有对象的“身份映射”并从那里获取对象,跳过数据库,请参阅docs.sqlalchemy.org/en/latest/orm/session_basics.html 太棒了,正是我希望有人会写的答案类型。我之前自己也得出过相同的结论,但是对于将来可能遇到相同问题的任何人(尤其是来自其他语言和框架的人,他们可能会发现一些 SQLAlchemy 急切加载),这真的很有帮助选项令人困惑)。长话短说:只需使用joinedload 策略,如果您真的想使用IN 的第二个查询,请编写一个简单的辅助函数来完成。 太棒了,但是您可以从文档中获得所有这些秘密知识,这不是答案。如果你能仔细阅读问题,你会发现当前的策略并不总是一个答案。 关于can easily implement — 告诉我,因为我认为这对迈克以外的任何人来说都不容易。【参考方案2】:

http://docs.sqlalchemy.org/en/latest/orm/loading_relationships.html#sqlalchemy.orm.selectinload

它已添加到 sqlalchemy,所以现在您可以使用 selectinload 策略。

【讨论】:

【参考方案3】:

您可以选择使用抽象 ORM 层,在这种情况下,您将子属性建模为与 ORM relationship 的关系,类似于:

from sqlalchemy.orm import relationship

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

使用lazy="joined" 会按照文档中的要求(这相当于@vsminkov 已经建议的joinedload)进行预加载:

任何 relationship() 的默认加载器策略由惰性关键字参数配置...下面我们将其设置为已连接,以便使用 JOIN 快速加载子关系

在定义关系时可以应用很多调整,因此请查看文档以充分利用它。

或者您可以选择使用Query 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)

你甚至可以使用expression language,比如:

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

万不得已,你完全可以做到raw sql:

from sqlalchemy import text

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

选择最适合您需求的。 请注意,正确建模架构并放置正确的 index 和 foreign keys 以优化性能仍然是您的责任。

【讨论】:

这不能回答问题。问题是在进行预加载时如何避免将复杂的原始查询作为子查询重复。 以上所有选项都避免子查询。很难回答如何不做某事。您写道,第二个 where-in 查询就是您要查找的内容(您在 cmets 中写道,并且一直说有一个“原始查询”),这也是您共享的链接上正在执行的操作。所以请帮助我们帮助你。能否请您展示一下您会在普通 sql 上执行的查询?【参考方案4】:

我在 SQLAlchemy 邮件列表中发了一篇关于此的帖子:https://groups.google.com/d/msg/sqlalchemy/8-kHuliJpr8/PHUZLLtMEQAJ

Boris Serebrov 提到的“in”加载在默认情况下似乎只以一种方式工作。如果您从一对多关系的“一”端访问关系,它仍然会运行查询(如果您不进行预加载)。

我最终得到了这个解决方案:https://gist.github.com/pawl/df5ba8923d9929dd1f4fc4e683eced40

【讨论】:

以上是关于我可以让 SQLAlchemy 在不重复完整原始查询的情况下进行子查询预加载吗?的主要内容,如果未能解决你的问题,请参考以下文章

SQLAlchemy中的PostgreSQL RAW查询[重复]

SQlAlchemy的增删改查

尝试使用 SQLAlchemy 捕获完整性错误

使用sqlalchemy对mysql进行增删改查

使用Postgres和SQLAlchemy过滤数组列

在不丢失原始创建日期的情况下解压缩多个文件[重复]