Rails 浅谈 ActiveRecord 的 N + 1 查询问题(copy)
【说明:资料来自https://ruby-china.org/topics/32364】
ORM框架的性能小坑
在使用ActiveRecord这样的ORM工具时,常会嵌套遍历model。
例如,有两个model,Post、Comment,关系是一对多。
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
总共有4个post。
> Post.count
(0.1ms) SELECT COUNT(*) FROM "posts"
=> 4
获取每个post的所有comment,我们可以:
> Post.all.map{|post| post.comments}
Post Load (0.3ms) SELECT "posts".* FROM "posts"
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 1]]
Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 2]]
Comment Load (0.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 3]]
Comment Load (0.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? [["post_id", 4]]
可以看到为了得到4条数据,我们执行了5(4 + 1)次的查询,这就是所谓N + 1查询问题。
发现问题
除了凭经验外,有一些gem也可以帮助我们提早发现N + 1查询问题。
例如收费的New Relic,免费的Bullet。
解决问题
预加载
简单来说,就是提前加载model关系,让ActiveRecord预先加载所需要的数据。
ActiveRecord提供了以下三个方法预加载。
includes
preload
eager_load
他们的区别可以参考这里或这里。
以最常用的includes
方法为例。
> Post.includes(:comments).map{|post| post.comments}
Post Load (0.2ms) SELECT "posts".* FROM "posts"
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3, 4)
得到的结果一样,但执行的查询只有两次。
傻瓜式预加载(Goldiloader)
传统预加载的“问题”
includes
方法的确很惊艳,但……
第一,代码不够优雅。
例如,假设我们现在想找的是id在1到3之间的post的comment。
一般的我们的逻辑是,查找id在1到3之间的post,获取各post的comment然后合并。
而预加载后的逻辑是,查找id在1到3之间的post,关联comment,再获取各post的comment然后合并。
总觉得有点冗余。
> Post.where(id: 1..3).includes(:comments).map{|post| post.comments}
Post Load (0.5ms) SELECT "posts".* FROM "posts" WHERE ("posts"."id" BETWEEN ? AND ?) [["id", 1], ["id", 3]]
Comment Load (0.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3)
第二,不符合DRY。
既然我们都不喜欢N + 1,那就应该从源头上杜绝,而不是每次查询时都要主动includes
一次。
Goldiloader
懒癌程序员的救星Goldiloader——几乎完美的解决了以上两个问题。
gem 'goldiloader'
bundle install
以后,就可以用最直接(傻瓜)的方式点点点……
> Post.where(id: 1..3).map{|post| post.comments}
Post Load (0.2ms) SELECT "posts".* FROM "posts" WHERE ("posts"."id" BETWEEN ? AND ?) [["id", 1], ["id", 3]]
Comment Load (0.3ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3)
auto_include
Goldiloader默认自动加载所有关联数据,用auto_include: false
可以方便地关闭自动加载。
class Post < ApplicationRecord
has_many :comments, auto_include: false
end
fully_load
以下的方法比较特殊,如果关系已经加载了,则会直接返回已缓存的值,如果没被加载,则会通过SQL查询。
- first
- second
- third
- fourth
- fifth
- forty_two
- last
- size
- ids_reader
- empty?
- exists?
假设现在我们需要获取每个post的最新的comment。
但这不是我们想要的。
> Post.all.sum{|post| [post.id, post.comments.last&.content]}
Post Load (0.1ms) SELECT "posts".* FROM "posts"
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."id" DESC LIMIT ? [["post_id", 1], ["LIMIT", 1]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."id" DESC LIMIT ? [["post_id", 2], ["LIMIT", 1]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."id" DESC LIMIT ? [["post_id", 3], ["LIMIT", 1]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."id" DESC LIMIT ? [["post_id", 4], ["LIMIT", 1]]
添加选项full_load: true
后,当调用上述方法时,Goldiloader会强制自动加载所需的关系。
class Post < ApplicationRecord
has_many :comments, fully_load: true
end
这才是我们想要的。
> Post.all.sum{|post| [post.id, post.comments.last&.content]}
Post Load (0.3ms) SELECT "posts".* FROM "posts"
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3, 4)
Goldiloader也不是万能的
has_one使用SQL limit时的隐患
Goldiloader是ActiveRecord的衍生工具,所以ActiveRecord预加载的副作用也一并继承了。
现在我们自定义一个has_one
关系,用以获取最新的一条comment。
class Post < ApplicationRecord
has_many :comments, fully_load: true
has_one :latest_comment, -> { order(created_at: :desc) }, class_name: 'Comment'
end
遍历post获取最新的comment。
> Post.all.map{|post| post.latest_comment}
不使用Goldiloader或者预加载时,每条SQL自动回加上limit 1
。
Post Load (0.3ms) SELECT "posts".* FROM "posts"
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."created_at" DESC LIMIT ? [["post_id", 1], ["LIMIT", 1]]
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."created_at" DESC LIMIT ? [["post_id", 2], ["LIMIT", 1]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."created_at" DESC LIMIT ? [["post_id", 3], ["LIMIT", 1]]
Comment Load (0.1ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ? ORDER BY "comments"."created_at" DESC LIMIT ? [["post_id", 4], ["LIMIT", 1]]
使用Goldiloader或者预加载时,世界变清净了,但同时会有性能隐患,因为post的数据量可能非常大。
Post Load (0.5ms) SELECT "posts".* FROM "posts"
Comment Load (0.2ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3, 4) ORDER BY "comments"."created_at" DESC
其他限制
遇到以下的关系(方法),Goldiloader会自动关闭自动预加载。
- limit
- offset
- finder_sql
- group (due to a Rails bug)
- from (due to a Rails bug)
- joins (only Rails 4.0/4.1 - due to a Rails bug)
- uniq (only Rails 3.2 - due to a Rails bug)
本文结束之前
N + 1查询问题是一个容易被忽略的问题。
发现解决它也不难,includes
已经够用,Goldiloader更是锦上添花,对新手足够友好。
不过对于我这种被Rails“坑”习惯的斯德哥尔摩症候群患者来说,没有includes
反而没安全感了>_<|||
参考文档
Rails 浅谈 ActiveRecord 的 N + 1 查询问题(copy)的更多相关文章
- 浅谈T-SQL中的子查询
引言 这篇文章我们来简单的谈一下子查询的相关知识.子查询可以分为独立子查询和相关子查询.独立子查询不依赖于它所属的外部查询,而相关子查询则依赖于它所属的外部查询.子查询返回的值可以是标量(单值).多值 ...
- 浅谈T-SQL中的联接查询
引言 平时开发时,经常会使用数据库进行增删改查,免不了会涉及多表联接.今天就简单的记录下T-SQL下的联接操作. 联接类型及其介绍 在T-SQL中联接操作使用的是JOIN表运算符.联接有三种基本的类型 ...
- 浅谈sql 、linq、lambda 查询语句的区别
浅谈sql .linq.lambda 查询语句的区别 LINQ的书写格式如下: from 临时变量 in 集合对象或数据库对象 where 条件表达式 [order by条件] select 临时变量 ...
- 浅谈SQL优化入门:1、SQL查询语句的执行顺序
1.SQL查询语句的执行顺序 (7) SELECT (8) DISTINCT <select_list> (1) FROM <left_table> (3) <join_ ...
- 浅谈oracle树状结构层级查询之start with ....connect by prior、level及order by
浅谈oracle树状结构层级查询 oracle树状结构查询即层次递归查询,是sql语句经常用到的,在实际开发中组织结构实现及其层次化实现功能也是经常遇到的,虽然我是一个java程序开发者,我一直觉得只 ...
- 浅谈oracle树状结构层级查询测试数据
浅谈oracle树状结构层级查询 oracle树状结构查询即层次递归查询,是sql语句经常用到的,在实际开发中组织结构实现及其层次化实现功能也是经常遇到的,虽然我是一个java程序开发者,我一直觉得只 ...
- oracle树形结构层级查询之start with ....connect by prior、level、order by以及sys_connect_by_path之浅谈
浅谈oracle树状结构层级查询 oracle树状结构查询即层次递归查询,是sql语句经常用到的,在实际开发中组织结构实现及其层次化实现功能也是经常遇到的,虽然我是一个java程序开发者,我一直觉得只 ...
- 浅谈MySQL中优化sql语句查询常用的30种方法 - 转载
浅谈MySQL中优化sql语句查询常用的30种方法 1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引. 2.应尽量避免在 where 子句中使 ...
- c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程
c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...
随机推荐
- Cloud BOS平台-自定义用户联系对象
适用业务场景:新增用户时,联系对象类型默认为:职员.客户.供应商.客户需要增加一类"承运商",类型选择"承运商"时,联系对象只显示相应的承运商."承运 ...
- Java多线程干货系列—(二)synchronized
原文地址:http://tengj.top/2016/05/03/threadsynchronized2/ <h1 id="前言"><a href="# ...
- Codevs 3409 搬礼物
时间限制: 1 s 空间限制: 64000 KB 题目等级 : 青铜 Bronze 题目描述 Description 小浣熊松松特别喜欢交朋友,今年松松生日,就有N个朋友给他送礼物.可是要把这些礼 ...
- ES6__数据结构 Map
/* 数据结构 Map */ /* * 字典:是用来存储不重复的key的hash结构.不同于集合(Set)的是,字典使用的是[键,值]的形式来储存数据的. *javaScript 的对象(Object ...
- SQL SERVER 2012 第五章 创建和修改数据表 の CREATE语句
CREATE <object type> <object name> CREATE DATABASE <database name> 比较完整的语法列表: 日志文件 ...
- scoi2018游记
day1: t1点分树 冬令营上jry讲过原题,t2启发式合并+解二次同余方程 预计100+100+0 结果t1卡内存,t2模数太大.导致调试到没有写t3 最后t1 85 t2 15 要是我会o1快速 ...
- HDU 3001【状态压缩DP】
题意: 给n个点m条无向边. 要求每个点最多走两次,要访问所有的点给出要求路线中边的权值总和最小. 思路: 三进制状态压缩DP,0代表走了0次,1,2类推. 第一次弄三进制状态压缩DP,感觉重点是对数 ...
- 解决Coldfusion连接MySQL数据库的问题
在连接MySQL时,出现了如下错误: Connections to MySQL Community Server are not supported. Please contact MySQL to ...
- python第四讲
三元运算符: 三元运算又叫三目运算,是对简单的条件语句的缩写. 书写格式: n1 = 值1 if 条件 else 值2 # 如果条件成立,那么将 “值1” 赋值给n1变量,否则,将“值2”赋值给n1变 ...
- HDU1306 String Matching 【暴力】
String Matching Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) ...