翻译 Rails :ActiveRecord 查询的小贴士

ashley · 2016年01月27日 · 最后由 wuguzaliang 回复于 2018年03月26日 · 4910 次阅读

最近太忙了,一直没抽出时间翻译一些好的文章,趁着年会休息的空档继续写写~~ 下面是文章 Rails: Tips for Speeding up ActiveRecord Queries 的翻译,作者 NICK REYNOLDS,原文链接http://www.webascender.com/Blog/ID/553/Rails-Tips-for-Speeding-up-ActiveRecord-Queries

运用 ActiveRecord 是使用 Ruby on Rails 的乐趣之一。它简短且贴心 – 将可能十分冗长的 SQL 语句变成简短且可读性高的 Ruby 语句。但是,如果不小心,轻易写出来的 ActiveRecord 请求会产生表现不佳的 SQL,或让你的 app 内存占用飙升—— 尤其是在你处理大型数据库表的情况下。幸运的是,ActiveRecord 为编写高性能查询提供了一个极好的工具包。这里,我们将重点介绍其中几个工具和一些注意事项。

在下面例子中,我生成了一个大型在线零售商一般都会用到的交易表。这个表包含几百万行的数据,每行都有保存金额、用户 ID 和其他各种关于不同事务的元数据。

sum(&:amount) vs. sum(:amount)

两个调用方式中其中一种比另一种快非常多:

Transaction.sum(&:amount) Transaction.sum(:amount)

它们之间的区别是微妙的。但是第一个调用语句中那个额外的”&” (查看Symbol#to_proc),使得求和采用Array#sum来计算,而不是SQL。这意味着rails必须选择所有的列,并将每行数据的模型实例化来构建一个潜在的巨大数组。第二个语句直接让数据库求和,有时候这可能比Ruby更好,而且这个方法完全不用选择任何多余的列或者创建任何模型:

SELECT SUM(amount) FROM transactions;

好消息是在 Rails 4.0 中 含有 block 的 ActiveRecord::Calculations#sum 已被弃用,所以如果不小心加了‘&’会收到警告。

pluck vs. map Transaction.all.map(&:user_id) Transaction.pluck(:user_id)

map(又名 collect)是另外一个有用的数组方法。遗憾的是,这里会有问题。因为在执行数组时,当我们想要的可能只是一个单独的列,然而在上面的例子中 rails 需要选择对所有的列实例化行成模型,即使我们只是用在 user_id 之后,交易表中所有列会被选中。

通过 pluck 的方法(rails 3.2.1 以上版本可用)只需要选择表格中的单一列,不用实现模型实例化。如下,上面 SQL 的示例变得简单多了:

SELECT user_id FROM transactions;

当然,如果你已经有了一系列相关的计算模型,映射在数组上可能比数据库查询更快。这种情况下最好衡量和比较下两种方法。

uniq

想象一下你想在交易表上建立唯一的用户 id。使用 pluck 方法,我们可以很容易得到这样一个列表。搭配使用 uniq,可以最少有 2 种方法去过滤列表中重复项,并且其中之一肯定比另一个快得多:

Transaction.pluck(:user_id).uniq Transaction.uniq.pluck(:user_id)

选用的方法无论是实例化模型还是选择任何额外字段。其不同之处在于在什么位置过滤重复项。在第一种情况下,pluck方法为交易表的每一行都返回一个user_ids的数组。然后用Array#uniq过滤重复项。

在第二种情况是,uniq 方法实际上是 ActiveRecord::QueryMethods#uniq,它增加了 DISTINCTkeyword 生成 SQL。

SELECT DISTINCT user_id FROM transactions

如果 user_id 列加上索引的话,数据库可能又会再次加速。

find_each vs. each

假设你要为每一个交易执行一些相关处理。对于一小部分的结果,使用 each 会有很好的效果:

Transaction.where(processed: false).each { |t| ... }

现在,假设你的结果集含有成千上万条记录。随着 all.each, 整个结果集需加载到内存来对数组进行迭代。在这一点上,app 很可能直接把内存耗尽。

为了防止这种情况,ActiveRecord 提供 find_each 方法,这种方法是内部查询的结果集设置在 1000 批次,以便整个结果集不被一次性加载到内存中。从表面上看,这是一个与 each 完全相同的接口:

Transaction.where(processed: false).find_each { |t| ... }

如果 1000 不是你的强项,find_each 只是个围绕ActiveRecord::Batches#find_in_batches的包装器,其批量和偏移量是可以进行设置.

joins vs. includes

在 includes 与 joins 之间做出正确的选择会对性能带来影响。 如果你需要访问一个关联,有一个很好的点子是使用 includes 预载入。因为 joins 只会在 SQL 上增加 JOIN 表达式,如果你试图访问一个关联(例如 user.transactions),rails 还是要用另外的 SELECT 来加载关联的每一个行。运用 includes,rails 可以在作用域里加载一个 has_many 关联。另一方面,如果添加表格仅用于过滤在 SQL 的结果集,只需继续使用 joins。如果使用 includes 将选择一些不会用到的列和实例化模型。

留意 SQL

警惕那些在 ActiveRecord 查询生成的 SQL,可能会产生糟糕的 SQL 查询和 Ruby 计算。首先你要检查 development.log。这里,有些好的 gems 来帮助你分析你的 APP 和提醒你去处理出现的问题: Bullet-提醒你应该立即加载 N+1 查询。

Peek-利用这个便利的工具查看这些请求在哪里经过/耗时。

RailsPanel- 在 Chrome 的调试窗口中显示请求信息。

很细心。

不错不错,经验的积累

需要 登录 后方可回复, 如果你还没有账号请 注册新账号