哪个是避免n + 1问题最快的表现方式?为什么?

时间:2011-09-23 08:28:22

标签: sql optimization orm select-n-plus-1

我希望添加一些实用工具方法来帮助避免遗留应用程序中的大量n+1 issues

常见的模式是:

select a.* /* over 10 columns */
from [table-A] a
where /* something */

检索到ClassA记录实例的集合

然后检索到的子实例是懒惰的:

select b.* /* over 10 columns */
from [sub-table-B] b
where b.ParentId = @ClassA_ID

这会导致n + 1选择问题。大多数情况下,这不是一个主要问题,因为在不经常命中的页面上只检索了几个ClassA实例,但是在越来越多的地方,这个n + 1问题导致页面变得太慢而不像应用程序已缩放。

我正在寻找替换此应用程序的现有数据访问代码的一部分,以便一起检索ClassA实例和ClassB实例。

我认为有三种方法可以做到:

1)像我们现在一样获取ClassA个实例,然后在一个聚合调用中获取ClassB个实例:

select b.*
from [sub-table-B] b
where b.ParentId in ( /* list of parent IDs */ )

这是两个单独的数据库调用,动态SQL的查询计划将不可缓存(由于ID列表)。

2)使用子查询获取ClassB个实例:

select b.*
from [sub-table-B] b
    inner join [table-A] a
        on b.ParentId = a.[ID]
where /* something */

这也是两个数据库调用,对[table-A]的查询必须进行两次评估。

3)将所有内容集中在一起,并对ClassA个实例进行重复删除:

select a.*, b.*
from [table-A] a
    left outer join [sub-table-B] b 
        on a.[ID] = b.ParentId
where /* something */

这只是一个数据库调用,但现在我们重复了[table-A]的内容 - 结果集会更大,从数据库向客户端发送数据的时间会更多。

所以这确实是3个可能的妥协:

  1. 2个DB调用,无查询缓存
  2. 2次DB调用,复杂查询评估两次
  3. 1个数据库调用,结果集明显更大
  4. 我可以为任何一个父子对表测试这三种模式,但我有很多这些模式。我想知道的是哪种模式一直更快?更重要的是为什么?其中一个妥协是一个明显的性能杀手吗?

    Linq,EF和NHibernate等现有机制使用了什么?

    第四种方式是否优于全部3种方式?

3 个答案:

答案 0 :(得分:1)

我认为EF和L2S使用你的第三种方法 - 肯定只有一个db调用。

正常情况下,更多的db往返比使用更大的结果集的更少的db往返需要更多的时间。

也许有一些边缘情况,你在表A中有大量数据,而较大的结果集会增加对客户的传输时间。

但这主要是数据库服务器和客户端之间的延迟和带宽问题。

第四种方法可能是编写一个返回多个结果集的存储过程。每个表一个,只查询您需要的记录。这符合您的第一种方法,但减少到一次往返。但这会使事情变得复杂,并不像其他方法那样灵活。

答案 1 :(得分:0)

在我看来,“哪种方式最快”取决于数据库服务器的延迟和带宽,以及结果集的大小。

在延迟是瓶颈的情况下(ADSL网络?),如果您的结果集不是很大,您最好向服务器发送一个查询。使用的带宽会更大,因为[table-A]记录被多次发送,但从全球来说,这可能是将数据传送到客户端的最快方式。

答案 2 :(得分:0)

大多数现代数据库(Oracle肯定会使用参数化查询)将缓存查询评估,您将遇到很少的打击。

某些ORM(如Django's)将允许您创建自定义查询,并仅返回呈现页面所需的部分结果。这是一个很好的方法 - 如果你看到一个数据库热点优化它,但另外让ORM进行其出价。

请记住,硬件很便宜(两天顾问的工作成本与服务器升级相同),无论您的财务经理说什么。