Ruby ransack 在使用的时候,可能需要注意 distinct 的影响

jicheng1014 · 2025年04月17日 · 最后由 stargwq 回复于 2025年04月17日 · 117 次阅读

同步发在了自己的 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 秒

还真没细研究过,我一般用加上就是为了去重,如果确认数据没有重复,就不加

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