同步发在了自己的 blog 3qruok.com 的 ransack 在使用的时候,可能需要注意 distinct 的影响
TLDR:
当你涉及搜索的模型比较多的时候(比如 千万),建议搜索页面初始时不要使用 result(distinct: true). 如果可能,穷举搜索条件,在不需要 join 表的搜索避免使用 distinct
废话文学:
在我们使用 ransack 的时候,我们经常习惯性的弄出一个类似这样的写法
Student.ransack(params[:q]).result(distinct: true)
有可能有人不太清楚为啥这里会有一个 distinct: true
, 有时候似乎不加也没有影响
实际上的原因是 这里是为了防止你在搜索的时候,搜索的是子条件,然而满足子条件是需要 join 表的,这样会导致主查询的数据变为多条
举个例子
Students 是学生表 has_many
book_rents
, 表结构为 id, name, gender
BookRents 是借书记录,记录了学生借出的书名, belongs_to
student
, 表结构为 id, student_id, book_name
假设 搜索 name_eq , 其实加不加 distinct 是没有影响的,因为查询不涉及到 join 表
但是
此时如果查询 借了“ruby 镐头书”的学生,则 搜索条件为。params[q][:book_rents_book_name_eq] = "ruby 镐头书”
如果不加 distinct 则查询记录,假设某个学生借了两次这本书,则最终结果就会出现两个相同的 student
官方为了避免沙雕,所以一般情况下,在 demo 的时候都默认了加入了 distinct: true,
而大多数人,选择了直接抄。。。
然而当数据量变大的时候,问题就变得严重了。
一般在系统中,初始的搜索页面,数据量往往是非常巨大的。如果你的初始 index 页面的数据量上了 千万级别后 就会有个非常尴尬的地方
系统会首先读默认的搜索结果
于是一个非常尴尬的事情出现了:
你可能有上亿的数据,虽然你做了分页,但是 ransack 会首先执行一次 distinct 你的 id,再去 count 算总数
这意味着数据库实际上是把整个表的 id 先加载出来再去重,而这一步,会比较漫长。
然而实际的情况是,默认搜索情况下,是不会涉及到 join 表的,只可能涉及到展示数据时候 includes 表,这样 distinct 的运算完全是浪费掉了。
那么怎么解决呢?
对症下药咯,加个判断,在默认情况下,result 的 distinct 为 false 或者直接不配置 distinct 即可,
更好的方式是 你需要实现一个 need_using_distinct?(params[:q]) 的方法,将需要 joins 表的情况都考虑清楚,这样每次搜索的时候,就会根据你的查询进行优化,避免浪费性能
那么,这么做可以优化多少呢?额 千万级别的数据,从原来的查询时间是 几十秒 到现在 0.x 秒