17 | 如何正确地显示随机消息?

Page content

17 | 如何正确地显示随机消息?

排序的选择

  • 对于 InnoDB 表的排序,全字段排序会减少回表的次数,性能更佳,会被优先选择
  • 对于存在于内存里的表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘,所以行越小排序性能越好

order by rand() 的执行过程

  • 建表语句

    mysql> CREATE TABLE `words` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `word` varchar(64) DEFAULT NULL,
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB;
    
  • 查询语句

    select word from words order by rand() limit 3;
    
  • explain 结果 explain

    Using temporary表示需要使用临时表;Using filesort 表示需要执行排序操作;

  • 全字段排序流程图

  • rowid排序流程图

  • 执行流程(假设内存足够放下临时表,内存不够时使用磁盘临时表)

    • 创建一个临时表。这个临时表使用的是 memory 引擎,表里有两个字段,第一个字段是 double 类型,为了后面描述方便,记为字段 R,第二个字段是 varchar(64) 类型,记为字段 W。并且,这个表没有建索引。
    • 从 words 表中,按主键顺序取出所有的 word 值。对于每一个 word 值,调用 rand() 函数生成一个大于 0 小于 1 的随机小数,并把这个随机小数和 word 分别存入临时表的 R 和 W 字段中,到此,扫描行数是 10000。
    • 现在临时表有 10000 行数据了,接下来你要在这个没有索引的内存临时表上,按照字段 R 排序。
    • 初始化 sort_buffer。sort_buffer 中有两个字段,一个是 double 类型,另一个是整型(记录位置信息)。
    • 从内存临时表中一行一行地取出 R 值和位置信息(类似rowid),分别存入 sort_buffer 中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加 10000,变成了 20000。
    • 在 sort_buffer 中根据 R 的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。
    • 排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出 word 值,返回给客户端。这个过程中,访问了表的三行数据,总扫描行数变成了 20003。
  • 执行流程图

优先队列排序算法

如果内存不足以存放临时表,除了使用磁盘临时表外,还有一个选择:使用优先队列排序算法。

结合上述可知,我们只是要取出临时表中 R 值最大的三条记录,那我们可以维护一个最大堆,堆的大小限制为 3,遍历临时表中的每条记录,获取最大的3个即可。

如果最终获取的行数过多,不适用优先队列排序算法

正确地去随机记录的方法

数据无空洞或空洞很少

  • 取得这个表的主键 id 的最大值 M 和最小值 N;
  • 用随机函数生成一个最大值到最小值之间的数 X = (M-N)*rand() + N;
  • 取不小于 X 的第一个 ID 的行。

数据有空洞时

  • 取得整个表的行数,并记为 C。
  • 取得 Y = floor(C * rand())。 floor 函数在这里的作用,就是取整数部分。
  • 再用 limit Y,1 取得一行。 不牵涉排序,MySQL一行一行取出,并丢掉前 Y 行,获取第 Y+1 行,共扫描行数 C + Y + 1

问题讨论

对于数据有空洞时的取随机记录的发放,需要扫描的行数为 C + (Y1+1) + (Y2+1) + (Y3+1),能否进一步优化,减少扫描函数

答案 取 Y1、Y2 和 Y3 里面最大的一个数,记为 M,最小的一个数记为 N,然后执行下面这条 SQL 语句: select * from t limit N, M-N+1; 扫描的行数为 C + M + 1