一条MySQL查询语句的执行流程

强颜欢笑 提交于 2021-01-18 06:32:41

这篇笔记主要记录mysql的基础架构,一条查询语句是如何执行的。

比如,在我们从student表中查询一个id=2的信息:

select * from student where id = 2;

在解释这条语句执行流程之前,我们看看MySQL的基础架构。

MySQL的逻辑架构

  • Server层包括连接器、查询缓存、分析器、优化器、执行器,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
  • 存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory 等多个存储引擎,MySQL默认的存储引擎是InnoDB。

1, 连接器

我们在使用数据库之前,需要连接到数据库,连接语句是

mysql -h $ip -u $username -p $password

而我们的连接器就是处理这个过程的,连接器的主要功能是负责跟客户端建立连接、获取权限、维持和管理连接,连接器在使用的过程中如果该用户的权限改变,是不会马上生效的,因为用户权限是在连接的时候读取的,只能重新连接才可以更新权限

连接器与客户端通信的协议是tcp协议的,连接以后可以使用show processlist;看到执行的连接数

MySQL [abc]> show processlist;
+------+--------------+---------------------+---------+-------------+-------+---------------------------------------------------------------+------------------+
| Id   | User         | Host                | db      | Command     | Time  | State                                                         | Info             |
+------+--------------+---------------------+---------+-------------+-------+---------------------------------------------------------------+------------------+
|    5 | slave_backup | 111.11.111.11:32598 | NULL    | Binlog Dump | 13180 | Master has sent all binlog to slave; waiting for more updates | NULL             |
|   55 | user_name    | 127.0.0.1:33216     | db_name | Sleep       |  3669 |                                                               | NULL             |
|  204 | user_name    | 127.0.0.1:34022     | db_name | Sleep       |  3888 |                                                               | NULL             |
| 7499 | user_name    | localhost           | db_name | Query       |     0 | starting                                                      | show processlist |
+------+--------------+---------------------+---------+-------------+-------+---------------------------------------------------------------+------------------+

在连接时间内超过8小时是sleep的状态会自动断开,这个是MySQL默认设置,如果一直不断开,那么这个过程可以叫做一个长连接

与之对应的有短连接,短连接是指在执行一条或几条的以后断开连接。

当不断使用长连接的时候会占用很大的内存资源,在MySQL 5.7以后可以使用mysql_reset_connection语句来重新初始化资源。

2, 查询缓存

经过连接以后,就连接上数据库了,这个时候可以执行语句了。

执行语句的时候,MySQL首先是去查询缓存,之前有没有执行过这样的语句,MySQL会将之前执行过的语句和结果key-value的形式存储起来(当然有一定的存储和失效时间),如果存在缓存,则直接返回缓存的结果。

缓存的工作流程是

  • 服务器接收SQL语句,以SQL和一些其他条件为key查找缓存表;
  • 如果找到了缓存,则直接返回缓存;
  • 如果没有找到缓存,则执行SQL查询,包括原来的SQL语句的解析,优化等;
  • 执行完SQL查询结果以后,将SQL查询结果缓存入缓存表;

当然,如果这个表修改了,那么使用这个表中的所有缓存将不再有效,查询缓存值的相关条目将被清空。所以在一张被反复修改的表中进行语句缓存是不合适的,因为缓存随时都会失效,这样查询缓存的命中率就会降低很多,不是很划算。

当这个表正在写入数据,则这个表的缓存(命中缓存,缓存写入等)将会处于失效状态,在Innodb中,如果某个事务修改了这张表,则这个表的缓存在事务提交前都会处于失效状态,在这个事务提交前,这个表的相关查询都无法被缓存。

一般来说,如果是一张静态表或者是很少变化的表就可以进行缓存,这样的命中率就很高。

下面来说说缓存的使用时机,衡量打开缓存是否对系统有性能提升是一个很难的话题。

  • 通过缓存命中率判断, 缓存命中率 = 缓存命中次数 (Qcache_hits) / 查询次数 (Com_select)
  • 通过缓存写入率, 写入率 = 缓存写入次数 (Qcache_inserts) / 查询次数 (Qcache_inserts)
  • 通过 命中-写入率 判断, 比率 = 命中次数 (Qcache_hits) / 写入次数 (Qcache_inserts), 高性能MySQL中称之为比较能反映性能提升的指数,一般来说达到3:1则算是查询缓存有效,而最好能够达到10:1

3, 分析器 Parsor 和 预处理器 Preprocessor

在查询缓存失效 或者 无缓存 的时候,这个时候MySQL的Server就会利用 分析器 和 预处理器 来分析语句,分析器也叫解析器。

MySQL分析器 和 预处理器 完成以下三部分动作:

  • 第一部分是用来 词法分析 扫描 字符流,根据构词规则识别单个单词,MySQL使用 Flex 来生成词法扫描程序,在sql/lex.h中定义了MySQL关键字和函数关键字,用两个数组存储;
  • 第二部分的功能是语法分析,在词法分析的基础上将单词序列组成语法短语(tokens),最后生成语法树(parse tree)提交给优化器。语法分析器使用Bison, 在sql/sql_yacc.yy中定义了语法规则,然后根据关系代数理论生成语法树。

  • 第三部分是预处理器,它会对上一步骤生成的语法树进行额外的、分析器无法实现的 语法检查。例如:它会检查所查询的表和字段是否存在,它也解决语句中所有的 名称 和 别名,以确保字段引用不是 模糊或有歧义的。接下来,预处理器检查权限,一般来说是非常快的,除非Server有大量的权限。

    The preprocessor then checks the resulting parse tree for additional semantics that the parser can’t resolve. For example, it checks that tables and columns exist, and it resolves names and aliases to ensure that column references aren’t ambiguous.

    Next, the preprocessor checks privileges. This is normally very fast unless your server has large numbers of privileges. (See Chapter 12 for more on privileges and security.)

上面解释分析器太官方和复杂了,其实分析器主要是用来进行"词法分析",然后知道这个数据库语句是要干嘛,代表啥意思。

这个时候如果分析器分析出这个语句有问题的时候会报错,比如ERROR 1064 (42000): You have an error in your SQL syntax

4, 优化器 Optimizer

经过分析器分析完了之后,知道这个语句没有语法错误,也知道了它需要查询哪些表和字段,接下来是专门用一个优化器进行语句优化。一条MySQL语句通常会有多种执行方式,且都能获得相同的结果。优化器的任务,就是在所有可能的执行计划(execution plans)中,搜索最佳方案,减少开销,提高执行效率。

MySQL 使用的是 一种基于成本的优化器,意味着,它尝试着预测各个执行计划的成本,并选择最低成本的那个。成本的单位,是一个单独的 随机的 4 数据页读。如果你要查看一条MySQL查询语句的成本,你可以先执行它,然后通过 Last_query_cost 这个会话变量来查看,例如:

mysql> SELECT SQL_NO_CACHE COUNT(*) FROM abc;
+----------+
| COUNT(*) |
+----------+
|        7 |
+----------+
1 row in set (0.04 sec)

mysql> SHOW STATUS LIKE 'last_query_cost';
+-----------------+----------+
| Variable_name   | Value    |
+-----------------+----------+
| Last_query_cost | 2.399000 |
+-----------------+----------+
1 row in set (0.04 sec)

MySQL的优化器是一个非常复杂的部件,它使用了非常多的 优化策略而且随着MySQL的不断发展,优化器使用的优化策略也在不断的进化,这里仅仅介绍几个非常常用且容易理解的优化策略:

4.1 重新定义连表顺序 Reordering joins

多表关联查询时,表与表之间并不总是按照SQL语句中所指明的关联顺序。判断最好的关联顺序是一个重要的优化。可以参考此处

Tables don’t always have to be joined in the order you specify in the query. Determining the best join order is an important optimization; we explain it in depth in “The join optimizer” on The join optimizer.

4.2 将外连接转为内连接 Converting OUTER JOINs to INNER JOINs

外连接并不是必须要按照外连接来执行的。在某些条件下,例如 WHERE 语句 和 表结构,可以实际上导致 外连接 等同于 一个内连接。MySQL可以识别并改写连接,使它能重新排序。

An OUTER JOIN doesn’t necessarily have to be executed as an OUTER JOIN. Some factors, such as the WHERE clause and table schema, can actually cause an OUTER JOIN to be equivalent to an INNER JOIN. MySQL can recognize this and rewrite the join, which makes it eligible for reordering.

4.3 应用代数等式规则 Applying algebraic equivalence rules

MySQL 应用代数转换去简化和规范化表达式。它同样可以覆盖和减少常量,消除不可能的约束,以及常量条件。例如 (5=5 AND a>5) 这个条件,可以直接缩减为 a>5 。类似地, (a>b AND b=c) AND a=5 可以变为 b>5 AND b=c AND a=5。这些规则对于条件查询非常有用,我们会稍后在本章节讨论。

MySQL applies algebraic transformations to simplify and canonicalize expressions. It can also fold and reduce constants, eliminating impossible constraints and constant conditions. For example, the term (5=5 AND a>5) will reduce to just a>5. Similarly, (a<b AND b=c) AND a=5 becomes b>5 AND b=c AND a=5. These rules are very useful for writing conditional queries, which we discuss later in the chapter.

4.4 COUNT(), MIN()  MAX() 优化

索引和字段的可为NULL性,可以经常帮助MySQL将这些表达式进行优化。例如,为了找出一个字段的最小值,也就是在B-Tree树索引的最左端,MySQL可以只请求索引的第一行即可。它甚至可以在查询优化的阶段这样做,然后这条查询的后续环节中将这个值当做一个常量。类似地,B-Tree树索引中的最大值,MySQL Server就读最后一行。如果MySQL Server使用这个优化,你可以从 EXPLAIN执行计划中看到 "Select tables optimized away"。这从字面上意味着,优化器已经将表从查询计划中移出了,并将它替代为一个常量。

同样地,不带WHERE条件的 COUNT(*) 语句,在一些存储引擎中也经常可被优化 (例如MyISAM,可以一直在表中保存精确的行数)。你可通过此处查看 COUNT()语句的优化。

Indexes and column nullability can often help MySQL optimize away these expressions. For example, to find the minimum value of a column that’s leftmost in a B-Tree index, MySQL can just request the first row in the index. It can even do this in the query optimization stage, and treat the value as a constant for the rest of the query. Similarly, to find the maximum value in a B-Tree index, the server reads the last row. If the server uses this optimization, you’ll see “Select tables optimized away” in the EXPLAIN plan. This literally means the optimizer has removed the table from the query plan and replaced it with a constant.

Likewise, COUNT(*) queries without a WHERE clause can often be optimized away on some storage engines (such as MyISAM, which keeps an exact count of rows in the table at all times). See “Optimizing COUNT() Queries” on Optimizing Specific Types of Queries for details.

4.5 评估并减少常量表达式 Evaluating and reducing constant expressions

例如主键查询,上面提到的 MIN()、MAX() 等。

4.6 覆盖索引 Covering indexes

当索引包含了查询语句所需的所有字段时,MySQL可以直接使用索引去避免读行数据。我们将在第三张讨论覆盖索引

MySQL can sometimes use an index to avoid reading row data, when the index contains all the columns the query needs. We discussed covering indexes at length in Chapter 3.

4.7 子查询优化 Subquery optimization

MySQL可以将一个类型的子查询转化为更有效率的其他形式,将它们降减为索引查找而不是独立的查询。

MySQL can convert some types of subqueries into more efficient alternative forms, reducing them to index lookups instead of separate queries.

4.8 提前终止 Early termination

MySQL可以在它满足查询的语句或步骤时就停止执行。最明显的例子就是 LIMIT 语句,但是还有几个其他类型的提前终止。例如:如果MySQL发现一个不可能的条件,它就会中断整个查询。你可以在以下例子中看到:(主键ID为-1或NULL)

MySQL can stop processing a query (or a step in a query) as soon as it fulfills the query or step. The obvious case is a LIMIT clause, but there are several other kinds of early termination. For instance, if MySQL detects an impossible condition, it can abort the entire query. You can see this in the following example:

mysql> EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1;
+----+...+-----------------------------------------------------+
| id |...| Extra                                               |
+----+...+-----------------------------------------------------+
|  1 |...| Impossible WHERE noticed after reading const tables |
+----+...+-----------------------------------------------------+

This query stopped during the optimization step, but MySQL can also terminate execution sooner in some cases. The server can use this optimization when the query execution engine recognizes the need to retrieve distinct values, or to stop when a value doesn’t exist. For example, the following query finds all movies without any actors: [42]

mysql> SELECT film.film_id
    -> FROM sakila.film
    ->    LEFT OUTER JOIN sakila.film_actor USING(film_id)
    -> WHERE film_actor.film_id IS NULL;

This query works by eliminating any films that have actors. Each film might have many actors, but as soon as it finds one actor, it stops processing the current film and moves to the next one because it knows the WHERE clause prohibits outputting that film. A similar “Distinct/not-exists” optimization can apply to certain kinds of DISTINCT, NOT EXISTS(), and LEFT JOIN queries.

4.9 等式传播 Equality propagation

MySQL认识到,当一个语句带了2个相同的字段时,例如,在一个 JOIN 条件 并在相同字段间 传递了 WHERE 语句。例如:

MySQL recognizes when a query holds two columns as equal—for example, in a JOIN condition—and propagates WHERE clauses across equivalent columns. For instance, in the following query:

mysql> SELECT film.film_id
    -> FROM sakila.film
    ->    INNER JOIN sakila.film_actor USING(film_id)
    -> WHERE film.film_id > 500;

在此例子中,MySQL知道 WHERE 语句不仅仅在 film 表中应用了,而且在 film_actor表中也用到了,因为 USING 语句强制将这两个字段进行了等同匹配。

MySQL knows that the WHERE clause applies not only to the film table but to the film_actor table as well, because the USING clause forces the two columns to match.

如果你在其他的不支持此操作的数据库这样使用,你可能会被建议通过手动地 为两个表都指名 WHERE语句,像这样:

If you’re used to another database server that can’t do this, you may have been advised to “help the optimizer” by manually specifying the WHERE clause for both tables, like this:

... WHERE film.film_id > 500 AND film_actor.film_id > 500

This is unnecessary in MySQL. It just makes your queries harder to maintain. 这在MySQL中是不必要的。这会让你的语句更难维护。

4.10 IN() 列表比较IN() list comparisons

在很多数据库中,IN() 仅仅是多个 OR语句的同义词,因为这两者在逻辑上是等同的。在MySQL中不是这样的,它会将 IN() 列表中的值进行排序,并使用快速二分查询去判断 一个值 是否在这个列表中。这在列表中是 O(log n) 的时间复杂度,而对应的 OR 语句 在列表中是 O(n) 的时间复杂度 (也就意味着,对大列表会非常慢)

In many database servers, IN() is just a synonym for multiple OR clauses, because the two are logically equivalent. Not so in MySQL, which sorts the values in the IN() list and uses a fast binary search to see whether a value is in the list. This is O(log n) in the size of the list, whereas an equivalent series of OR clauses is O(n) in the size of the list (i.e., much slower for large lists).

4.11 优化排序

在老版本MySQL会使用两次传输排序,即先读取行指针 和 需要排序的字段在内存中对其排序,然后再根据排序结果去读取数据行,而新版本采用的是单次传输排序,也就是一次读取所有的数据行,然后根据给定的列排序。对于I/O密集型应用,效率会高很多。

 

5, 执行器 

在分析器知道语句要干什么,优化器知道怎么做以后,下面就到了执行的阶段,执行是交给执行器的。

执行器在执行的时候首先判断该用户对该表有没有执行权限,如果没有则会返回denied之类的错误提示。

如果有权限,则会打开表继续执行。打开表的时候,执行器会根据表定义的引擎,去使用该引擎的接口。

最后执行语句得到数据返回给客户端。

6,总结

MySQL得到sql语句后,大概执行流程如下:

6.1 连接器负责和客户端进行通信
6.2 查询缓存:首先查询缓存看是否存在k-v缓存
6.3 解析器:负责解析和转发SQL
6.4 预处理器:对解析后的SQL树进行验证
6.5 优化器:得到一个执行计划
6.6 查询执行引擎:执行器执行语句得到数据结果集
6.7 将数据放回给调用端

 

参考链接:

https://www.oreilly.com/library/view/high-performance-mysql/9780596101718/ch04.html

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!