我可以让 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 在不重复完整原始查询的情况下进行子查询预加载吗?的主要内容,如果未能解决你的问题,请参考以下文章