MySQL中的随机抽取

Posted XHHP

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL中的随机抽取相关的知识,希望对你有一定的参考价值。

1. 引言

现在有一个需求是从一个单词表中每次随机选取三个单词。

这个表的建表语句和如下所示:

mysql> Create table 'words'(
    'id' int(11) not null auto_increment;
    'word' varchar(64) default null;
    primary key ('id')
) ENGINE=InnoDB;

然后我们向其中插入10000行数据。接下来我们看看如何从中随机选择3个单词。

2. 内存临时表

首先,我们通常会想到用order by rand()来实现这个逻辑:

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

虽然这句话很简单,但是执行流程则比较复杂。我们使用explain来看看语句的执行情况:

Extra字段中Using temporary表示需要使用临时表,Using filesort表示需要进行排序。也就是需要进行排序操作。

对于InnoDB表来说,执行全字段排序能够减少对于磁盘的访问,所以会被优先选择。

而对于内存表来说,回表过程只是简单地根据数据行的位置,直接访问内存得到数据,根本不会导致多访问磁盘。所以这时MySQL会优选选择rowid排序。

我们接下来再来梳理下这条语句的执行流程:

  1. 创建一个临时表,这个表使用memory引擎,表里有两个字段,第一个字段是double类型,记为R,第二个字段是varchar(64)类型,记为W。并且这个表没有索引。
  2. 从words表中,按主键顺序取出所有的word。对于每个word,调用rand()函数随机生成一个大于0小于1的随机小数,并把这个随机小数和word分别存入临时表的R和W字段中。
  3. 接下来就是按照字段R进行排序
  4. 初始化sort_buffer。sort_buffer有两个字段,一个是double类型,另一个是整型。
  5. 从内存临时表中一行行取出R值和位置信息,分别存入sort_buffer的两个字段里。
  6. sort_buffer按照R值进行排序
  7. 排序完成后,取出前三个结果的位置信息,到内存临时表中取出相应的word,返回给客户端。

流程示意图如下所示:

上面讲的位置信息,其实就是行所在的位置,也就是我们之前说的rowid。

对于InnoDB引擎来说,对于有没有主键表来说有两种处理方式:

  • 对于有主键的InnoDB表来说,这个rowid就是主键id
  • 对于没有主键的InnoDB表来说,这个rowid是由系统生成的,用来标识不同行。

因此,order by randn()使用了内存临时表,内存临时表的排序方法用的是rowid排序方法

3. 磁盘临时表

不是所有的临时表都是内存临时表。tmp_table_size这个配置限制了内存临时表的大小,如果超过了这个大小,就会使用磁盘临时表。InnoDB引擎就是默认使用磁盘临时表

4. 优先队列排序算法

在MySQL5.6之后,引入了优先队列排序算法,这种算法是不需要使用临时文件的。而原本的归并排序算法则是需要使用临时文件。

因为当你使用归并算法的时候,其实你只需要得到前3,但是你是用完归并排序,那已经整体有序了,造成了资源的浪费。

而优先队列排序算法则可以只取到前三,执行流程如下:

  1. 对于这10000个准备排序的(R,rowid),先取前三行,构造成一个堆,并且将最大的值放在堆顶;
  2. 取下一行(R’,rowid’),跟当前堆里面最大的R比较,如果R’小于R,则把(R,rowid)从堆中去掉,换成(R’,rowid’)。
  3. 不断重复上面的过程。

流程如下图所示:

但是当limit的数比较大时,维护堆比较困难,所以又会使用归并排序算法。

来源:自己整理的MySQL实战45讲笔记

MYSQL:随机抽取一条数据库记录

  今天我们要实现从随机抽取一条数据库记录的功能,并且抽取出来的数据记录不能重复

  1、首先我们看文章表中的数据:

  

  2、实现功能代码如下:

  

  1   /**
  2      * 获取随机的N篇文篇
  3      * @param int $len 文章篇数
  4      */
  5     public static function getRandom($len = 6) {
  6         # 查询数据库,得到最小Id
  7         # SELECT min(id) FROM mimi_aritcle
  8         $min = Db::name(self::$tb)->field(\'min(id)\')->select();
  9         $min = $min[0][\'min(id)\'];
 10 
 11         # 查询数据库,得到最大id
 12         # SELECT max(id) FROM mimi_article
 13         $max = Db::name(self::$tb)->field(\'max(id)\')->select();
 14         $max = $max[0][\'max(id)\'];
 15 
 16         # 初始化存储数据
 17         $result = [];
 18         # 初始化id数组
 19         $randId_arr = [];
 20 
 21         for ($i = 1; $i <= $len; $i++) {
 22             # 先设重复提取记录为false,进入do循环
 23             $is_repeat_2 = false;
 24 
 25             # do..while,即使条件不成立,循环起码执行一次
 26             do {
 27                 do {
 28                     # 产生一个随机整数 
 29                     # 注意:产生的整数与数据库无关,不要因为两个参数是从数据库中得到的,
 30                     #      就误以为是在产生数据库中的随机id
 31                     $randId    = rand($min, $max);
 32 
 33                     # 判断是否有重复提取的记录
 34                     $is_repeat = in_array($randId, $randId_arr);
 35                 } while ($is_repeat);
 36 
 37                 if($is_repeat_2){
 38                     # 将提取出的不同id进行入栈
 39                     array_push($randId_arr, $row[0][\'id\']);
 40                 }
 41 
 42                 # 抽取一条数据库记录
 43                 /**
 44                  * 首先,我们要记住:
 45                  * 步骤一表图中查询数据库,获取到最小id是1, 最大id是50,就能知道数据库中一定有id=1和id=50的记录
 46                  * 所以,
 47                  * 1、这里使用id>$randId而不是id=$randId,是防止数据库中的某条记录被删除,导致找不到数据库中对应的id记录;
 48                  * 例子:
 49                  * (1) 如果随机抽取的$randId为6,那么使用条件id>$randId-1的话,就会读取到数据库中所有id>5的记录;
 50                  *     从数据库截图中,我们可以看到,能够读取到数据库id=10、20、30、40、50的记录
 51                  * (2) 如果我们使用id=$randId,随机抽取的$randId为6,那么使用功能id=$randId的话,就会读取到数据库中id=6的固定记录,
 52                  *     但是数据库中没有id=6的记录,就会发生错误
 53                  * 
 54                  * 2、这里使用id>$randId-1而不是id>$randId,是为了防止我们随机抽取的数为最大数50的时候,条件不成立,
 55                  *    从而导致报错,因为id=50是数据库中最大数了
 56                  * 例如:
 57                  * (1) 如果随机抽取的$randId为最大数50,那么使用条件id>$randId-1的话,就会读取到数据库中所有id>49的记录,
 58                  *     由于符合id>49条件的只有id=50的记录,那么就能保证,我有数据可以提取到;
 59                  * (2) 如果随机抽取的$randId为最大数50,那么使用条件id>$randId的话,就会读取到数据库中所有id>50的记录,
 60                  *     但是数据库中没有符合条件的数据,就会发生错误
 61                  * (3) 如果随机抽取的$randId为最小数1,使用条件 id>$randId 的话,id = 1 的记录永远也不能被随机选中
 62                  * 
 63                  * 3、这里使用order(\'id ASC\')而不是order(\'id DESC\')原因:当我们获取到数据后,要获取最接近$randId的值
 64                  * (1) 如果随机抽取的$randId为6,我们就能从所有id>5的记录,即id=10、20、30、40、50
 65                  *     的记录,这时候的结果是循环6次后的总体列表,那我们需要最靠近6的记录,就需要使用order(\'id ASC\')
 66                  *     进行升序排列,这时候,列表中的第一个数据就是id=10的,就是我们想要的记录
 67                  * (2) 如果随机抽取的$randId为6,我们就能从所有id>5的记录,即id=10、20、30、40、50
 68                  *     的记录,如果使用order(\'id DESC\'),那么我们得到的数据永远是最不靠近6的记录,即id=50的记录
 69                  * (3) 不使用order,有时候会排列不一样,(通常在数据记录不同时才会发现到)
 70                  * 
 71                  * 4、这里使用limit原因:因为我们需要的是一条记录,而不是一个列表,所以,我们只需要提取符合条件的第一条记录即可
 72                  * 例子:
 73                  * (1) 如果随机抽取的$randId为6,那么使用条件id>$randId-1的话,就会读取到数据库中所有id>5的记录而不是固定一条记录;
 74                  *     从数据库截图中,我们可以看到,能够读取到数据库id=10、20、30、40、50的记录,但是我们循环一次只需要一条记录,那么
 75                  *     只需要提取第一条符合的记录,就是id=10的记录
 76                  * (2) 如果我们不使用limit的话,那么系统就会把所有符合条件的记录都提取出来
 77                  * 
 78                 */
 79                 $row = Db::name(self::$tb)
 80                     ->field(\'id, title, href\')
 81                     ->where([\'id\' => [\'>\', $randId - 1]])
 82                     ->order(\'id ASC\')
 83                     ->limit(0, 1)
 84                     ->select();     
 85                 
 86                 # 判断是否有重复提取的数据库记录
 87                 $is_repeat_2 = in_array($row[0][\'id\'], $randId_arr);
 88             } while ($is_repeat_2);
 89 
 90 
 91             # 将提取出来的数据库id进行入栈
 92             array_push($randId_arr, $row[0][\'id\']);
 93 
 94             if(empty($row)){
 95                return false;
 96             }
 97         
 98             # 将提取出的所有最终数据库数据入栈
 99             array_push($result, $row[0]);
100         }
101 
102         return $result;
103     }

 

 

  

  

 

  以上

  加油ヾ(◍°∇°◍)ノ゙

以上是关于MySQL中的随机抽取的主要内容,如果未能解决你的问题,请参考以下文章

MySQL随机抽取数据的性能问题

mysql随机抽取数据

用哪个函数可以随机抽取集合中的元素

mysql随机抽取一定数量的记录

MySQL 中随机获取数据

MySQL实现随机获取几条数据的方法