Acadia 如何彻底解决 1+N 查询问题
Solving the 1+N Query Problem
在使用 Ruby 或 Python 等语言中的 ORM 时,看似无害的 for 循环往往会导致 1+N 查询问题,即先执行一次查询,再为每一行结果执行额外查询,严重拖慢性能。Acadia 的查询语言设计灵感源自 1970 年代的 Datalog,通过摒弃递归和 for 循环,从语言层面彻底杜绝了这一问题。这不仅消除了隐式查询带来的性能陷阱,更保证了所有查询必然在多项式时间内终止,绝不会出现无限循环。Acadia 证明了简化语言特性反而能带来更强的性能保证和代码可靠性。
事实证明,我们不仅能彻底消除 1+N 问题,更能保证所有查询必然终止。
HN 评论区
52- red_admiral
这就是当你让 ORM 在不懂 JOIN 的情况下随意操作数据库时会得到的结果。尤其是像 'book.author.name' 这种看似简单的字段解引用,实际上却是通过 Python 的 __getattr__ 或类似机制,在 ORM 代理对象(book)上触发的一次方法调用;如果所需数据尚未加载,它就会发起一个新的查询。
有些 ORM 允许你指定所需数据的范围,比如 Hibernate 就有自己的 Hibernate Query Language。
不过,到了某个阶段,自己手写 SQL 往往是更好的选择。即便没有 JOIN 问题,如果你让 ORM 获取 user id 为 123 的用户,而你只需要他们的名字,除非你明确告诉它,否则 ORM 并不知道这一点,结果你得到的就是一个 'SELECT *' 类型的查询。
- stephen
据我理解,他们的解决方案是“提供一个类 Prolog 的查询 DSL,安全地将类函数式编程的代码转换为 JOIN”。
这听起来不错,但在我看来,1+N 问题通常发生在业务逻辑与数据库加载交织在一起时——比如你的业务逻辑“真的想放在循环里”,因为它是“难以用 SQL 表达”的逻辑。
所以,作者那大约 3 行“没有业务逻辑的循环”例子并不怎么令人信服,声称“解决了 1+N 问题”似乎有些为时过早?
也许,如果你能表达出真正的通用业务逻辑,并且某种方式将其转换为“在数据库端求值”的 SQL,那才更有意义?
(免责声明:我正在开发一个 ORM,它允许交织业务逻辑和数据库加载,同时仍能避免 1+N 问题:https://joist-orm.io/goals/avoiding-n-plus-1s/)
- quibono
不错,我也理解为什么使用 `getAuthorNames` 能在这里解决 N+1 问题。
但是……这难道不是通过移除导致该问题的大部分原因来解决问题的吗?我想大多数人使用 ORM 是为了利用其 SQL 与原生类数据同步的能力。而这里假设人们会运行 Acadia 查询。
顺便说一句,我并非想唱反调,只是我的一般印象是,这些 N+1 问题通常发生是因为人们_想要_直接对象访问,_想要_编写循环,并且_想要_访问字段,同时让底层 SQL 由 ORM 自动排序。
- WilcoKruijer
我真心认为,每个编写查询(即使是 SQL)的工程师都应该阅读 FoundationDB 的数据建模指南 [0]。它真的能让你体会到,聪明的主键选择对查询效率能起到多大作用。通过一些反规范化,甚至不需要 JOIN 就能保证性能。
Postgres 已经支持查询流水线(query pipelining)很久了。在我看来,大多数查询都应该这样编写:顺序执行的查询之间完全不依赖前一个查询的数据。这能让应用速度大幅提升。
- kstrauser
附带一提:我强烈建议像作者在这里做的那样,将其称为“1+N 问题”。当人们谈论“N+1”时,我完全不明白他们在抱怨什么。
N+1:你已经做了 N 次查询,再多做 1 次有那么大不了的么?
1+N:这本来应该只有 1 次查询,但不知怎么搞的,你把它搞成了 1 次加 N 次。
我见过很多次这种查询反模式,知道它很糟糕,但我没意识到这就是人们所说的“N+1”,我以为那肯定是指别的意思。