分布式任务调度内的 MySQL 分页查询优化

作者:vivo 互联网数据库团队- Qiu Xinbo

本文主要通过图示介绍了用主键进行分片查询的过程,介绍了主键分页查询存在SQL性能问题,如何去创建高效的索引去优化主键分页查询的SQL性能问题。

对于数据分布不均如何发现,提供了一些SQL查询案例来进行参考,对MySQL Index Condition Pushdown优化算法做了一些简单介绍。

一、背景介绍

最近在线上环境发现了一条执行较慢的分页查询,高并发执行,产生了大量的慢查询日志,CPU使用率逐步升高。

通过观察它的执行时间,发现该SQL查询时快时慢,执行时间并不稳定,以至于在高并发执行场景时,数据库来不及响应,数据库服务变慢。

图片

图片

二、分析定位

2.1 定位 SQL 执行变慢的原因

通过数据库管理平台查看SQL执行信息发现,SQL解析行数(扫描行数)和SQL执行时间都很不稳定,执行时长和解析行数(扫描行数)是成正比的。

这个也能解释的通为什么SQL执行时长变了,因为扫描行数变多了,SQL执行时间成比例增长。

-- SQL全文
selectid,uuid,name,user_type,is_deleted,modify_date
fromtest_user
whereis_deleted=0    and user_type=0    and id > 10000    and id % 10 = 9
order byid  limit 500;

图片

2.2 了解 SQL 的业务背景

通过与研发沟通发现,该SQL原来是串行执行,单个线程在跑,后来觉得比较慢,改为分布式任务并行执行,通过id取模0-9,调度10个线程,每个线程处理1个分区,这样就有10个并发相当于把数据做了切片,并发查询并发处理,由此带来数据库端的并发升高。从技术角度上看,提高数据处理速度,给数据做切片,改单线程为并发处理,并没有任何问题,反而是一种比较好的优化方案,但是高并发执行的SQL都是要有一个前提,SQL执行效率要特别高,否则会导致数据库端物理机资源耗尽,数据库服务来不及响应。

图片

2.3 定位 SQL 扫描行数变化的原因

2.3.1 慢 SQL 及表结构信息

-- 为了方便理解和说明,新建一个test_user表,造了一些模拟数据,将SQL做了一些简化,不影响整体的分析效果-- SQL全文
selectid,uuid,name,user_type,is_deleted,modify_date
fromtest_user 
whereis_deleted=0     and user_type=0     and id > 10000     and id % 10 = 9 
order byid  limit 500;-- 表信息CREATE TABLE `test_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`uuid` varchar(64) NOT NULL COMMENT '用户ID',`name` varchar(20) DEFAULT '' COMMENT '用户名',`user_type` tinyint(4) NOT NULL DEFAULT '0',`is_deleted` tinyint(4) NOT NULL DEFAULT '0',`modify_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',`create_date` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (`id`),UNIQUE KEY `uniq_uuid` (`uuid`),KEY `idx_modifydate` (`modify_date`)
) ENGINE=InnoDB AUTO_INCREMENT=7986024 DEFAULT CHARSET=utf8mb4

2.3.2 查看 SQL 执行计划

通过查看SQL执行计划,发现执行计划走主键索引扫描,以下是SQL执行计划的关键信息解读:

  • type=range     范围扫描

  • key = primary 使用主键索引

  • rows = 877w   预估的扫描行数

  • filter = 1.00     百分比,满足过滤条件返回的行数  = rows * filter 

mysql> explain  select->     id,->     uuid,->     name,->     user_type,->     is_deleted,->     modify_date-> from->     test_user -> where->     is_deleted=0     ->     and user_type=9     ->     and id > 10000     ->     and id % 10 = 9 -> order by->     id  limit 500; 
+----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table     | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | SIMPLE      | test_user | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL | 8775507 |     1.00 | Using where |
+----+-------------+-----------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

2.3.3 图示 SQL 执行过程

通过简单的图示,描述下SQL扫描过程,由于是通过主键索引遍历,避免了额外的排序行为,从最小id开始取到最大id。

mysql> select min(id),max(id) from test_user;
+---------+----------+
| min(id) | max(id)  |
+---------+----------+
|       3 | 17889149 |
+---------+----------+
1 row in set (0.00 sec)

图片

2.3.4 计算数据分布

从SQL过滤条件看只有is_deleted、user_type、id这三个,能预估到is_deleted和user_type区分度不高,通过SQL查看下数据的分布。

mysql> select is_deleted,user_type,count(*) from test_user group by is_deleted,user_type order by count(*) desc limit 1,10;
+------------+-----------+----------+
| is_deleted | user_type | count(*) |
+------------+-----------+----------+
|          1 |         1 |  4473019 |
|          1 |         0 |  4471648 |
|          0 |         0 |  4470140 |
|          0 |         2 |      999 |
+------------+-----------+----------+
4 rows in set (4.81 sec)
-- 从数据分布来看user_type等于2的数据较少,只有999条,其他相对比较均匀

数据分布验证测试

将上述4种结果(is_deleted和user_type)分别通过SQL查看最近1000条满足条件的数据的id区间,验证数据的分布。

  • is_deleted=1、user_type=1

  • is_deleted=1、user_type=0

  • is_deleted=0、user_type=0

-- 最近1000条is_deleted=1、user_type=1的数据记录分布在id 6-3876,大约扫描3871条数据,能返回500条满足条件的值,数据分布均匀.
mysql> select max(id),min(id) from( select id from test_user where  is_deleted=1 and user_type=1 order by id  limit 1000) a;
+---------+---------+
| max(id) | min(id) |
+---------+---------+
|    3876 |       6 |
+---------+---------+
1 row in set (0.00 sec)-- 最近1000条is_deleted=1、user_type=0的数据记录分布在id 3-4019,大约扫描4016条数据,能返回500条满足条件的值,数据分布均匀.
mysql> select max(id),min(id) from( select id from test_user where  is_deleted=1 and user_type=0 order by id  limit 1000) a;
+---------+---------+
| max(id) | min(id) |
+---------+---------+
|    4019 |       3 |
+---------+---------+
1 row in set (0.00 sec)-- 最近1000条is_deleted=0、user_type=0的数据记录分布在id 5-4020,大约扫描4015条数据,能返回500条满足条件的值,数据分布均匀.
mysql> select max(id),min(id) from( select id from test_user where  is_deleted=0 and user_type=0 order by id  limit 1000) a;
+---------+---------+
| max(id) | min(id) |
+---------+---------+
|    4025 |       5 |
+---------+---------+
1 row in set (0.00 sec)

图片

is_deleted=0、user_type=2

-- 最近1000条is_deleted=0、user_type=2的数据记录分布在id 17890648-17891147,是比较紧凑的,但是由于id比较大,整体排在较后的位置。
-- 如果按照主键遍历,需要遍历完前面的1700w条不符合条件数据,才能遍历到满足条件的数据。
mysql> select max(id),min(id) from( select id from test_user where  is_deleted=0 and user_type=2 order by id  limit 1000) a;
+----------+----------+
| max(id)  | min(id)  |
+----------+----------+
| 17891147 | 17890149 |
+----------+----------+
1 row in set (0.00 sec)

图片

2.3.5 实际执行测试

重要字段信息说明:

  • Query_time:SQL执行时间

  • Rows_examined:SQL扫描行数

  • Rows_sent:SQL返回行数

# Query_time: 0.012232  Lock_time: 0.000076 Rows_sent: 500  Rows_examined: 19507SET timestamp=1695711685;select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=1 and user_type=1 and id > 0 and id % 10 = 9 order by id  limit 500;

# Query_time: 0.009549  Lock_time: 0.000074 Rows_sent: 500  Rows_examined: 20537SET timestamp=1695711745;select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=1 and user_type=0 and id > 0 and id % 10 = 9 order by id limit 500;

# Query_time: 0.009835  Lock_time: 0.000081 Rows_sent: 500  Rows_examined: 21037SET timestamp=1695711779;select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=0 and id > 0 and id % 10 = 9 order by id limit 500;

(这边大家可能会有疑惑,为什么扫描行数要比预估的多一些,其实也正常,我们在做预估时并没有把取模的过滤条件加上,所以必然会多扫描)

# Query_time: 6.981938  Lock_time: 0.000076 Rows_sent: 100  Rows_examined: 17890145SET timestamp=1695711818;select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=2 and id > 0 and id % 10 = 9 order by id limit 500;

2.3.6 自此能得到结论

因为is_deleted和user_type数据分布不均匀并且数据区分度不高,执行计划走主键顺序扫描, 在查询is_deleted=0 and user_type=2 特定场景的时,因为走主键索引顺序遍历,满足user_type=2 的id比较靠后,需要先扫描完成前面1700w条数据后,才能找到满足user_type=2的数据,SQL扫描行数变多, SQL执行时间变长。

三、优化方案

3.1 优化方案确定

当前SQL执行计划以主键进行顺序遍历,是一个范围扫描,有点像在一片很大的居民区按照序号挨家挨户寻找一些特定的人一样,比较简单也比较低效。

既然查询是以is_deleteduser_type为主要的过滤条件,查询特定的人群信息,可以考虑直接在这两列上添加索引,记录特定人群信息的位置,根据位置直接去定向寻找。

虽然is_deleteduser_type字段区分度很低,但是成为有序结构,能避免这条SQL大量的读取不符合条件的数据的行为,添加索引的收益远大于索引带来负面影响。

最终的添加的索引:

alter table test_user add index idx_isdeleted_usertype_id(is_deleted,user_type,id);

添加该索引的考虑:遵循ESR原则(等值在前,排序在中间,范围在最后),既能高效扫描到对应的数据,还能避免id的排序,extra内显示使用了Using index condition。

mysql>  explain select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=2 and id > 0 and id % 10 = 9 order by id limit 500;
+----+-------------+-----------+------------+-------+-----------------------------------+---------------------------+---------+------+------+----------+-----------------------+
| id | select_type | table     | partitions | type  | possible_keys                     | key                       | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-----------+------------+-------+-----------------------------------+---------------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | test_user | NULL       | range | PRIMARY,idx_isdeleted_usertype_id | idx_isdeleted_usertype_id | 10      | NULL |  999 |   100.00 | Using index condition |
+----+-------------+-----------+------------+-------+-----------------------------------+---------------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)  

3.2 优化效果对比

优化前

# Query_time: 6.981938  Lock_time: 0.000076 Rows_sent: 100  Rows_examined: 17890145
SET timestamp=1695711818;
select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=2 and id > 0 and id % 10 = 9 order by id limit 500;

优化后

# Query_time: 0.000884  Lock_time: 0.000091 Rows_sent: 100  Rows_examined: 100
SET timestamp=1695714485;
select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=2 and id > 0 and id % 10 = 9 order by id limit 500;

优化提升

扫描行数从1700w条降低为100条,查询时间从6.98s 降低为 0.8ms

3.3  图示的优化后的SQL执行过程

  1. 通过idx_isdeleted_usertype_id索引的有序性,进行二分查找,快速定位到满足is_deleted和user_type、id条件主键信息。

  2. 通过主键信息回表读取完整的数据。

  3. 返回数据给客户端服务。

图片

3.4 ICP特性(Index Condition Pushdown) 

补充下执行计划内extra列体现Using index condition优化。

  • 索引条件下推 (ICP) 是针对 MySQL 使用索引从表中检索行的情况的优化。

  • 如果没有 ICP,存储引擎会遍历索引以定位基表中的行,并将它们返回给 MySQL  server,由 MySQL  server评估行的 WHERE 条件。

  • 在启用 ICP 的情况下,如果 WHERE 条件的一部分可以通过仅使用索引中的列来评估,MySQL  server会将这部分 WHERE 条件下推到存储引擎。

  • 然后存储引擎通过使用索引条目来评估推送的索引条件,并且只有在满足这一条件时才从表中读取行。

  • ICP可以减少存储引擎必须访问基表的次数和MySQL server必须访问存储引擎的次数。

图片

ICP优化的使用和局限性路

ICP优化在数据库优化器内默认是开启的,ICP优化适用性取决于以下条件:

  • icp 对于使用rang、ref、eq_ref 和ref_or_null访问模式去检索全表数据行时候。

  • icp 只适用于innodb、myisam引擎的表,包括分区的InnoDB和MyISAM表。

  • icp只会使用二级索引,减少完整行记录的读取和减少I/O操作 对于聚集索引,完整行记录已经被读入innodb buffer中,using icp不能减少I/O操作。

  • icp不支持使用创建在虚拟列上的二级索引,innodb引擎支持在虚拟列上创建二级索引。

  • 引用子查询的条件无法下推。

  • 引用存储函数的条件无法下推。存储引擎无法调用存储的函数。

  • Triggered conditions cannot be pushed down。

-- 测试下相同的SQL执行在开启ICP优化和关闭ICP优化,执行时间和扫描行数的对比.-- 关闭ICP,SQL执行扫描行数是5043行,执行时间为8.03ms.
SET optimizer_switch='index_condition_pushdown=off';
# Query_time: 0.008031  Lock_time: 0.000085 Rows_sent: 500  Rows_examined: 5043
select id,uuid,name,user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=0  and id > 10000 and id % 10 = 9  order by id limit 500;-- 开启ICP,SQL执行扫描行数仅为500行,执行时间为2.72ms.
SET optimizer_switch='index_condition_pushdown=on';
# Query_time: 0.002724  Lock_time: 0.000082 Rows_sent: 500  Rows_examined: 500
select id,uuid, name, user_type,is_deleted,modify_date from test_user where is_deleted=0 and user_type=0 and id > 10000 and id % 10 = 9 order by id limit 500;

结论:本次测试,开启ICP优化,SQL执行时扫描的行数仅为未开启时的1/10,执行时间提升约2-3倍。

四、总结

  1. 将SQL查询从串行改为高并发执行,需要评估下SQL查询效率是否足够高,评估的标准:SQL扫描行数/SQL返回行数  结果越大说明存在很多低效的数据扫描,执行效率不高。

  2. 分页查询通过主键遍历是顺序遍历,从最小id到最大id,当存在其它过滤条件时,需要再次判断数据是否满足这些过滤条件,扫描的行数会随着增长。

  3. 区分度较低的字段并非不适合创建索引,仔细评估查询的场景,建立特定的组合索引,触发MySQL icp优化,对查询性能会有很大提升。

参考文章

Index Condition Pushdown介绍:

  • Index Condition Pushdown

  • Index Condition Pushdown Optimization

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/13994.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C语言初阶——5操作符

一、算数操作符 除了% 操作符之外,其他的几个操作符可以作用于整数和浮点数。对于/ 操作符如果两个操作数都为整数,执行整数除法。而只要有浮点数执行的就是浮点数除 法。% 操作符的两个操作数必须为整数。返回的是整除之后的余数。 1、类型转换 C语言…

Simplicity Studui V5 新安装后无法Product Updates

之前(2021年)在SiliconLabs官网下载了SSV5,安装包我也保存在硬盘了,最近换了台电脑安装SSV5后安装 SDK之前必须Product Updates,但死活安装不上,老是提示发生了错误。来来回回卸载安装几十遍,后…

每日新闻掌握【2024年5月24日 星期五】

2024年5月24日 星期五 农历四月十七 TOP大新闻 卫龙因缺斤少两致歉并开展自查 5月23日,卫龙官方账号发布致消费者的一封信。信中提到:针对近日消费者反馈的15g魔芋爽存在克重不足的情况我们表示诚挚的歉意。产品质量是卫龙的重中之重。卫龙已经与相关消…

瓦解信息茧房,IPWO打破“墙”的限制

国外与国内的网络之间隔着一道无形的“墙”,这面“墙”让我们避免了海外不法分子的窥视,保护了我们的网络隐私。但是,“墙”的存在同样阻止了我们访问全球网络,获取海外资源,形成巨大的信息茧房。 越来越多的人渴望撕开…

Plant Simulation 双深位立库开发系列教程-出入口参数化

上一节讲到货架参数化,这一节我们继续讲出入口参数化,先看我们需要达到的效果,在Conveyor增加一个出入口设置功能,用户可以通过该功能设置多个出入口 步骤1:创建ConveyorList 在RackLane中新建一个表格ConveyorList,用于存储用户的设置参数 表格设置名字、类型、方向 、…

c# 基础 .net core、.net framework、c#、mono之间的关系

它们都是微软旗下的产品.NET Framework 主要针对window环境开发,是一套开发框架,它包含了大量的类库和运行时环境(CLR),支持多种编程语言,包括 C#。- .NET Framework 的版本(如 4.5、4.6、4.7 …

Docker配置国内镜像源

添加Docker国内镜像源 在/etc/docker/daemon.json文件中添加以下内容: {"registry-mirrors": ["http://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn","https://registry.docker-cn.com"] }重启docker s…

Java enum 枚举类

Java Enum 枚举类 概述 枚举类型本质上也是一种类,只不过是这个类的对象是有限的、固定的几个,不能让用户随意创建。 如果针对于某个类,其实例是确定个数,则推荐将此类声明为枚举类。 如果枚举类的实例只有一个,则可…

paligemma、Grounding-DINO-1.5简单无需标注无需训练直接可以使用的VLM图像到文本模型

1、paligemma 参考:https://github.com/google-research/big_vision/blob/main/big_vision/configs/proj/paligemma/README.md 模型架构: 文本与图像特征一起送入大模型 在线体验网址: https://huggingface.co/spaces/big-vision/paligemma 通过文字prompt既可与图片对话…

ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1+解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

HCIP-Datacom-ARST自选题库__MPLS判断【道题】

1.在MPLS网络中,每台设备依然要遵循最长匹配原则,即每台设备都要有到达目的IP地址的路由,否则网络设备会丢弃收到的MPLS报文。 2.在MPLS网络中,运行DP协议的SR之间通过交换LDP消息来实现邻居发现、会活建立与维护以及标签管理等功…

Java 9的模块化系统(JPMS):探讨Java 9引入的模块化系统,并解释其对Java生态的影响

Java 9 模块化系统(JPMS)简介 Java 9 模块系统,也被称为 Java 平台模块系统 (JPMS),它是 Java 9 的核心特性之一,用于改进 Java 的大型应用的封装性和可维护性。 JPMS 的主要功能如下: 模块化代码:JPMS 允许你将代码库划分为不同的模块,在没有显示声明的情况下,模…

QT creator centralwidget前面有个禁止符号

centralwidget前面有个禁止符号:表示分拆布局 在主窗口空白处,右键,选择布局即可选择不同的布局方式,表示对窗口内所有控件部件进行布局,不如垂直布局。

AI商业化之路:开源大模型VS闭源大模型

开源大模型与闭源大模型,你更看好哪一方? 简介:评价一个AI模型“好不好”“有没有发展”,首先就躲不掉“开源”和“闭源”两条发展路径。对于这两条路径,你更看好哪一种呢? 探讨开源大模型和闭源大模型在商…

【机器学习】—机器学习和NLP预训练模型探索之旅

目录 一.预训练模型的基本概念 1.BERT模型 2 .GPT模型 二、预训练模型的应用 1.文本分类 使用BERT进行文本分类 2. 问答系统 使用BERT进行问答 三、预训练模型的优化 1.模型压缩 1.1 剪枝 权重剪枝 2.模型量化 2.1 定点量化 使用PyTorch进行定点量化 3. 知识蒸馏…

线性回归 10 种图表 上【建议收藏】

这段时间,不少同学提到了一些图表的问题。 每次在使用matplotlib画图,运用这些图表说明问题的时候,很多时候是模糊的,比如说什么时候画什么图合适? 其实这个根据你自己的需求,自己的想法来就行。 今天的…

不是人人都懂的学习要点

不是人人都懂的学习要点_什么叫独特?别人都学的东西,人人都懂的东西咱就不学了,学别人知道的少的东西-CSDN博客

高光谱成像技术简介,怎么选择成像方案?

目录 一、什么是光谱?二、光谱和光谱分析方法的类型三、多光谱和高光谱的区别四、高光谱在水果品质检测中的应用1. 高光谱成像系统2. 高光谱图像的获取方式3. 高光谱图像处理与分析4. 在水果品质检测中的应用总结 五、针对自己的应用场景怎么使用高光谱技术六、参考…

海山数据库(He3DB)从方法到实践,构建以场景为中心的体验管理体系

编者按:体验优化的过程中设计师经常会遇到几个阶段,发现问题、定义问题、优化问题、查看反馈,但在产品快速迭代的过程中,体验的问题经常被归类到“不紧急”需求中,并逐步转为长尾问题,这些不被重视的问题聚…

重组蛋白表达中的质粒构建流程-卡梅德生物

我们在进行重组蛋白表达服务时,首先要根据序列特点合成基因并构建到合适的载体上。重组蛋白表达中的质粒构建是一个复杂但关键的过程,它涉及到将目标基因(即编码所需蛋白质的基因)插入到适当的质粒载体中,以便在后续的…