Rails 5查询优化:获取多集合关联记录计数以减少查询量
嘿,我帮你搞定这个让人头疼的N+1查询问题!你现在的做法是先拉取主集合,再挨个查关联计数,这不就导致查询次数蹭蹭往上涨嘛——10条medication就11次查询,10个集合直接飙到110次,这性能肯定顶不住。结合你用的PostgreSQL 9.6,我给你几个实用的重构方案:
1. 单个集合:一次查询搞定主数据+关联计数
先从单个集合(比如@medications)入手,我们可以把计数逻辑直接嵌入初始查询,不用再遍历执行额外查询:
方案A:用相关子查询
@medications = Medication.select( :id, :name, '(SELECT COUNT(*) FROM records WHERE records.medication_id = medications.id) AS record_count' ).where(office_id: current_user.selected_office)
执行完这个查询后,每个medication对象都会自带record_count属性,直接用就行,不用再调用count触发新查询。原理是PostgreSQL会在主查询的SELECT阶段,为每条medication计算对应的关联记录数。
方案B:LEFT JOIN + GROUP BY
如果你觉得子查询不够直观,也可以用左连接+分组统计的方式:
@medications = Medication.select( 'medications.id', 'medications.name', 'COUNT(records.id) AS record_count' ).left_joins(:records) .where(office_id: current_user.selected_office) .group('medications.id, medications.name')
注意PostgreSQL 9.6对GROUP BY要求比较严,所有非聚合的SELECT字段都得放进GROUP BY里,不然会报错。
2. 多个集合:批量优化,把查询次数砍到最低
如果要同时处理@medications、@treatments、@therapies这类10个集合,有两种思路:
思路1:每个集合单独用上述优化
每个集合只需要1次查询,10个集合就是10次查询,相比原来的110次,已经大幅减少。这种方式改代码最简单,不需要大动干戈,适合快速解决问题。
思路2:用PostgreSQL CTE合并成单次查询
如果想把查询次数压到最低,可以用公共表表达式(CTE)把多个集合的查询合并成一次,不过代码复杂度会高一点:
WITH medications_counts AS ( SELECT m.id, m.name, COUNT(r.id) AS record_count, 'medication' AS type FROM medications m LEFT JOIN records r ON r.medication_id = m.id WHERE m.office_id = ? GROUP BY m.id, m.name ), treatments_counts AS ( SELECT t.id, t.name, COUNT(r.id) AS record_count, 'treatment' AS type FROM treatments t LEFT JOIN records r ON r.treatment_id = t.id WHERE t.office_id = ? GROUP BY t.id, t.name ) -- 这里继续添加其他集合的CTE SELECT * FROM medications_counts UNION ALL SELECT * FROM treatments_counts; -- 继续UNION ALL其他集合的结果
在Rails里可以用ActiveRecord::Base.connection.execute执行这段SQL,然后把返回的结果按type字段拆分到对应的变量里。这种方式把10个集合的查询合并成1次,适合对性能要求极高的场景。
3. 数据库层面的进阶优化(可选)
如果这类计数查询特别频繁,还可以从数据库端入手:
- 创建数据库视图:把主表数据+关联计数做成一个视图,然后在Rails里建个模型映射到这个视图,查视图就像查普通表一样简单。
- 添加缓存字段:在
medications这类主表里加个record_count字段,然后通过Rails回调(比如records创建/销毁时更新计数)或者数据库触发器来维护这个字段。这种方式查询速度最快,但要注意维护缓存的一致性,适合计数不需要绝对实时的场景。
最后验证一下
你可以用Rails的Bullet gem来检测优化后的查询是否还有N+1问题,跑一遍代码确认查询次数符合预期就行。
内容的提问来源于stack exchange,提问作者Sean Kelley

