一招搞定电商首页随机排序数据算法

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 大家好前面我们了解了order by的实现方式以及内部的涉及到的知识点。今天我们介绍一下关于实战的知识点。主要应用于表数据比较多的情况下,如何巧妙地从中取出几条数据。

案例


mysql> CREATE TABLE `words` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `word` varchar(64) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB;
delimiter ;;
create procedure isdata()
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;;
delimiter ;
call isdata();

这是一个单词表,我们为了便于观察,插入了10000条数据。

现在我们要做的需求就是从10000条数据中查询随机取出3条数据即可。


内存临时表


我相信很多人第一选择就是采用内存临时表,所谓的内存操作! 代码如下

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

这个语句的意思很直白,随机排序取前 3 个。虽然这个 SQL 语句写法很简单,但执行流程却有点复杂的。我们先看一下执行计划吧

image.png

从图中得知Extra 字段显示 Using temporary,表示的是需要使用临时表;Using filesort,表示的是需要执行排序操作。

因此这个 Extra 的意思就是,需要临时表,并且需要在临时表上排序。

当前内容是接着上篇文件介绍order by排序继续扩展延伸的。上述不是说需要执行排序嘛,那问题来了。你是采用什么算法进行排序呢?

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

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

大概的逻辑就是这样的。下面我们根据上述SQL分析一下执行流程。看看通过什么地方可以优化一下。

  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。

image.png

以上是SQL执行的流程图。

紧接上文你可以会有疑惑,为什么上文是根据ID,这篇的pos是啥玩意啊。我们先介绍一下这里的pos是什么字段?

这里的pos也就是rowid。这个rowid就是平时我们会新建表的时候建立一个主键索引,如果那个表没有主键的话那是不是就群龙无首了啊,是不是就不能排序了啊。答案显然是不对的。如果没有定义的主键。MySQL会自动生成一个主键,这个主键就是rowid。

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


磁盘临时表


那么,是不是所有的临时表都是内存表呢? 并不是


tmp_table_size

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

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

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

为了复现这个过程,我把 tmp_table_size 设置成 1024,把 sort_buffer_size 设置成 32768, 把 max_length_for_sort_data 设置成 16。

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

image.png

因为将 max_length_for_sort_data(控制要排序的参数) 设置成 16,小于 word 字段的长度定义,所以我们看到 sort_mode 里面显示的是 rowid 排序,这个是符合预期的,参与排序的是随机值 R 字段和 rowid 字段组成的行。

这时候你可能心算了一下,发现不对。R 字段存放的随机值就 8 个字节,rowid 是 6 个字节(至于为什么是 6 字节,就留给你课后思考吧),数据总行数是 10000,这样算出来就有 140000 字节,超过了 sort_buffer_size 定义的 32768 字节了。但是,number_of_tmp_files 的值居然是 0,难道不需要用临时文件吗?

这个 SQL 语句的排序确实没有用到临时文件,采用是 MySQL 5.6 版本引入的一个新的排序算法,即:优先队列排序算法。接下来,我们就看看为什么没有使用临时文件的算法,也就是归并排序算法,而是采用了优先队列排序算法。

其实,我们现在的 SQL 语句,只需要取 R 值最小的 3 个 rowid。但是,如果使用归并排序算法的话,虽然最终也能得到前 3 个值,但是这个算法结束后,已经将 10000 行数据都排好序了。

那么问题来了,我们要3行,为啥给我们排10000行呢? 也就是说有没有更优的处理方法

优先队列排序算法:

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

image.png

模拟 6 个 (R,rowid) 行,通过优先队列排序找到最小的三个 R 值的行的过程。整个排序过程中,为了最快地拿到当前堆的最大值,总是保持最大值在堆顶,因此这是一个最大堆。

OPTIMIZER_TRACE 结果中,filesort_priority_queue_optimization 这个部分的 chosen=true,就表示使用了优先队列排序算法,这个过程不需要临时文件,因此对应的 number_of_tmp_files 是 0。

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

如下SQL

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

你可能会问,这里也用到了 limit,为什么没用优先队列排序算法呢?原因是,这条 SQL 语句是 limit 1000,如果使用优先队列算法的话,需要维护的堆的大小就是 1000 行的 (name,rowid),超过了我设置的 sort_buffer_size 大小,所以只能使用归并排序算法。

总之,不论是使用哪种类型的临时表,order by rand() 这种写法都会让计算过程非常复杂,需要大量的扫描行数,因此排序过程的资源消耗也会很大。

再回到我们文章开头的问题,怎么正确地随机排序呢?


随机排序方法


我们先把问题简化一下,如果只随机选择 1 个 word 值,可以怎么做呢?思路上是这样的:

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

我们把这个算法,暂时称作随机算法 1

这个方法效率很高,因为取 max(id) 和 min(id) 都是不需要扫描索引的,而第三步的 select 也可以用索引快速定位,可以认为就只扫描了 3 行。但实际上,这个算法本身并不严格满足题目的随机要求,因为 ID 中间可能有空洞,因此选择不同行的概率不一样,不是真正的随机。

比如你有 4 个 id,分别是 1、2、4、5,如果按照上面的方法,那么取到 id=4 的这一行的概率是取得其他行概率的两倍。

如果这四行的 id 分别是 1、2、40000、40001 呢?这个算法基本就能当 bug 来看待了。所以,为了得到严格随机的结果,你可以用下面这个流程:

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

我们把这个算法,称为随机算法 2。

由于 limit 后面的参数不能直接跟变量,所以我在上面的代码中使用了 prepare+execute 的方法。你也可以把拼接 SQL 语句的方法写在应用程序中,会更简单些。这个随机算法 2,解决了算法 1 里面明显的概率不均匀问题。

MySQL 处理 limit Y,1 的做法就是按顺序一个一个地读出来,丢掉前 Y 个,然后把下一个记录作为返回结果,因此这一步需要扫描 Y+1 行。再加上,第一步扫描的 C 行,总共需要扫描 C+Y+1 行,执行代价比随机算法 1 的代价要高。

当然,随机算法 2 跟直接 order by rand() 比起来,执行代价还是小很多的。

现在,我们再看看,如果我们按照随机算法 2 的思路,要随机取 3 个 word 值呢?你可以这么做:

  1. 取得整个表的行数,记为 C;
  2. 根据相同的随机方法得到 Y1、Y2、Y3;
  3. 再执行三个 limit Y, 1 语句得到三行数据。

我们把这个算法,称作随机算法 3。


总结


今天这篇文章,我是借着随机排序的需求,介绍了 MySQL 对临时表排序的执行过程。

如果你直接使用 order by rand(),这个语句需要 Using temporary 和 Using filesort,查询的执行代价往往是比较大的。所以,在设计的时候你要尽量避开这种写法。今天的例子里面,我们不是仅仅在数据库内部解决问题,还会让应用代码配合拼接 SQL 语句。在实际应用的过程中,比较规范的用法就是:尽量将业务逻辑写在业务代码中,让数据库只做“读写数据”的事情。因此,这类方法的应用还是比较广泛的。


相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
8天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
49 8
|
8天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
36 7
|
15天前
|
存储 编解码 负载均衡
数据分片算法
【10月更文挑战第25天】不同的数据分片算法适用于不同的应用场景和数据特点,在实际应用中,需要根据具体的业务需求、数据分布情况、系统性能要求等因素综合考虑,选择合适的数据分片算法,以实现数据的高效存储、查询和处理。
|
15天前
|
存储 缓存 算法
分布式缓存有哪些常用的数据分片算法?
【10月更文挑战第25天】在实际应用中,需要根据具体的业务需求、数据特征以及系统的可扩展性要求等因素综合考虑,选择合适的数据分片算法,以实现分布式缓存的高效运行和数据的合理分布。
|
28天前
|
机器学习/深度学习 人工智能 算法
"拥抱AI规模化浪潮:从数据到算法,解锁未来无限可能,你准备好迎接这场技术革命了吗?"
【10月更文挑战第14天】本文探讨了AI规模化的重要性和挑战,涵盖数据、算法、算力和应用场景等方面。通过使用Python和TensorFlow的示例代码,展示了如何训练并应用一个基本的AI模型进行图像分类,强调了AI规模化在各行业的广泛应用前景。
29 5
|
1月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
23 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
20天前
|
存储 JSON 算法
TDengine 检测数据最佳压缩算法工具,助你一键找出最优压缩方案
在使用 TDengine 存储时序数据时,压缩数据以节省磁盘空间是至关重要的。TDengine 支持用户根据自身数据特性灵活指定压缩算法,从而实现更高效的存储。然而,如何选择最合适的压缩算法,才能最大限度地降低存储开销?为了解决这一问题,我们特别推出了一个实用工具,帮助用户快速判断并选择最适合其数据特征的压缩算法。
28 0
|
30天前
|
人工智能 算法 前端开发
无界批发零售定义及无界AI算法,打破传统壁垒,累积数据流量
“无界批发与零售”是一种结合了批发与零售的商业模式,通过后端逻辑、数据库设计和前端用户界面实现。该模式支持用户注册、登录、商品管理、订单处理、批发与零售功能,并根据用户行为计算信用等级,确保交易安全与高效。
|
30天前
|
前端开发 算法 JavaScript
无界SaaS模式深度解析:算力算法、链接力、数据确权制度
私域电商的无界SaaS模式涉及后端开发、前端开发、数据库设计、API接口、区块链技术、支付和身份验证系统等多个技术领域。本文通过简化框架和示例代码,指导如何将核心功能转化为技术实现,涵盖用户管理、企业店铺管理、数据流量管理等关键环节。
|
1月前
|
机器学习/深度学习 算法 数据处理
EM算法对人脸数据降维(机器学习作业06)
本文介绍了使用EM算法对人脸数据进行降维的机器学习作业。首先通过加载ORL人脸数据库,然后分别应用SVD_PCA、MLE_PCA及EM_PCA三种方法实现数据降维,并输出降维后的数据形状。此作业展示了不同PCA变种在人脸数据处理中的应用效果。
32 0