Mysql 系列 | order by rand()

上一篇 Mysql 系列 | order by 中介绍了全字段排序rowid 排序以及借助索引优化排序。

更复杂的 SQL 中,order by 还会有其他排序方式。

场景

在英语学习 APP 首页,每次打开会随机显示三个英语单词。也就需要每次从表中随机查出三条数据。

表数据越多,随机查询会变得很慢,影响首页打开的速度。

有什么办法可以快速取到随机的三条数据呢!

内存临时表

select word from words order by rand() limit 3;
explain select word from words order by rand() limit 3;
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: words
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9980
     filtered: 100.00
        Extra: Using temporary; Using filesort

  • explain 结果中,Extra 显示 Using temporary 表示要使用临时表。Using filesort 表示需要执行排序。则这条语句需要在临时表上排序

  • 对于内存临时表,回表也是在内存上查找数据,不会导致多访问磁盘。所以对于临时表排序,会选择排序内容较少的 rowid 排序

  • 语句的执行过程如下,

    • 创建一个临时表,临时表使用 memory 引擎。表中有两个字段,一个是 double(简称为 R),一个是 varchar(64)(简称为 W),并且表中没有索引。

    • 从 words 表中,按主键顺序取出所有 word。对于每一个 word,调用函数 rand() 生成 0 和 1 之间的小数。把取到的 word 和随机小数分别放在 W 和 R 字段中。

    • 在临时表上,按照 R 进行排序

    • 初始化 sort buffer,里面有两个字段,一个存放 R,一个是 rowid。memory 引擎中用的不是索引组织表,可以理解为是一个数组,相应的 rowid 就是数组的下标

    • 从临时表中一行一行取出 R 和位置信息(rowid),放入 sort buffer 的两个字段中。这个过程会扫描临时表全表。

    • 在 sort buffer 中,根据 R 值进行排序。这个过程不涉及数据扫描。

    • 排序结束后,取出前三个结果的位置信息(rowid),依次到临时表取出 word,返回给客户端。

磁盘临时表

  • 当临时表内存不够用时,会转移到磁盘上。默认为 16M
mysql> show variables like "tmp_table_size";
+----------------+----------+
| Variable_name  | Value    |
+----------------+----------+
| tmp_table_size | 33554432 |
+----------------+----------+
1 row in set (0.00 sec)
  • 磁盘临时表默认使用的引擎为 InnoDB
mysql> show variables like "internal_tmp_disk_storage_engine";
+----------------------------------+--------+
| Variable_name                    | Value  |
+----------------------------------+--------+
| internal_tmp_disk_storage_engine | InnoDB |
+----------------------------------+--------+
1 row in set (0.00 sec)
  • 因为只取三条数据,这里使用优先队列排序算法,可以精确得到前三个数据,而后面的数据不需要排序。执行顺序如下,

    • 对于 10,000 个准备排序的(R,rowid),取出前三行构成一个堆

    • 取下一个(R,rowid),和堆中最大值比较,比最大值小则替换掉原来的最大值。这个过程中,为了更快地拿到堆中的最大值,最大值总是放在最上面。

    • 以此类推,找到 R 最小的三行数据。这个过程不需要借助临时文件。

    • 得到取出的三行的 rowid,然后去临时表取得 word。

  • 不管使用哪种类型的临时表,order by rand() 计算过程都比较复杂,会耗费比较多的资源。

随机排序方法

方案一

  • 取随机一行数据

    • 取出表中 ID 的最大值 M 和最小值 N

    • 用随机函数生成 M、N 之间的一个数,X = (M - N) * rand() + N

    • 去表中取 ID 不小于 X 的第一条数据

  • 这个方法效率很高,扫描行数很少。但是 id 中间可能有空洞,不能做到真正的随机。

方案二

  • 取随机一行数据

    • 取出表的行数 C

    • 得到 Y = floor(C * rand())

    • 去表中取数据 limit Y,1

  • limit Y,1 的执行顺序是,从表中依次读数据,然后丢掉前面 Y 个,返回第 Y+1 条数据。

  • 方案二的扫描行数为 C + Y + 1,比方案一多很多,不过可以方案一的问题。

  • 按照方案二取三条随机数,则需要算出三个 Y,分别用 limit 找到第 Y+1 条数据。这样扫描行数是 C + Y1 + Y2 + Y3 + 3

  • 对于方案二取三条随机数,可以进一步优化,减少扫描行数。

  • 从扫描行数考虑,方案二和 order by rand() 查不了多少。但是 limit 取数据是根据 ID 索引,远远比前面的构成临时表,再根据 R 字段无索引排序效率要高。


--- 在数据库的使用中,尽量把逻辑写在业务代码中,让**数据库只用来读写数据**。
posted @ 2022-08-15 14:21  菜乌  阅读(1673)  评论(0编辑  收藏  举报