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

英语学习 App性能问题。另一种排序需求。首页随机显示三个单词(根据用户级别)。单词表变大,选单词越来越慢

简化:去掉用户级别对应单词表,随机选。

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))));

插入了 10000 行记录。

一、内存临时表

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

随机排序取前 3 个。执行流程复杂。

图 1 使用 explain 命令查看语句的执行情况  

Using temporary需临时表;Using filesort需要执行排序。Extra :在临时表上排序

图 2 全字段排序
图 3 rowid 排序

对于 InnoDB 表,全字段排序会减少磁盘访问,优先。

内存表,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。优化器没有这顾虑,、排序行越小越好,选择 rowid 排序。执行流程:

1.  创建临时表。memory 引擎,字段R(double),W varchar(64) 。没有索引。

2.  按主键顺序取出所有word。调用 rand() 生成0 到1 随机数存入临时表 R 和 W 字段中,扫描行数10000。

3.  没有索引内存临时表上,按 R 排序

4.  初始化 sort_buffer

5.  一行行地取出 R 值和位置信息,存入 sort_buffer 。对内存临时表做全表扫描,扫描行数 10000变20000。

6.  sort_buffer 中R 排序。没有涉及到表,不会增加扫描行数。

7.  排序完成后,取前三位置信息,到临时表中取出 word 值返回给客户端。访问了表三行数据,总扫描行数变20003。

慢查询日志(slow log)验证

# Query_time:  0.900376  Lock_time: 0.000347  Rows_sent: 3 Rows_examined: 20003

SET  timestamp=1541402277;  select word from  words order by rand() limit 3;

图 4 随机排序完整流程图 1

pos位置信息:定位一行数据。rowid:每个引擎用来唯一标识数据行的信息。

有主键rowid 就是主键ID;没有rowid 系统生成(6 字节);

MEMORY 引擎不是索引组织表。 rowid 其实就是数组的下标。

order by rand()用内存临时表(rowid 排序)

二、磁盘临时表

tmp_table_size默认16M限制内存临时表大小,超过tmp_table_size,内存临时表转成磁盘临时表(默认InnoDB,internal_tmp_disk_storage_engine控制)。

磁盘临时表时,没显式索引InnoDB 排序。为了复现

set  tmp_table_size=1024;

set  sort_buffer_size=32768;

set  max_length_for_sort_data=16;

/* 打开 optimizer_trace,只对本线程有效*/SET  optimizer_trace='enabled=on';

/* 执行语句*/   select word from  words order by rand() limit 3;

/* 查看 OPTIMIZER_TRACE 输出*/   SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G

图 5 OPTIMIZER_TRACE 部分结果

max_length_for_sort_data  16,小于 word 字段长度定义 rowid 排序,符合预期,参与排序的是随机值 R 字段和 rowid 字段组成的行。

R  8 字节,rowid 6 字节,总行数 10000,算出140000 字节,sort_buffer_size的 32768 字节了。number_of_tmp_files  0,没用临时文件,filesort_priority_queue_optimization  chosen=true,表示优先队列排序

取 R 值最小3 个 rowid。归并排10000 行数据。浪费计算量。优先队列算法:

1.  10000  (R,rowid),取前三行,构造堆;

2.  取下个行 (R’,rowid’),跟当前堆最大的 R 比较,R’小于 R, (R,rowid) 换成 (R’,rowid’);

3.  重复第 2 步,10000 个。

图 6 优先队列排序算法示例  

如果limit 1000,超过sort_buffer_size 大小,用归并排序算法。

不论哪种临时表,order by rand() 计算复杂,要大量扫描行数,资源消耗大。

三、随机排序方法

随机算法 1,随机选择 1 个 word 值:

1.  取得主键 id 的最大值 M 和最小值N;

2.  随机函数生成(最大到最小值之间数)X =(M-N)*rand() + N

3.  取不小于 X 第一个 ID 行

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

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

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

效率高,max(id) 和 min(id) 不需扫描索引,select 索引快速定位,只扫描了 3 行。不严格满足题目的随机要求,ID 中可能有空洞,不是真正随机

id是 1、2、4、5,取到 id=4 概率是其他两倍。

id 是 1、2、40000、40001,基本 bug 

随机算法 2,严格随机结果流程:

1.  取整表行数,记为 C。

2.  取得 Y = floor(C * rand())。 floor 取整数部分

3.  limit Y,1 取一行。

mysql> select  count(*) into @C from t;

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

set @sql =  concat("select * from t limit ", @Y, ",1");

prepare stmt from  @sql;

execute stmt;

DEALLOCATE  prepare stmt;

解决1概率不均匀问题,limit 后参数不能直接跟变量,用prepare+execute 方法。把拼接 SQL 写程序中,更简单。

处理 limit Y,1丢掉前 Y 个,扫描 Y+1 行。加上 C 行,总C+Y+1 行,代价高。

跟直接 order by rand() 比代价小。

C=10000,随机大Y 值,跟 20000 差不多,接近 order by rand()?

随机算法 3,2 思路,随机取 3 个:

1.  整表行,记为 C;

2.  随机方法得 Y1、Y2、Y3;

3.  执行三个 limit Y, 1 得三行

小结

借随机排序需求,MySQL 对临时表排序执行过程。

直接用 order by rand(),需Using temporary 和 Using filesort,查询代价大。设计时避开。

今天例子,不仅数据库内部解决问题,还配合拼接 SQL 语句。实际规范用法:业务逻辑写业务代码中,数据库只“读写”。

思考题

随机算法 3 ,总扫描行数:C+(Y1+1)+(Y2+1)+(Y3+1),怎么减少扫描行?

(1)Y1、Y2 和 Y3 最大为M,最小为 N,执行:mysql> select  * from t limit N, M-N+1;

总 C+M+1 行。

(2)先取 id 值,确定以后,再执行三次 where id=X 

(3)装jdk或redis缓存:量不多,随机访问数组好。单词10字节,10*10000,1M装下。

(4)表里存无空洞自增值,用数据库,方案1好,单词库不变,把空洞处理调。原在A表,新建B表 ,insert into B(word) select word from A. B的id自增,生成连续主键。量大禁用,RR隔离级别会锁A表 

(5)rowid 方法:增加主键字段,记录每行rowid,连续一万随机,回表查行记录

(6)id 迭代往下,主键索引有序性按Y排序,第一取完,拿到对应id,where id大于xxx,limit y2-y1,1

你可能感兴趣的:(17 | 如何正确地显示随机消息?)