我正在开发一个项目(Rails 3.0.15,ruby 1.9.3-p125-perf),其中 db 位于 localhost 中,并且 users 表有一点超过 10 万条记录。
使用
按 RAND() 排序
很慢
User.order("RAND(id)").first
变成
SELECT users.* FROM users ORDER BY RAND(id) LIMIT 1
响应时间为 8 到 12 秒!!
Rails 日志:
用户负载 (11030.8ms) SELECT users.* FROM users ORDER BY RAND()
限制 1 个
来自mysql的解释
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 110165 | Using temporary; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+--------+---------------------------------+
您可以看到没有使用索引(possible_keys = NULL),创建了一个临时表,并且需要额外的 pass 才能获取所需的值(extra = Using temporary; Using文件排序)。
另一方面,通过将查询分成两部分并使用 Ruby,我们在响应时间上得到了合理的改进。
users = User.scoped.select(:id);nil
User.find( users.first( Random.rand( users.length )).last )
(;nil 用于控制台)
Rails 日志:
用户负载 (25.2ms) SELECT id FROM users 用户负载 (0.2ms) SELECT
users.* FROM users WHERE users.id = 106854 限制 1
mysql 的解释证明了原因:
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
| 1 | SIMPLE | users | index | NULL | index_users_on_user_type | 2 | NULL | 110165 | Using index |
+----+-------------+-------+-------+---------------+--------------------------+---------+------+--------+-------------+
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
我们现在可以只使用索引和主键,完成这项工作的速度提高了大约 500 倍!
更新:
正如icantbecool in cmets所指出的,如果表中有删除记录,上述解决方案存在缺陷。
一种解决方法可以是
users_count = User.count
User.scoped.limit(1).offset(rand(users_count)).first
转化为两个查询
SELECT COUNT(*) FROM `users`
SELECT `users`.* FROM `users` LIMIT 1 OFFSET 148794
并在大约 500 毫秒内运行。