5.MySQL查询语句优化

MySQL逻辑架构

  • MySQL逻辑架构整体分为三层,最上层为客户端层,并非MySQL所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。

  • MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。

  • 最下层为存储引擎,其负责MySQL中的数据存储和提取。和Linux下的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。

MySQL查询过程

语法解析和预处理

MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。

查询优化

经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成查询计划。多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。

MySQL使用基于成本的优化器,它尝试预测一个查询使用某种执行计划时的成本,并选择其中成本最小的一个。在MySQL可以通过查询当前会话的last_query_cost的值来得到其计算当前查询的成本。

mysql> select * from employees where first_name='Eric'  limit 1;
mysql> show status like 'last_query_cost';
+-----------------+--------------+
| Variable_name   | Value        |
+-----------------+--------------+
| Last_query_cost | 60795.999000 |
+-----------------+--------------+
1 row in set (0.00 sec)

mysql> select * from employees where emp_no=10226;
mysql> show status like 'last_query_cost';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| Last_query_cost | 1.000000 |
+-----------------+----------+
1 row in set (0.00 sec)
 

示例中的第一个结果表示优化器认为大概需要做60795.999个数据页(?)的随机查找才能完成上面的查询。这个结果是根据一些列的统计信息计算得来的,这些统计信息包括:每张表或者索引的页面个数、索引的基数、索引和数据行的长度、索引的分布情况等等。

The total cost of the last compiled query as computed by the query optimizer. This is useful for comparing the cost of different query plans for the same query. The default value of 0 means that no query has been compiled yet. The default value is 0. Last_query_cost has session scope.

有非常多的原因会导致MySQL选择错误的执行计划,比如统计信息不准确、不会考虑不受其控制的操作成本(用户自定义函数、存储过程)、MySQL认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但MySQL值选择它认为成本小的,但成本小并不意味着执行时间短)等等。

查询执行引擎

在完成解析和优化阶段以后,MySQL会生成对应的执行计划,查询执行引擎根据执行计划给出的指令逐步执行得出结果。整个执行过程的大部分操作均是通过调用存储引擎实现的接口来完成,这些接口被称为handler API。查询过程中的每一张表由一个handler实例表示。实际上,MySQL在查询优化阶段就为每一张表创建了一个handler实例,优化器可以根据这些实例的接口来获取表的相关信息,包括表的所有列名、索引统计信息等。存储引擎接口提供了非常丰富的功能,但其底层仅有几十个接口,这些接口像搭积木一样完成了一次查询的大部分操作。

返回结果给客户端

查询执行的最后一个阶段就是将结果返回给客户端。即使查询不到数据,MySQL仍然会返回这个查询的相关信息,比如改查询影响到的行数以及执行时间等等。

回头总结一下MySQL整个查询执行过程,总的来说分为6个步骤:

  1. 客户端向MySQL服务器发送一条查询请求

  2. 服务器首先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段

  3. 服务器进行SQL解析、预处理、再由优化器生成对应的执行计划

  4. MySQL根据执行计划,调用存储引擎的API来执行查询

  5. 将结果返回给客户端,同时缓存查询结果

查询语句优化

  1. MySQL不会使用索引的情况:非独立的列

mysql> explain SELECT * FROM employees.employees where emp_no=10226;
+----+-------------+-----------+-------+---------------+---------+---------+-------+------+-------+
| id | select_type | table     | type  | possible_keys | key     | key_len | ref   | rows | Extra |
+----+-------------+-----------+-------+---------------+---------+---------+-------+------+-------+
|  1 | SIMPLE      | employees | const | PRIMARY       | PRIMARY | 4       | const |    1 | NULL  |
+----+-------------+-----------+-------+---------------+---------+---------+-------+------+-------+
1 row in set (0.00 sec)
mysql> explain SELECT * FROM employees.employees where emp_no+1=10227;
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 299335 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)

  

2.前缀索引

如果列很长,通常可以索引开始的部分字符,这样可以有效节约索引空间,从而提高索引效率。

mysql> EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
| id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows   | Extra       |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
|  1 | SIMPLE      | employees | ALL  | NULL          | NULL | NULL    | NULL | 299335 | Using where |
+----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
1 row in set (0.00 sec)

  

根据选择性建立索引:

mysql> SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.0042 |
+-------------+
1 row in set (0.14 sec)
mysql> SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9313 |
+-------------+
1 row in set (0.32 sec)
mysql> SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.7879 |
+-------------+
1 row in set (0.29 sec)
mysql> SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees;
+-------------+
| Selectivity |
+-------------+
|      0.9007 |
+-------------+
1 row in set (0.29 sec)
 

增加前缀索引:

mysql> ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4));
Query OK, 0 rows affected (0.61 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido';
+----+-------------+-----------+------+-----------------------+-----------------------+---------+-------------+------+------------------------------------+
| id | select_type | table     | type | possible_keys         | key                   | key_len | ref         | rows | Extra                              |
+----+-------------+-----------+------+-----------------------+-----------------------+---------+-------------+------+------------------------------------+
|  1 | SIMPLE      | employees | ref  | first_name_last_name4 | first_name_last_name4 | 22      | const,const |    1 | Using index condition; Using where |
+----+-------------+-----------+------+-----------------------+-----------------------+---------+-------------+------+------------------------------------+
1 row in set (0.00 sec)
 

3.多列索引

在多数情况下,在多个列上建立独立的索引并不能提高查询性能。理由非常简单,MySQL不知道选择哪个索引的查询效率更好,所以在老版本,比如MySQL5.0之前就会随便选择一个列的索引,而新的版本会采用合并索引的策略。

mysql> explain SELECT * FROM employees.employees where emp_no < 10020 or first_name='Saniya';
+----+-------------+-----------+-------------+-------------------------------+-------------------------------+---------+------+------+--------------------------------------------------------------+
| id | select_type | table     | type        | possible_keys                 | key                           | key_len | ref  | rows | Extra                                                        |
+----+-------------+-----------+-------------+-------------------------------+-------------------------------+---------+------+------+--------------------------------------------------------------+
|  1 | SIMPLE      | employees | index_merge | PRIMARY,first_name_last_name4 | first_name_last_name4,PRIMARY | 16,4    | NULL |  275 | Using sort_union(first_name_last_name4,PRIMARY); Using where |
+----+-------------+-----------+-------------+-------------------------------+-------------------------------+---------+------+------+--------------------------------------------------------------+
1 row in set (0.00 sec)

  

4.覆盖索引

当发起一个索引覆盖的查询时,在EXPLAIN的Extra列可以看到“Using index”的信息。

mysql> explain select emp_no,first_name from employees where first_name = 'Saniya';
+----+-------------+-----------+------+-----------------------+-----------------------+---------+-------+------+--------------------------+
| id | select_type | table     | type | possible_keys         | key                   | key_len | ref   | rows | Extra                    |
+----+-------------+-----------+------+-----------------------+-----------------------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | employees | ref  | first_name_last_name4 | first_name_last_name4 | 16      | const |  256 | Using where; Using index |
+----+-------------+-----------+------+-----------------------+-----------------------+---------+-------+------+--------------------------+
1 row in set (0.00 sec)

  

5.使用索引扫描来排序

MySQL有两种方式可以生产有序的结果集,其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的。如果explain的结果中type列的值为index表示使用了索引扫描来做排序。

在设计索引时,如果一个索引既能够满足排序,又满足查询,是最好的。

只有当索引的列顺序和ORDER BY子句的顺序完全一致,并且所有列的排序方向也一样时,才能够使用索引来对结果做排序。如果查询需要关联多张表,则只有ORDER BY子句引用的字段全部为第一张表时,才能使用索引做排序。ORDER BY子句和查询的限制是一样的,都要满足最左前缀的要求(有一种情况例外,就是最左的列被指定为常数),其他情况下都需要执行排序操作,而无法利用索引排序。

6.优化LIMIT分页

当需要分页操作时,通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY字句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。

一个常见的问题是当偏移量非常大的时候,比如:LIMIT 10000 20这样的查询,MySQL需要查询10020条记录然后只返回20条记录,前面的10000条都将被抛弃,这样的代价非常高。

优化这种查询一个最简单的办法就是尽可能的使用覆盖索引扫描,而不是查询所有的列。然后根据需要做一次关联查询再返回所有的列。对于偏移量很大时,这样做的效率会提升非常大。


有时候如果可以使用书签记录上次取数据的位置,那么下次就可以直接从该书签记录的位置开始扫描,这样就可以避免使用OFFSET,比如下面的查询:

代码块
SELECT id FROM t LIMIT 10000, 10;
改为:
SELECT id FROM t WHERE id > 10000 LIMIT 10;

参考资料:

1.我必须得告诉大家的MySQL优化原理

https://www.jianshu.com/p/d7665192aaaf

2.MySQL InnoDB索引选择性与统计信息

http://www.ywnds.com/?p=8378

3.MySQL服务与存储引擎间的接口

https://blog.csdn.net/itopcat/article/details/73614101

4.Writing a Custom Storage Engine

https://dev.mysql.com/doc/internals/en/custom-engine.html

猜你喜欢

转载自www.cnblogs.com/pugongying017/p/9616374.html