Ruby on Rails为何执行大量SELECT 1 AS one查询?性能排查
嘿,我来帮你拆解这个问题——你遇到的这些SELECT 1 AS one查询其实是Rails里很常见的一个坑,我来一步步给你讲清楚来源和解决办法:
一、SELECT 1 AS one 查询到底是什么?
这个查询是Rails的exists?方法生成的,当代码里需要检查「某个关联是否存在」或者「某个记录是否在数据库里」时就会触发。比如调用service.points.exists?或者无参数的service.points.any?,都会生成这条SQL。哪怕你已经用includes预加载了关联数据,某些方法还是会默认绕开内存里的缓存,直接去查数据库。
二、为什么你的场景里会批量触发?
结合你的代码来看,问题大概率出在两个地方:
- 首先,你的
with_pointsscope里的where.not(points: [])其实有点多余——joins(:points)是内连接,本身就会自动过滤掉没有关联Points的Service,所以这个条件其实是重复筛选,而且Rails处理where.not(points: [])时,底层会用exists子查询,但这一步是在控制器查询时完成的,还不是批量查询的原因。 - 真正的元凶是视图遍历阶段:如果你的视图里写了类似
<% if service.points.any? %>的代码,哪怕你用includes(:points)预加载了所有Points数据,无参数的any?方法依然会触发单独的SELECT 1查询——因为它默认会直接去数据库验证,而不是检查已经加载到内存里的Points集合。
当然也有小概率是Service模型里的回调、验证或者关联配置(比如dependent选项)触发的,但最常见的还是视图里的方法调用。
三、怎么解决这个性能问题?
这里给你几个实用的方案,按优先级排序:
1. 把视图里的any?换成present?
既然你已经用includes(:points)预加载了所有Points数据,直接用service.points.present?就能检查关联是否非空——这个方法会直接用内存里的预加载数据,不会触发任何额外的数据库查询,完美解决批量查询的问题。
2. 优化你的with_points scope
刚才说过,joins(:points).distinct已经能筛选出有至少一个Points关联的Service了,where.not(points: [])完全是多余的,删掉它能简化查询,还能避免框架后续可能的冗余检查:
scope :with_points, -> { joins(:points).distinct }
3. 强制用预加载的数据(如果必须用any?)
如果你因为某些业务逻辑必须保留any?,可以先把关联数据转成数组,再调用any?:
service.points.to_a.any?
to_a会强制使用已经加载到内存里的预加载数据,之后的any?就不会去查数据库了。
4. 试试用eager_load替代includes(可选)
includes在某些场景下会分成两次查询(先查Service,再查关联的Points),而eager_load会直接用左外连接一次性加载所有数据。如果你的查询本来就是要筛选有Points的Service,结合joins使用可能会更高效:
@services = Service.eager_load(:points).with_points
不过这个方案需要你结合实际数据量测试,不一定总是最优,但可以作为备选。
总结
你遇到的批量SELECT 1查询,90%以上的概率是视图里的points.any?导致的。按照上面的步骤调整,应该就能彻底消除这些冗余查询,提升页面的加载速度。
内容的提问来源于stack exchange,提问作者Vincent

