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

首页随机展示三个单词。

数据表:

create table `words` (

    id int(11) not null auto_increment,

    word varchar(64) default null,

    primary key (`id`)

)engine=innodb;

 

delimiter;;

create procedure idata()

begin

    declare i int;

    set i =0;

    while i < 10000 do

        insert into words(word) values (concat(char(97+(i div 1000)), char(97+(i % 1000 div 100)), char(97+(i % 100 div 10)), char(97+(i % 10))));

        set i=i+1;

    end while;

end;;

 

call idata();

内存临时表

order by rand(),随机排序然后取前三个。

select word from words order by rand() limit 3;

语句看起来简单,但是执行流程很复杂。

全字段排序和rowid排序,如下:

对于InnoDB来说,执行全字段排序会减少磁盘访问,因此会被优先选择。

但是,对于内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。优化器没有了这一层顾虑,会优先考虑用于排序的行越小越好,所以这时就会选择rowid排序。

上面随机查询三个单词的语句的执行流程:

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

执行流程图:

没有主键的表,MySQL自己会生成一个长度为6字节的rowid来作为主键(这也就是排序模式里rowid的来历)。每个引擎用来唯一标识数据行的信息。  对于有主键的表来说,这个rowid就是主键ID,没有主键的就是系统生成的。

总结:order by rand()使用了内存临时表,内存临时表排序的时候使用了rowid排序方法。

 

磁盘临时表

不是所有的临时表都在内存中。

tmp_table_size这个配置限制了内存临时表的大小,默认值是16M。如果临时表大小超过了tmp_table_size,那么内存临时表就会转成磁盘临时表。

磁盘临时表的默认引擎是InnoDB,是由参数internal_tmp_disk_storage_engine控制的。

当使用磁盘临时表的时候,对应的就是一个没有显示索引的InnoDB表的排序过程。

优先队列算法。

我们的查询并不需要这些数据都是有序的,浪费了非常多的计算量。而优先队列算法就可以精确地只得到三个最小的值。执行流程如下:

  1. 对于这10000个准备排序的(R,rowid),先取前三行,构造成一个堆;
  2. 取下一个行(R',rowid'),跟当前堆里面最大的R比较,如果R'小于R,把这个(R,rowid)从堆里去掉,换成(R',rowid');
  3. 重复第2步,直到第10000个(R',rowid')完成比较。

filesort_priority_queue_optimization部分的chosen=true,表示使用了优先队列排序算法,这个过程不需要临时文件。

这个流程结束后,堆里面就是这10000行里面R值最小的三行。然后依次把它们的rowid取出来,去临时表里面拿到word字段,这个过程就跟上一篇文章的rowid排序的过程一行了。

select city,age from t where city='杭州' order by name limit 1000;

这里用了limit,但是没有用优先队列算法。因为,这条SQL语句是limit 1000,如果使用优先队列算法,需要维护的堆的大小就是1000行的(name,rowid),超过了设置的sort_buffer_size大小,所以只能用归并排序算法。

order by rand()会让计算过程非常复杂,需要大量的扫描行数,排序过程的资源消耗也会很大。

 

随机排序算法

如果只选择一个word值:

随机算法一:

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

select max(id),min(id) into @M,@N from t;

set @X = floor((@M-@N+1) + @N);

select * from t where id >= @X limit 1;

该算法本身不严格,因为id中间可能有空洞,因此选择不同行的概率不一样,不是正真的随机。

随机算法二:

  1. 取的整个表的行数,并记为C。
  2. 取得Y=floor(C * rand())。floor函数在这里的作用,就是取整数部分。
  3. 再用limit Y,1取得一行。

select count(*) from @C from t;

set @Y =foor(@C * rand());

set @sql = concat('select * from t limit ', @Y,)prepare stmt from @sql;

execute stmt;

DEALLOCATE prepare stmt;

由于limit后面不能直接跟变量,所以上面代码中使用了prepare+execute的方法。

 

上一篇:16 | “order by”是怎么工作的?

下一篇:18 | 为什么这些SQL语句逻辑相同,性能却差异巨大?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值