MySQL大数据分页调优实践

一、实践回顾

MySQL版本5.7.28。

需求是将A表(350W记录)中的有效数据存入ES中。所谓有效数据是指未逻辑删除且未Disabled的数据,其实就是一组where条件。

原有的方案是使用SQL的Limit功能分页读取数据,将数据压入MQ中,消费者监听MQ并将数据存入ES。伴随着pageIndex的增长,MySQL查询速度越来越缓慢,3个小时仅处理了150W数据。

为加快数据同步速度,在对SQL Explain之后,确定了两条优化方案:

1. SQL仅取A表的主键,消费者根据主键列表再获取A表各字段信息;

2. 优化SQL分页,通过使用lastId和limit组合获取分页数据。

最终,采用单消费者单线程的方式,仅用40分钟就处理完成了全部350数据,比之前提速10倍以上。

二、步骤过程

1. 我们先来看一下使用传统limit在2000页200W数据之后的性能表现:

select id, xxx, xxx
from A
where xxx and enabled = 1 and deleted = 0
order by id
limit 2300000, 1000;

这个耗时在网络繁忙的情况下甚至达到过30秒的速度。

Explain一下该SQL

2. 在来看一下使用传统limit仅对主键进行分页之后的性能表现:

select id
from A
where xxx and enabled = 1 and deleted = 0
order by id
limit 2300000, 1000;

3. 采用lastId过滤后的结果:

select id
from A
where xxx and enabled = 1 and deleted = 0
order by id
limit 1000;

注意,以上三次explain的结果均显示possible_keys中的idx_enabled未在实际查询中使用,这是因为enabled是一个bit类型字段,该类型的索引区分度太低。

三、Explain字段

Explain有两种格式,一种是按字段格式展示,另一种是Json格式,各个字段的对应关系如下:

Column Json Name 描述
id select_id 语句编号
select_type None 查询(select)的类型
table table_name 当前语句的表名
partitions partitions 匹配的分区
type access_type 连接类型
possible_keys possible_keys 可用的索引
key key 当前查询实际使用的索引
key_len key_len 使用的索引的长度
ref ref 哪些列或常量被用来与"key"字段中的索引进行比较。
rows rows 被检索的行数。对InnoDB存储引擎来讲,这是一个估计值。
filtered filtered 被过滤的行数占"rows"字段的比例。也是一个估计值。
Extra None 额外信息。

四、Explain字段详解

1.id: SQL语句编号,如果有子查询,会按照查询顺序编号。如果SQL中包含union关键字,该值可能为null。

2.select_type: 每个查询(select)子句的类型,共有以下类型:

select_type Json Name 描述
SIMPLE None 简单查询语句(不包含UNION或子查询)
PRIMARY None 对于包含子查询的复杂语句,最外层的语句会被标记为PRIMARY
UNION None 在UNION语句中第一个之后的select语句
DEPENDENT UNION dependent (true) 在UNION语句中第一个之后的select语句,依赖外层查询
UNION RESULT union_result 在UNION语句的结果
SUBQUERY None 子查询中第一个select语句
DEPENDENT SUBQUERY dependent (true) 子查询中第一个select语句,依赖外部查询
DERIVED None 派生表,from后的子查询
MATERIALIZED materialized_from_subquery 视图查询
UNCACHEABLE SUBQUERY cacheable (false) 如果一个子查询的结果对于外部查询无法缓存,只能重新计算,则标记此类型
UNCACHEABLE UNION cacheable (false) 对于上面的UNCACHEABLE SUBQUERY,如果它同时属于union语句中第二个或之后的查询

3. table: 每个查询的表,除了实际表名之外,还有三种特殊格式的表名:

(1)<unionM,N> 这种格式说明数据来源于两个查询union之后的结果,M和N是union前后两个查询的id编号;

(2)<derivedN> 这种格式说明数据来源于派生表的结果,N是派生查询的id编号;

(3)<subqueryN> 这种格式说明数据来源于子查询的结果,N是子查询的id编号;

4. partitions: 当前查询所匹配的分区,若无分区则值为NULL;

5. type: 当前查询的访问类型(access type),性能从优到劣分别如下:

(1)system, 如果当前查询的表只有一行数据;

(2)const, 如果当前查询只匹配最多一条数据;

(3)eq_ref, 如果查询全部使用了主键或者唯一非空索引,不同于上面两种类型,这是绝大多数查询中性能最佳的访问类型;

(4)ref, 如果查询使用了普通索引;

(5)fulltext, 如果查询使用了全文检索索引;

(6)ref_or_null, 与ref类型类似,但MySQL额外进行了空值处理;如: SELECT * FROM ref_table WHERE key_column=expr OR key_column IS NULL;

(7)index_merge, 对于同一个表,如果检索条件中的多个字段均使用了索引,则MySQL可能对多个索引列进行index merge优化;

(8)unique_subquery, 在in类型的子查询中,当子查询返回了拥有唯一索引(比如主键)的字段时,MySQL会使用该类型来提升当前查询的性能;

(9)index_subquery, 与unique_subquery类似,应用于返回的字段拥有普通索引;

(10)range, 查询语句只检索指定范围的行,当查询语句包含 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, LIKE, 或 IN()时,可能触发该种访问类型;

(11)index, 全索引树检索,当查询的字段仅有部分拥有索引时,MySQL可能会触发该类访问。绝大多数情况下,扫描全索引树要比扫描全表更快,因为前者远小于后者;该访问类型分为两种情形:

a. 如果当前查询所需的列均包含在索引树中,所查询的数据需要扫描全树,此时type=index,Extra=Using index;

b. 如果当前查询按索引树的顺序进行全表扫描,此时type=index,Extra中没有Using index。

(12)ALL, 全表扫描。这是需要绝对避免的类型。

6. possible_keys: 当前查询MySQL可以使用的索引,但MySQL在具体执行查询过程中可能不会使用其中的全部或部分索引。如果值为NULL,说明本次查询没有可用的索引。

7. key: 当前查询MySQL实际使用的索引。

8. key_len: 当前查询所用索引的长度(字节)。可空字段的索引长度要长于非空字段的索引长度,所以字段最好设置为非空。

9. ref: 哪些列或常量被用来与"key"字段中的索引进行比较。

10. rows: 被检索的行数。对InnoDB存储引擎来讲,这是一个估计值。

11. filtered: 被过滤的行数占"rows"字段的比例。也是一个估计值。

12. Extra: 包含MySQL执行当前查询的额外信息。

posted @ 2021-06-02 20:39  白马黑衣  阅读(192)  评论(0编辑  收藏  举报