最近在做一个数据迁移的小程序,其中有一个大表的分页查询,该表有60个字段,300多万条数据,按每页10000条进行分页,显然按简单的分页查询是不可取的:
select * from 大表 limit offset, n
随着offset增大,查询效率会变得很低,这里解释一下:
mysql分页不是跳过offset行,而是每次都获取offset + n行,然后放弃前offset行,返回n行,所以当offset特别大的时候,查询效率特别低。
因此对sql进行了改写,通过延迟关联来优化sql:
-- 先定位要查询的id段:
-- 方案1
select a.* from 大表 a, (select id from 大表 order by id limit 2990000, 10000) b where a.id = b.id
这是方案1的sql,其中子查询使用主键id上的索引来优化查询。
-- 先定位要查询的id段:
-- 方案2
select a.* from 大表 a, (select id from 大表 order by mobile limit 2990000, 10000) b where a.id = b.id
这是方案2的sql,其中子查询使用了mobile字段上的唯一索引来优化查询。
实际测试的结果:
方案2查询耗时小于方案1
对于这个结果,我隐约感觉到发现了一个坑,继续深挖:
为了简单起见,把上面的子查询抽取出来:
-- sql1
select id from 大表 order by id limit 2990000, 10000
-- sql2
select id from 大表 order by mobile limit 2990000, 10000
下面是查询时间截图:
sql1耗时:
sql2耗时:
可以看到使用moblie字段上的唯一索引比使用主键id上的索引耗时少了大概400ms,这是什么原因呢?
根据mysql innodb引擎的聚集索引选择规则:
- 每张innodb表只能创建一个聚集索引
- 首先选择显式定义的主键索引做为聚集索引;
- 如果没有,则选择第一个NOT NULL的唯一索引;
- 还是没有的话,就采用InnoDB引擎内置的ROWID作为聚集索引;
通过explain分析sql,两条sql的type都是index(索引物理文件全扫描),而且extra列都是Using index,这表示两条sql都进行了索引文件全扫描,并且要查询的列都能包含在索引列中,无需二次查找。
因此得出这么个结论:
*查询相同数量的数据行时,当发生索引文件全扫描时,非聚集索引更快
原因:
聚集索引:索引的逻辑顺序就是数据行在磁盘的物理存储顺序,例如上述主键id,是int类型的自增字段,则数据行就是按id自增的顺序存储。反过来讲,由于数据行在磁盘上的物理存储顺序有且只有一种,所以一个innodb表的聚集索引只能创建一个。
非聚集索引:索引的逻辑顺序和数据行在磁盘的物理存储顺序不同。
有一个很形象的比喻:
按聚集索引查询就像按拼音查询中文字典,拼音字母的排序和字的排序一致;
按非聚集索引查询就像按偏旁部首查询中文字典,偏旁部首的排序和字的排序并不一样。
分析下两种索引的数据结构(都是B树),聚集索引的叶子节点存储的是数据行(包含所有列),非聚集索引的叶子节点存储的是索引的键值和聚集索引的键值,这样只有2列,因此每行的数据量很小,在I/O一页时,能读到的行数更多,因此效率更高,这也就解释了上面的结论。
最后,不要形成定势思维,多用explain来分析sql,同时尽量避免type=index,尽量优化到type=range(索引文件范围查找)