MySQL回表
五花马,千金裘,呼儿将出换美酒,与尔同销万古愁。
回表,顾名思义就是回到表中,也就是先通过普通索引扫描出数据所在的行,再通过行主键ID取出索引中未包含的数据。所以回表的产生也是需要一定条件的,如果一次索引查询就能获得所有的select记录就不需要回表,如果select所需获得列中有其他的非索引列,就会发生回表动作。即基于非主键索引的查询需要多扫描一棵索引树。
要弄明白回表,首先得了解 InnoDB 两大索引,即聚集索引 (clustered index)和普通索引(secondary index)。
InnoDB聚集索引的叶子节点存储行记录,因此,InnoDB必须要有且只有一个聚集索引。
普通索引也叫二级索引,除聚簇索引外的索引都是普通索引,即非聚簇索引。
InnoDB的普通索引叶子节点存储的是主键(聚簇索引)的值,而MyISAM的普通索引存储的是记录指针。
先创建一张表t_back_to_table ,表中id为主键索引即聚簇索引,drinker_id为普通索引。
CREATETABLE t_back_to_table ( idINTPRIMARYKEY, drinker_idINTNOTNULL, drinker_nameVARCHAR (15 )NOTNULL, drinker_featureVARCHAR (15 )NOTNULL,INDEX ( drinker_id ) ) ENGINE= INNODB;
再执行下面的SQL语句,插入四条测试数据。
INSERTINTO t_back_to_table ( id, drinker_id, drinker_name, drinker_feature )VALUES (1,2,'广西-玉林','喝到天亮' ), (2,1,'广西-河池','白酒三斤半啤酒随便灌' ), (3,3,'广西-贵港','喝到晚上' ), (4,4,'广西-柳州','喝酒不吃饭' );
使用主键索引id,查询出id为3的数据。
EXPLAINSELECT*FROM t_back_to_tableWHERE id=3;
执行EXPLAIN SELECT * FROM t_back_to_table WHERE id = 3,这条SQL语句就不需要回表。
因为是根据主键的查询方式,则只需要搜索ID这棵B+树,树上的叶子节点存储了行记录,根据这个唯一的索引,MySQL就能确定搜索的记录。
使用drinker_id这个索引来查询drinker_id = 3的记录时就会涉及到回表。
SELECT*FROM t_back_to_tableWHERE drinker_id=3;
因为通过drinker_id这个普通索引查询方式,则需要先搜索drinker_id索引树(该索引树上记录着主键ID的值),然后得到主键ID的值为3,再到ID索引树搜索一次。这个过程虽然用了索引,但实际上底层进行了两次索引查询,这个过程就称为回表。
回表小结
InnoDB引擎的聚集索引和普通索引都是B+Tree存储结构,只有叶子节点存储数据。
id 是主键,所以是聚簇索引,其叶子节点存储的是对应行记录的数据。
聚簇索引存储结构
如果查询条件为主键(聚簇索引),则只需扫描一次B+树即可通过聚簇索引定位到要查找的行记录数据。
如:
SELECT*FROM t_back_to_tableWHERE id=1;
查找过程:
drinker_id是普通索引(二级索引),非聚簇索引的叶子节点存储的是聚簇索引的值,即主键ID的值。
普通索引存储结构
如果查询条件为普通索引(非聚簇索引),需要扫描两次B+树。
如:
SELECT*FROM t_back_to_tableWHERE drinker_id=1;
(1)第一步,先通过普通索引定位到主键值id=1;
(2)第二步,回表查询,再通过定位到的主键值即聚集索引定位到行记录数据。
普通索引查找过程
既然我们知道了有回表这么回事,肯定就要尽可能去防微杜渐。最常见的防止回表手段就是索引覆盖,通过索引打败索引。
为什么可以使用索引打败索引防止回表呢?因为其只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表查询。
例如:SELECT*FROM t_back_to_tableWHERE drinker_id=1;
如何实现覆盖索引?
常见的方法是将被查询的字段,建立到联合索引中。
解释性SQL的explain的输出结果Extra字段为Using index时表示触发了索引覆盖。
No覆盖索引case1
继续使用之前创建的 t_back_to_table 表,通过普通索引drinker_id 查询id 和 drinker_id 列。
EXPLAINSELECT id, drinker_idFROM t_back_to_tableWHERE drinker_id=1;
explain分析:为什么没有创建覆盖索引Extra字段仍为Using index,因为drinker_id是普通索引,使用到了drinker_id索引,在上面有提到普通索引的叶子节点保存了聚簇索引的值,所以通过一次扫描B+树即可查询到相应的结果,这样就实现了隐形的覆盖索引,即没有人为的建立联合索引。(drinker_id索引上包含了主键索引的值)
No覆盖索引case2
继续使用之前创建的 t_back_to_table 表,通过普通索引drinker_id查询id、drinker_id和drinker_feature三列数据。
EXPLAINSELECT id, drinker_id, drinker_featureFROM t_back_to_tableWHERE drinker_id=1;
explain分析:drinker_id是普通索引其叶子节点上仅包含主键索引的值,而drinker_feature列并不在索引树上,所以通过drinker_id索引在查询到id和drinker_id的值后,需要根据主键id 进行回表查询,得到drinker_feature的值。此时的Extra列的NULL表示进行了回表查询。
覆盖索引case
为了实现索引覆盖,需要建组合索引idx_drinker_id_drinker_feature(drinker_id,drinker_feature)
#删除索引 drinker_idDROPINDEX drinker_idON t_back_to_table; #建立组合索引CREATEINDEX idx_drinker_id_drinker_featureon t_back_to_table(`drinker_id`,`drinker_feature`);
继续使用之前创建的 t_back_to_table 表,通过覆盖索引idx_drinker_id_drinker_feature查询id、drinker_id和drinker_feature三列数据。
EXPLAINSELECT id, drinker_id, drinker_featureFROM t_back_to_tableWHERE drinker_id=1;
explain分析:此时字段drinker_id和drinker_feature是组合索引idx_drinker_id_drinker_feature,查询的字段id、drinker_id和drinker_feature的值刚刚都在索引树上,只需扫描一次组合索引B+树即可,这就是实现了索引覆盖,此时的Extra字段为Using index表示使用了索引覆盖。
适合使用索引覆盖来优化SQL的场景如全表count查询、列查询回表和分页查询等。
#首先删除 t_back_to_table 表中的组合索引DROPINDEX idx_drinker_id_drinker_featureON t_back_to_table; EXPLAINSELECTCOUNT(drinker_id)FROM t_back_to_table
explain分析:此时的Extra字段为Null 表示没有使用索引覆盖。
使用索引覆盖优化,创建drinker_id字段索引。
#创建 drinker_id 字段索引CREATEINDEX idx_drinker_idon t_back_to_table(drinker_id); EXPLAINSELECTCOUNT(drinker_id)FROM t_back_to_table
explain分析:此时的Extra字段为Using index表示使用了索引覆盖。
前文在描述索引覆盖使用的例子就是列查询回表优化。
例如:
SELECT id, drinker_id, drinker_featureFROM t_back_to_tableWHERE drinker_id=1;
使用索引覆盖:建组合索引 idx_drinker_id_drinker_feature on t_back_to_table(`drinker_id`,`drinker_feature`)即可。
#首先删除 t_back_to_table 表中的索引 idx_drinker_idDROPINDEX idx_drinker_idON t_back_to_table; EXPLAINSELECT id, drinker_id, drinker_name, drinker_featureFROM t_back_to_tableORDERBY drinker_id limit200,10;
explain分析:因为drinker_id 字段不是索引,所以在分页查询需要进行回表查询,此时Extra为U sing filesort 文件排序,查询性能低下。
使用索引覆盖:建组合索引idx_drinker_id_drinker_name_drinker_feature
#建立组合索引 idx_drinker_id_drinker_name_drinker_feature (`drinker_id`,`drinker_name`,`drinker_feature`)CREATEINDEX idx_drinker_id_drinker_name_drinker_featureon t_back_to_table(`drinker_id`,`drinker_name`,`drinker_feature`);
再次根据 drinker_id 分页查询:
EXPLAINSELECT id, drinker_id, drinker_name, drinker_featureFROM t_back_to_tableORDERBY drinker_id limit200,10;
explain分析:此时的Extra字段为Using index表示使用了索引覆盖。