|
本文分享自华为云社区《【华为云MySQL技术专栏】GaussDB(for MySQL)与MySQL的COUNT查询并行优化策略》,作者:GaussDB 数据库。
1.背景介绍
统计表的行数(COUNT)是客户应用和DBA运维常用的操作。MySQL虽是业界广泛使用的OLTP数据库,但大表执行COUNT操作非常耗时,原因在于:
(1) COUNT操作需要遍历表的全量数据来获取精确的行数,当表数据量较大或部分数据不在Buffer Pool时,查询操作很耗时。
(2) MySQL 8.0.14之前的版本无并行查询技术,只能串行执行SQL语句,无法利用多核技术进行加速。
(3) MySQL 8.0.14及后续版本InnoDB存储引擎支持并行扫描主键,但不支持并行扫描二级索引,在主键很大、二级索引较小的场景下,相比老版本(MySQL 5.7)串行扫描二级索引,社区版本并行扫描可能出现性能劣化,并且不支持关闭并行扫描主键特性。
GaussDB(for MySQL)通过自研并行查询(PQ)和计算下推(NDP)特性,解决了大表COUNT慢的问题,典型场景下,相比MySQL并行扫描主键性能可提升超过80倍。
2. MySQL COUNT并行介绍
MySQL8.0.14版本InnoDB存储引擎支持并行扫描主键,这样可以利用并行的能力对COUNT操作进行加速,特性说明参见图1。
图1 MySQL 8.0 InnoDB存储引擎并行扫描主键特性
2.1原理介绍
MySQL COUNT并行在InnoDB存储引擎层实现的框架图参见图2。优化器决策走COUNT并行后,生成COUNT并行算子“UnqualifiedCountIterator”, 调用handler API接口“handler::ha_records”,InnoDB层在函数“Parallel_reader::parallel_read”中调度worker线程进行拆分、扫描、计数汇总。
图2 InnoDB 并行扫描调度逻辑
下面基于MySQL 8.0.14源码,介绍COUNT并行在SQL引擎和InnoDB存储引擎中的实现。
2.1.1 COUNT并行在SQL引擎中的实现
(1)SQL引擎层在优化阶段判断SQL是否为简单的COUNT,记录在变量“JOIN:: select_count”中,变量的定义参见下方代码。- /*
- When join->select_count is set, tables will not be optimized away.
- The call to records() will be delayed until the execution phase and the counting will be done on an index of Optimizer's choice.
- The index will be decided in find_shortest_key(), called from
- optimize_aggregated_query().
- */
- bool JOIN::select_count{false};
复制代码 (2)SQL引擎层在生成执行计划阶段,判断变量“JOIN::select_count”的值,如果变量值为TRUE,则生成并行COUNT算子“UnqualifiedCountIterator”,用户可以通过“EXPLAIN FORMAT=TREE”或“EXPLAIN ANALYZE”命令查看执行计划,如果包含“Count rows”关键字说明 COUNT 并行生效,参见下面的执行计划。- mysql> explain format=tree select count(*) from lineitem\G
- *************************** 1. row ***************************
- EXPLAIN: -> Count rows in lineitem
复制代码 2.1.2 COUNT并行在InnoDB 存储引擎中的实现(1) SQL引擎调用handler API 接口“handler::ha_records”,传递优化器选择的索引给InnoDB存储引擎,获取COUNT结果。
(2) InnoDB存储引擎只支持主键的并行扫描,函数“ha_innobase::records_from_index”忽略索引信息,强制选择主键进行并行扫描。
(3) InnoDB存储引擎在函数“Parallel_reader::parallel_read”中对主键索引进行初步分片,并调度 worker 线程对分片进一步拆分、扫描、计数。
(4) 我们把InnoDB中响应“handler::ha_records”接口并调度worker进行工作的的线程称为leader线程,leader线程调用堆栈信息如下:- UnqualifiedCountIterator::Read
- get_exact_record_count
- handler::ha_records
- ha_innobase::records_from_index
- ha_innobase::records
- row_scan_index_for_mysql
- row_mysql_parallel_select_count_star
- Parallel_reader::run
- Parallel_reader::parallel_read
复制代码 (5) 我们把InnoDB中响应“Parallel_reader::worker”接口并进行扫描、计数工作的线程称为worker线程,worker线程的并发度可以通过参数“ innodb_parallel_read_threads”控制,worker线程调用堆栈信息如下:- Parallel_reader::worker
- Parallel_reader::Ctx::traverse
- Parallel_reader::Ctx::traverse_recs
复制代码 2.2 性能提升效果
我们使用4U16G规格ECS实例,部署MySQL Community 8.0.14版本,innodb_buffer_pool_size设置为8GB。采用TPC-H测试模型,Scale Factor(Gigabytes)为20,lineitem表主键大小约17.4GB,二级索引i_l_orderkey大小约2.3GB,二级索引i_l_partkey_suppkey大小约3.3GB,表结构如下:- mysql> show create table lineitem\G
- *************************** 1. row ***************************
- Table: lineitemCreate Table:
- CREATE TABLE `lineitem` (
- `L_ORDERKEY` bigint NOT NULL,
- `L_PARTKEY` int NOT NULL,
- `L_SUPPKEY` int NOT NULL,
- `L_LINENUMBER` int NOT NULL,
- `L_QUANTITY` decimal(15,2) NOT NULL,
- `L_EXTENDEDPRICE` decimal(15,2) NOT NULL,
- `L_DISCOUNT` decimal(15,2) NOT NULL,
- `L_TAX` decimal(15,2) NOT NULL,
- `L_RETURNFLAG` char(1) NOT NULL,
- `L_LINESTATUS` char(1) NOT NULL,
- `L_SHIPDATE` date NOT NULL,
- `L_COMMITDATE` date NOT NULL,
- `L_RECEIPTDATE` date NOT NULL,
- `L_SHIPINSTRUCT` char(25) NOT NULL,
- `L_SHIPMODE` char(10) NOT NULL,
- `L_COMMENT` varchar(44) NOT NULL,
- PRIMARY KEY (`L_ORDERKEY`,`L_LINENUMBER`),
- KEY `i_l_orderkey` (`L_ORDERKEY`),
- KEY `i_l_partkey_suppkey` (`L_PARTKEY`,`L_SUPPKEY`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
复制代码 lineitem表的主键约17GB,无法全部加载到Buffer Pool中,每次COUNT执行触发的磁盘IO基本相同(约82万次)。在这个场景下,提升InnoDB并行扫描并发度(innodb_parallel_read_threads),COUNT性能可以线性提升,1并发执行时间约585秒,2并发执行时间约300秒,4并发执行时间约145秒,数据参见图3。
图3 MySQL 8.0 COUNT并行提升效果
2.3 约束限制
(1) 社区MySQL COUNT并行在InnoDB存储引擎实现,只支持主键的并行扫描,忽略了优化器选择的最佳索引。当一个表主键很大、二级索引较小,相比老版本(MySQL 5.7)串行扫描二级索引,社区并行无优化效果。
(2) 社区MySQL COUNT并行只支持无WHERE条件的COUNT,原因在于InnoDB存储无法进行过滤计算。
(3) 当扫描主键数据量很大时,可能会淘汰Buffer Pool中的热数据,导致后续的性能波动。
(4) 社区MySQL COUNT并行强制生效,无法关闭,当遇到(1)中的性能问题时,无法回退至串行扫描二级索引。
使用2.2节相同的测试环境和测试模型,执行“SELECT COUNT(*) FROM lineitem” SQL语句,对比MySQL 5.7.44版本与MySQL 8.0.14版本执行时间,数据参见表1。
表1 MySQL 5.7.44与8.0.14版本COUNT执行时间对比
在这个场景下,MySQL 8.0版本使用4并发扫描主键,但是由于扫描的数据量较大,触发大量的磁盘IO,导致性能差于MySQL 5.7串行扫描二级索引。
3. GaussDB(for MySQL) COUNT 优化
针对MySQL COUNT并行存在的问题,GaussDB(for MySQL)进行了针对性优化,通过自研的并行查询(PQ)和计算下推(NDP)特性,实现了三层并行,加快COUNT执行。框架图参见图4。
- 第一层并行: SQL引擎层,通过自研并行查询,利用多核计算加速;
- 第二层并行:InnoDB存储引擎层,通过自研计算下推特性,触发批量读请求,SAL层将批量读的Page组装、打包,并发将读请求发送至分布式存储(Page Store);
- 第三层并行:Page Store接受到读请求后,每个Page Store内部并发响应读请求,待页面扫描、过滤、聚合操作完成后,将结果返回至计算层。
图4 GaussDB(for MySQL) COUNT并行优化
3.1 原理介绍
下面介绍下GaussDB(for MySQL) COUNT优化细节。
3.1.1 支持动态关闭社区MySQL COUNT并行
当遇到2.3节的性能问题时,可以通过调整参数“innodb_parallel_select_count”动态关闭或开启MySQL COUNT并行功能,使用方法如下:- mysql> SET innodb_parallel_select_count=OFF;mysql> EXPLAIN FORMAT=TREE SELECT COUNT(*) FROM lineitem\G
- *************************** 1. row ***************************
- EXPLAIN: -> Aggregate: count(0)
- -> Index scan on lineitem using i_l_orderkey (cost=12902405.32 rows=118641035)
复制代码 3.1.2 GaussDB(for MySQL)并行查询特性
GaussDB(for MySQL)支持并行查询(PQ)[1],用以降低分析型查询场景的处理时间,满足企业级应用对查询低时延的要求。相比社区MySQL并行查询的诸多限制,GaussDB(for MySQL)自研的并行查询支持主键、二级索引多种扫描方式,适用于大部分SELECT语句。
针对COUNT操作,可以利用PQ特性,并行扫描二级索引,提升查询性能。
用户可以通过Hint的方式开启PQ,当执行计划中出现Parallel、Gather关键字时,说明PQ特性生效。使用方法如下:- mysql> EXPLAIN FORMAT=TREE SELECT/*+ PQ() */ COUNT(*) FROM lineitem\G
- *************************** 1. row ***************************
- EXPLAIN: -> Aggregate: count(`<temporary>`.`0`)
- -> Gather: 4 workers, parallel scan on lineitem
- -> Aggregate: count(`<temporary>`.`0`)
- -> Parallel index scan on lineitem using i_l_orderkey (cost=4004327.70 rows=29660259)
复制代码 3.1.3 GaussDB(for MySQL)计算下推特性
计算下推(Near Data Processing)[2]是GaussDB(for MySQL)提高数据复杂查询效率的解决方案。针对数据密集型查询,将列投影、聚合运算、条件过滤等操作从计算节点向下推送给分布式存储层的多个节点,并行执行。通过计算下推方法,提升了并行处理能力,减少网络流量和计算节点的压力,提高了查询处理执行效率。
针对COUNT操作,可以利用NDP特性,将聚合操作下推至分布式存储,减少网络流量,提升查询性能。
用户可以通过Hint的方式开启NDP,执行计划中出现NDP 关键字时,说明此特性生效。使用方法如下:- mysql> EXPLAIN FORMAT=TREE SELECT/*+ PQ() NDP_PUSHDOWN() */ COUNT(*) FROM lineitem\G
- *************************** 1. row ***************************
- EXPLAIN: -> Aggregate: count(`<temporary>`.`0`)
- -> Gather: 4 workers, parallel scan on lineitem
- -> Aggregate: count(`<temporary>`.`0`)
- -> Parallel index scan on lineitem using i_l_orderkey Using pushed NDP (aggregate) (cost=4046562.45 rows=29047384)
复制代码 3.2性能优化效果
使用2.2节相同的测试环境和测试模型,执行“SELECT COUNT(*) FROM lineitem” SQL语句,对比GaussDB(for MySQL)开启PQ特性与开启PQ+NDP特性的执行时间,参见表2。
表2 GaussDB(for MySQL) COUNT操作执行时间
从测试结果看:只开启PQ特性,并行查询并发度设置为4,磁盘IO约13万次,查询耗时约31秒;
同时开启PQ和NDP特性,并行查询并发度设置为4,NDP通过IO合并和计算下推,大幅减少了磁盘IO,查询耗时只有1.7秒,相比社区MySQL 8.0.22 执行耗时145秒,COUNT性能提升超过80倍。
图5 GaussDB(for MySQL) COUNT优化提升效果
4.总结
社区MySQL 8.0引入了并行扫描主键功能,但不支持并行扫描二级索引,导致在大表或冷数据场景(表页面数据不在Buffer Pool)反而出现劣化,GaussDB(for MySQL)通过并行查询(PQ)和计算下推(NDP)特性,解决了大表COUNT慢的问题,典型场景下相比社区并行,性能提升超过80倍,为用户提供更加极致的体验。
5.相关参考
[1] 并行查询(PQ)https://support.huaweicloud.com/usermanual-gaussdbformysql/gaussdbformysql_05_0150.html
[2] 算子下推(NDP)https://support.huaweicloud.com/usermanual-gaussdbformysql/gaussdbformysql_05_0129.html
点击关注,第一时间了解华为云新鲜技术~
来源:https://www.cnblogs.com/huaweiyun/p/18322194
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作! |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|