首页随机展示三个单词。
数据表:
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排序。
上面随机查询三个单词的语句的执行流程:
- 创建一个临时表。这个临时表使用的是memory引擎,表里有两个字段,第一个字段是double类型,为了后面描述方便,记为字段R,第二个字段是varchar(64)类型,记为字段W。并且这个表没有建索引。
- 从words表中,按主键顺序取出所有的word值。对于每一个word值,调用rand()函数生成一个大于0小于1的随机小数,并把这个随机小数和word分别存入临时表的R和W字段中,至此,扫描行数是10000。
- 现在临时表有10000行数据了,接下来要在这个没有索引的内存临时表上,按照字段R排序。
- 初始化sort_buffer,sort_buffer中有两个字段,一个是double类型,另一个整型。
- 从内存临时表中一行一行地取出R值和位置信息,分别存入sort_buffer中的两个字段里。这个过程要对内存临时表做全表扫描,此时扫描行数增加10000,变成20000。
- 在sort_buffer中根据R的值进行排序。注意,这个过程没有涉及到表操作,所以不会增加扫描行数。
- 排序完成后,取出前三个结果的位置信息,依次到内存临时表中取出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表的排序过程。
优先队列算法。
我们的查询并不需要这些数据都是有序的,浪费了非常多的计算量。而优先队列算法就可以精确地只得到三个最小的值。执行流程如下:
- 对于这10000个准备排序的(R,rowid),先取前三行,构造成一个堆;
- 取下一个行(R',rowid'),跟当前堆里面最大的R比较,如果R'小于R,把这个(R,rowid)从堆里去掉,换成(R',rowid');
- 重复第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值:
随机算法一:
- 取这个表的主键id的最大值M和最小值N;
- 用随机函数生成一个最大值到最小值之间的数X=(M-N)*rand() + N;
- 取不小于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中间可能有空洞,因此选择不同行的概率不一样,不是正真的随机。
随机算法二:
- 取的整个表的行数,并记为C。
- 取得Y=floor(C * rand())。floor函数在这里的作用,就是取整数部分。
- 再用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的方法。