MYSQL 为什么不要使用SELECT * 作为查询条件?(持续更新)

请注意,这篇文章仅针对 InnoDB 存储引擎讨论。

1、减少回表次数

        我们知道,InnoDB 中索引分为两种,聚簇索引和二级索引,每个索引对应一颗B+树,其中聚簇索引的叶子节点存放完整的用户数据,而二级索引的叶子节点只存放 索引列+主键 数据,并且叶子节点按照索引列值从小到大排序链接,具体的,大家可以自行查阅资料补充知识点。

        当我们执行像这么一条查询语句时

SELECT
    *
FROM
    t1
WHERE
    key1 = "a"

        假设 t1 中有2个索引,分别是聚簇索引和二级索引 idx_key1 (key1),(忽略执行计划)那么将会利用 idx_key1 来执行查询语句,从 idx_key1 索引中查找匹配 key1= "a" 的第一条记录,索引 idx_key1 除了记录k1的值外,还记录了对应的主键值,但是由于 SELECT "*" 需要查询所有用户数据,而这里的只有值 (key1,id),所以需要拿着主键值立刻回表查询该行的完整数据,然后返回给客户端。请记住,是立刻回表查询,而不是等查询到所有匹配的记录再一次性回表,当然 InnoDB 为了优化这个情况,用了一些策略来减少回表次数,这里忽略。

回表:二级索引中匹配到的记录中会有对应的索引列和主键值,根据这个主键值回到聚簇索引(也就是主键索引)中查询对应的完整用户记录,这个过程就是回表。

        所以假设索引 idx_key1 中匹配了10条数据,那么将会产生10次回表,而回表的代价是很大的,回表意味着产生页面IO,会影响性能。

        如果我们改一下这条查询语句:

SELECT
    key1, id
FROM
    t1
WHERE
    key1 = "a"

        那么只需要到二级索引中查找匹配的记录,则不需要回表,对性能更加友好。


2、利用到 index 访问方法

        假设表 t1 有2个索引,分别是聚簇索引和二级索引(联合索引) idx_key_part (key_part1, key_part2, key_part3)  。

        当我们执行像这么一条查询语句时

SELECT
    *
FROM
    t1
WHERE
    key_part2 = "a"

        虽有联合索引,但是访问方法仍然是 ALL 全表扫描,因为在索引 idx_key_part 中,数据是先按照 key_part1 排序,再按照 key_part2 排序,最后按照 key_part3 排序的,当查询条件是 key_part2=a,InnoDB无法知道 key_part2=a 的记录分布在哪,需要全表扫描匹配。

        举个例子,假设索引 idx_key_part (key_part1, key_part2, key_part3)  中有4条数据:

(1,b,X)
(1,c,X)
(2,c,X)
(3,a,X)

,在忽略了第一个元素的情况下,第二个元素单独抽出来是(b,c,c,a),如我们所见是无序的,所以只能全表扫描,挨个匹配。

        这种情况下 InnoDB 不会利用索引 idx_key_part ,因为与其查询二级索引,还要花费回表的代价,还不如直接查询聚簇索引来的快。

        但是如果我们把查询修改以下,改为

SELECT
    key1, key2, key3, id
FROM
    t1
WHERE
    key2 = "a"

        则可以利用到 idx_key_part 索引,因为我们虽然WHERE子句中无法利用索引,但是 idx_key_part 记录了 key1, key2, key3, id,与直接查聚簇索引相比,聚簇索引存储的元素更多,这就意味着一次页面IO能够读取的记录更加少,会产生更多的IO,所以 InnoDB 会选择 idx_key_part 来查询,也就是 INDEX 访问方法,也就是大家说的索引覆盖。

        我们通过 EXPLAIN 分析一下 InnoDB 是不是确实这样做的:


3、加快连表速度

        简单说一下联表的原理是先选择一个驱动表,对驱动表执行查询结果,每匹配一条查询结果就立刻访问一次被驱动表。比如下列查询语句

SELECT 
    *
FROM 
    t1, t2
WHERE 
    t1.m1>1 AND t1.m1=t2.m2 AND t2.n2<'d'

        假设选择 t1 作驱动表,查询 t1 后的结果集中符合 t1.m1>1 的有2条记录,分别是

t1.m1 = 2
t1.m1 = 3

        那么每匹配到一条,就要立刻访问 t2 表一次,分别对 t2 表执行查询

SELECT * FROM t2 WHERE t2.m2=2 AND t2.n2<'d';
SELECT * FROM t2 WHERE t2.m2=3 AND t2.n2<'d';

        请注意,是每匹配到一条t1记录,就立刻查询t2一次,而不是全部查询出来再执行查询。假设表 t2 建立了索引 idx_m1_m2_n2,很显然,上面2条查询语句无法利用这个索引,但是如果我们把查询改成

SELECT 
    t1.*, t2.m2
FROM 
    t1, t2
WHERE 
    t1.m1>1 AND t1.m1=t2.m2 AND t2.n2<'d'

        虽然WHERE子句无法利用二级索引,但是 InnoDB 还是选择二级索引 idx_m1_m2_n2 来执行查询,因为可以使用 INDEX 访问方法,代价比 ALL 低,再说一遍,INDEX 比 ALL 效率更高的原因是,二级索引中每页存储的数据量更多,即使对二级索引进行全扫描,产生页面IO也比聚簇索引的更少。

        总而言之,最好不要使用 * 作为查询列表,而是查询真正使用的列,增加 INDEX 作为查询方法的概率。


4、在 Join Buffer 中放置更多记录

        上面提到,联表查询中每匹配到驱动表中的一条数据,就要访问一次被驱动表。如果这个被驱动表数据特别多而且不能使用索引,相当于要访问这个表N次(N取决于表页数),这个IO代价也忒大了!!

        所以 InnoDB 为了减少对被驱动表的访问次数,想了个策略。就是 Join Buffer ,在执行查询前申请一块固定大小的内存。

        先把若干条驱动表的结果集中的记录放到 Join Buffer 中,然后扫描被驱动表,每条被驱动表的记录一次性和  Join Buffer 中的多条驱动表记录作匹配,这个匹配的内存发生在内存中,这样可以显著减少被驱动表的IO代价。

Join Buffer 并不会存放驱动表记录的所有列,而是存放查询列表和过滤条件中的列

        所以,我们写查询的时候尽量不要写 SELECT *, 而是只写真正需要使用的列,提高  Join Buffer 的利用率,SELECT 的列越少,可以存储的条数越多,发生的IO次数也就越少!!

        Join Buffer 可以通过系统变量进行配置,默认265KB

MYSQL 为什么不要使用SELECT * 作为查询条件?(持续更新)_第1张图片

 


 

你可能感兴趣的:(mysql,数据库)