diff --git a/docs/basic/9.mysql_architecture.md b/docs/basic/9.mysql_architecture.md index eda337e0..16d1577a 100644 --- a/docs/basic/9.mysql_architecture.md +++ b/docs/basic/9.mysql_architecture.md @@ -1,22 +1,21 @@ -## MySQL逻辑架构 +## MySQL 逻辑架构 ![architecture](https://fengzhaonote.notion.site/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F60e63f69-3261-48df-a6d9-34ac0f8d73a7%2FUntitled.png?table=block&id=d6d2f0cf-e51d-48c7-a078-9126b92ec080&spaceId=5468c791-49fa-43aa-825f-28501aabf76f&width=1920&userId=&cache=v2) -MySQL逻辑架构整体分为三层: +MySQL 逻辑架构整体分为三层: -- 连接层:最上层为客户端层,并非MySQL所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。 -- 中间层:MySQL大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。 -- 存储引擎层:最下层为存储引擎,其负责MySQL中的数据存储和提取。和Linux中的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过API与存储引擎通信,这些API接口屏蔽了不同存储引擎间的差异。 +- 连接层:最上层为客户端层,并非 MySQL 所独有,诸如:连接处理、授权认证、安全等功能均在这一层处理。 +- 中间层:MySQL 大多数核心服务均在中间这一层,包括查询解析、分析、优化、缓存、内置函数(比如:时间、数学、加密等函数)。所有的跨存储引擎的功能也在这一层实现:存储过程、触发器、视图等。 +- 存储引擎层:最下层为存储引擎,其负责 MySQL 中的数据存储和提取。和 Linux 中的文件系统类似,每种存储引擎都有其优势和劣势。中间的服务层通过 API 与存储引擎通信,这些 API 接口屏蔽了不同存储引擎间的差异。 +MySQL Server 是一个单进程多线程的服务程序,在 MySQL Server 上 用 ps -ef | grep mysqld 就能看到其系统进程 ID 了。 -MySQL Server是一个单进程多线程的服务程序,在 MySQL Server上 用 ps -ef | grep mysqld 就能看到其系统进程ID了。 +MySQL 请求处理流程 -MySQL请求处理流程 - -1. 客户端向MySQL服务器发送一条查询请求。 +1. 客户端向 MySQL 服务器发送一条查询请求。 2. 服务器首先检查查询缓存,如果命中缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。 -3. 服务器进行SQL解析、预处理、再由优化器生成对应的执行计划。 -4. MySQL根据执行计划,调用存储引擎的API来执行查询。 +3. 服务器进行 SQL 解析、预处理、再由优化器生成对应的执行计划。 +4. MySQL 根据执行计划,调用存储引擎的 API 来执行查询。 5. 将结果返回给客户端,同时缓存查询结果。 每一个客户端发起一个新的请求都由服务器端的 **连接/线程处理工具** 负责接收客户端的请求,并在服务端内存中开辟一个新的内存空间,生成一个新的线程。 @@ -27,13 +26,11 @@ MySQL请求处理流程 综上所述:用户发起请求,连接/线程处理器开辟内存空间,开始提供查询的机制。 -用户总是希望MySQL能够获得更高的查询性能,最好的办法是弄清楚MySQL是如何优化和执行查询的。 - -一旦理解了这一点,就会发现:很多的查询优化工作实际上就是遵循一些原则让MySQL的优化器能够按照预想的合理方式运行而已。 +用户总是希望 MySQL 能够获得更高的查询性能,最好的办法是弄清楚 MySQL 是如何优化和执行查询的。 +一旦理解了这一点,就会发现:很多的查询优化工作实际上就是遵循一些原则让 MySQL 的优化器能够按照预想的合理方式运行而已。 - -以MySQL为例,数据库在执行`SQL`语句时,需要经历7个步骤: +以 MySQL 为例,数据库在执行`SQL`语句时,需要经历 7 个步骤: - **词法分析**:将`SQL`语句分解成一个个`token`(关键字、标识符、运算符,常量等),然后对`token`进行分类和解析,生成相应的数据结构。 @@ -49,30 +46,25 @@ MySQL请求处理流程 - **返回数据**:将执行结果返回给客户端,比如查询结果集或操作结果。 -在这里,我们粗暴的把执行过程理解成两步,即:先编译`SQL`语法结构(1-3步),再执行SQL语句(4-7步)。 +在这里,我们粗暴的把执行过程理解成两步,即:先编译`SQL`语法结构(1-3 步),再执行 SQL 语句(4-7 步)。 一个查询语句在不同的阶段,生成的树是不同的,这些树的顺序应该是先生成语法树,然后得到查询树,最终得到计划树,计划树就是我们说的执行计划。 查询树就是查询优化器的输入,经过逻辑优化和物理优化,最终产生一颗最优的计划树,而我们要做的就会看看查询优化器是如何产生这棵最优的计划树的。 -正常情况下,用户输入的参数会直接参与SQL语法的编译,而预编译则是先构建语法树,确定SQL语法结构以后,再拼接用户的参数。 - - - +正常情况下,用户输入的参数会直接参与 SQL 语法的编译,而预编译则是先构建语法树,确定 SQL 语法结构以后,再拼接用户的参数。 ### **客户端/服务端通信协议** - 一般来说,不需要去理解 MySQL 通信协议的内部实现细节,只需要大致理解通信协议是如何工作的。 -MySQL客户端/服务端 **通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。** +MySQL 客户端/服务端 **通信协议是“半双工”的:在任一时刻,要么是服务器向客户端发送数据,要么是客户端向服务器发送数据,这两个动作不能同时发生。** 这种协议让 MySQL 通信简单快速, 但是也从很多地方限制了 MySQL。一个明显的限制是, 这意味着没法进行流量控制。 一旦一端开始发送消息,另一端要接收完整个消息才能响应它,所以我们无法也无须将一个消息切成小块独立发送,也没有办法进行流量控制。 - -**客户端提交一个SQL请求给服务器时是直接用一个单独的数据包发送的,所以当查询语句很长的时候,比如一次性提交插入大量数据时,需要注意控制 `max_allowed_packet` 参数。** +**客户端提交一个 SQL 请求给服务器时是直接用一个单独的数据包发送的,所以当查询语句很长的时候,比如一次性提交插入大量数据时,需要注意控制 `max_allowed_packet` 参数。** ```shell # max_allowed_packet表示服务器所能处理的请求包的最大值。默认是64MB,最大值是1GB。MySQL 8.0单个packet可以允许的最大值是1GB @@ -89,59 +81,52 @@ max_allowed_packet=64MB ``` -MySQL是C/S结构,client与server之间的通信需要遵循约定的规则,C/S之间通信的最小单元就是packet,通信约定的规则就是packet的结构。 +MySQL 是 C/S 结构,client 与 server 之间的通信需要遵循约定的规则,C/S 之间通信的最小单元就是 packet,通信约定的规则就是 packet 的结构。 一个传输的包(a communication packet )代表着: -- 客户端发送到mysql 服务端的单个SQL STATEMENT +- 客户端发送到 mysql 服务端的单个 SQL STATEMENT - 服务端发送到客户端的单行数据 -- master发往slave的一个binary log event +- master 发往 slave 的一个 binary log event 但是需要注意的是,如果查询实在是太大,服务端会拒绝接收更多数据并抛出异常。当服务器收到大于 max_allowed_packet 字节的信息包时,将发"信息包过大"错误,并关闭连接。 于某些客户端,如果通信信息包过大,在执行查询期间,可能会遇到"丢失与 MySQL 服务器的连接"错误。 - 与之相反的是,服务器响应给用户的数据通常会很多,由多个数据包组成。当服务器响应客户端请求时,客户端必须完整的接收整个返回结果,而不能简单的只取前面几条结果,然后让服务器停止发送。 这种情况下,客户端若接收完整的结果,然后取前面几条需要的结果,或者接收完几条结果后就"粗暴"地断开连接,都不是好主意。 因而在实际开发中,**尽量保持查询简单且只返回必需的数据,减小通信间数据包的大小和数量是一个非常好的习惯**。 -**这也是查询中尽量避免使用SELECT * 以及加上LIMIT限制的原因之一。** - ->> 引自《高性能 MySQL》。 +**这也是查询中尽量避免使用 SELECT \* 以及加上 LIMIT 限制的原因之一。** +> > 引自《高性能 MySQL》。 当客户端从服务器取数据时,看起来是一个拉数据的过程,但实际上是 MySQL 在向客户端推送数据的过程。客户端不断地接收从服务器推送的数据,客户端也没法让服务器停下来。 - - -InnoDB的数据是保存在主键索引上的,所以全表扫描实际上是直接扫描表的主键索引,对于单表超过系统内存的情况,查询会不会把数据都读到内存中导致系统内存耗尽呢? +InnoDB 的数据是保存在主键索引上的,所以全表扫描实际上是直接扫描表的主键索引,对于单表超过系统内存的情况,查询会不会把数据都读到内存中导致系统内存耗尽呢? **显然不会,实际上服务端并不需要保存一个完整的结果集**。取数据和发数据的流程是这样的: -- 获取一行,写到net_buffer中。这块内存的大小是由参数net_buffer_length定义的,默认是16k。 - -- 重复获取行,直到net_buffer写满,调用网络接口发出去。 +- 获取一行,写到 net_buffer 中。这块内存的大小是由参数 net_buffer_length 定义的,默认是 16k。 -- 如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer。 +- 重复获取行,直到 net_buffer 写满,调用网络接口发出去。 -- 如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。 +- 如果发送成功,就清空 net_buffer,然后继续取下一行,并写入 net_buffer。 -一个查询在发送过程中,占用的MySQL内部的内存最大就是net_buffer_length这么大,并不会达到200G; +- 如果发送函数返回 EAGAIN 或 WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送。 -socket send buffer 也不可能达到200G(默认定义/proc/sys/net/core/wmem_default),如果 **socket send buffer** 被写满,就会暂停读数据的流程。 +一个查询在发送过程中,占用的 MySQL 内部的内存最大就是 net_buffer_length 这么大,并不会达到 200G; -也就是说,MySQL 是 **边读边发的**,这个概念很重要。这就意味着,如果客户端接收得慢,会导致MySQL服务端由于结果发不出去,这个事务的执行时间变长。 +socket send buffer 也不可能达到 200G(默认定义/proc/sys/net/core/wmem_default),如果 **socket send buffer** 被写满,就会暂停读数据的流程。 +也就是说,MySQL 是 **边读边发的**,这个概念很重要。这就意味着,如果客户端接收得慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间变长。 - -### MySQL插件体系 +### MySQL 插件体系 MySQL 现在很多模块都是通过`plugin`的方式连接到 MySQL 核心中的,除了大家熟悉的存储引擎都是`Plugin`之外,MySQL 还支持其他类型的`plugin`。 - ```SQL select * from information_schema.plugins -- where plugin_name like "%daemon%"; @@ -154,22 +139,17 @@ select * from information_schema.plugins -- where plugin_name like "%daemon%"; #### 克隆插件 - - ### **连接器/连接管理** - 连接器负责跟客户端建立连接、获取权限、维持和管理连接。 客户端使用命令行登陆时连接命令:`mysql -h${ip} -P${port} -u${user} -p${password}` -在完成TCP三次握手之后,连接器就要开始认证身份进行账密校验,校验通过之后,**连接器会到权限表里查询拥有的权限之后在这个连接里的权限判断,SQL执行的权限都依赖于此时读取的权限。** +在完成 TCP 三次握手之后,连接器就要开始认证身份进行账密校验,校验通过之后,**连接器会到权限表里查询拥有的权限之后在这个连接里的权限判断,SQL 执行的权限都依赖于此时读取的权限。** 这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,**只有再新建的连接才会使用新的权限设置**。 - -MySQL客户端,当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。 - +MySQL 客户端,当使用默认参数连接的时候,MySQL 客户端会提供一个本地库名和表名补全的功能。 为了实现这个功能,客户端在连接成功后,需要多做一些操作: @@ -183,7 +163,6 @@ MySQL客户端,当使用默认参数连接的时候,MySQL 客户端会提供 这里自动补全的效果就是,你在输入库名或者表名的时候,输入前缀,可以使用 Tab 键自动补全表名或者显示提示。实际使用中,如果你自动补全功能用得并不多,建议每次使用的时候都默认加 -A。 - MySQL 客户端发送请求后,接收服务端返回结果的方式有两种: 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。如果你用 API 开发,对应的就是 mysql_store_result 方法。 @@ -203,57 +182,53 @@ MySQL 客户端默认采用第一种方式,而如果加上–quick 参数, 第三点,是不会把执行命令记录到本地的命令历史文件。 –quick 参数的意思,是让客户端变得更快。 +#### MySQL 连接线程模型 -#### MySQL连接线程模型 - -MySQL是一个 **单进程多线程** 的软件,启动一个 MySQL 实例,操作系统中会使用 mysqld 这个可执行文件来启动一个mysqld进程。 +MySQL 是一个 **单进程多线程** 的软件,启动一个 MySQL 实例,操作系统中会使用 mysqld 这个可执行文件来启动一个 mysqld 进程。 -mysqld 通过创建多个线程来服务于不同的用户连接。通常情况下,随着用户连接数的增加,MySQL内部用于处理用户连接的线程也会同步的增加,在一定范围内,增加用户并发连接,对提高系统的吞吐量有一定的帮助,然而用户并发连接数超过某个阈值,MySQL的性能反而会降低。 +mysqld 通过创建多个线程来服务于不同的用户连接。通常情况下,随着用户连接数的增加,MySQL 内部用于处理用户连接的线程也会同步的增加,在一定范围内,增加用户并发连接,对提高系统的吞吐量有一定的帮助,然而用户并发连接数超过某个阈值,MySQL 的性能反而会降低。 -每个线程至少有两个唯一标识符: 一个是操作系统线程ID, 另一个是MySQL内部线程ID。 +每个线程至少有两个唯一标识符: 一个是操作系统线程 ID, 另一个是 MySQL 内部线程 ID。 -- THREAD_ID: MySQL内部的线程ID -- PROCESSLIST_ID: 连接ID,每个前台线程都有一个指定的PROCESSLIST_ID连接标识符。我们平时 kill processlist_id 就是这个 -- THREAD_OS_ID: 操作系统线程ID +- THREAD_ID: MySQL 内部的线程 ID +- PROCESSLIST_ID: 连接 ID,每个前台线程都有一个指定的 PROCESSLIST_ID 连接标识符。我们平时 kill processlist_id 就是这个 +- THREAD_OS_ID: 操作系统线程 ID -上述ID均可以通过 `performance_schema.threads` 查询。对于操作系统线程ID可以通过系统相关工具查看, 如在`Linux`系统中可使用`ps -eLf`命令查看。 +上述 ID 均可以通过 `performance_schema.threads` 查询。对于操作系统线程 ID 可以通过系统相关工具查看, 如在`Linux`系统中可使用`ps -eLf`命令查看。 我们打开 htop 或 top 时,如果查看以线程方式查看,就可以看到很多 `mysqld` 线程。这些就是用于处理客户端连接而创建的线程。 -### MySQL线程管理 +### MySQL 线程管理 -MySQL是一个单进程多线程的程序,根据 `type` 可以分为 `BACKGROUND` 和 `FOREGROUND` 线程。可通过 `performance_schema.threads` 表查看。 +MySQL 是一个单进程多线程的程序,根据 `type` 可以分为 `BACKGROUND` 和 `FOREGROUND` 线程。可通过 `performance_schema.threads` 表查看。 使用 htop 也可以看到 MySQL 服务的各个线程。 #### 后台线程 -| | | | -| ------------------------------- | ------------------------------------------------------------ | ---- | -| thread/innodb/srv_master_thread | srv_master_thread是最重要的后台主线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO页的回收等。 | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | -| | | | - - - +| | | | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | --- | +| thread/innodb/srv_master_thread | srv_master_thread 是最重要的后台主线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO 页的回收等。 | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | +| | | | #### 前台线程 -MySQL处理用户连接的都是前台线程。可以使用如下语句查看: +MySQL 处理用户连接的都是前台线程。可以使用如下语句查看: ```SQL select * from `performance_schema`.threads where type='FOREGROUND'; @@ -261,19 +236,17 @@ select * from `performance_schema`.threads where type='FOREGROUND'; SELECT * FROM `information_schema`.PROCESSLIST; ``` -MySQL内部处理用户连接的线程调度方式由 `thread_handling` 参数控制。严格来说有三种: +MySQL 内部处理用户连接的线程调度方式由 `thread_handling` 参数控制。严格来说有三种: - `no-threads`,单线程处理所有用户连接,一般在调试时使用。 - `one-thread-per-connection` , 多线程处理用户连接,一个线程对应一个用户连接,也是 `MySQL Community Server` 默认的连接处理方式。主要由 `thread_handling` 参数配置。 -- `Thread pool`, 在Percona,MariaDB,Oracle MySQL 企业版,[阿里云polarDB](https://help.aliyun.com/document_detail/206413.htm),[腾讯数据库](https://cloud.tencent.com/document/product/236/48851)中提中,提供了 **线程池** 特性。 +- `Thread pool`, 在 Percona,MariaDB,Oracle MySQL 企业版,[阿里云 polarDB](https://help.aliyun.com/document_detail/206413.htm),[腾讯数据库](https://cloud.tencent.com/document/product/236/48851)中提中,提供了 **线程池** 特性。 -在 `one-thread-per-connection` 情况下,每个连接分配一个线程,当客户端和MySQL服务器建立TCP连接之后,MySQL服务器就会给这个连接分配一个线程,当这个连接收到SQL时,对应的线程就执行这个SQL,而当SQL执行结束后,这个线程就去Sleep,等待客户端的新请求。这个线程会一直存活,直到客户端退出登录或线程超时断开(由wait_timeout或interactive_timeout参数控制,),并关闭连接,这个线程才会退出(或者进入MySQL的ThreadCache)。 +在 `one-thread-per-connection` 情况下,每个连接分配一个线程,当客户端和 MySQL 服务器建立 TCP 连接之后,MySQL 服务器就会给这个连接分配一个线程,当这个连接收到 SQL 时,对应的线程就执行这个 SQL,而当 SQL 执行结束后,这个线程就去 Sleep,等待客户端的新请求。这个线程会一直存活,直到客户端退出登录或线程超时断开(由 wait_timeout 或 interactive_timeout 参数控制,),并关闭连接,这个线程才会退出(或者进入 MySQL 的 ThreadCache)。 超时参数,详见 https://zhuanlan.zhihu.com/p/82554484 - - -MySQL 连接方式有很多种,区分Unix系统 和 Windows 系统以及通用的连接方式,在这里仅说两种方式: +MySQL 连接方式有很多种,区分 Unix 系统 和 Windows 系统以及通用的连接方式,在这里仅说两种方式: - 一种为 `unix domain socket` @@ -288,8 +261,6 @@ tcp/ip:mysql -h127.0.0.1 -uroot -p 建立连接也分为两类:**短连接**和**长连接**: - - **短连接** 所谓短连接就是指应用程序和数据库通信完毕之后连接关闭。这种连接每次的操作就是: @@ -300,27 +271,23 @@ tcp/ip:mysql -h127.0.0.1 -uroot -p 这样做的问题是: -1. 频繁的建立 / 释放连接对数据库来说增加了系统负担,频繁创建/销毁线程**增加数据库服务器CPU上下文切换开销,影响数据库服务器性能。**; +1. 频繁的建立 / 释放连接对数据库来说增加了系统负担,频繁创建/销毁线程**增加数据库服务器 CPU 上下文切换开销,影响数据库服务器性能。**; -2. 应用程序每次操作数据库的过程将会变得很慢,**tcp三次握手四次断开要时间开销的**; +2. 应用程序每次操作数据库的过程将会变得很慢,**tcp 三次握手四次断开要时间开销的**; 3. 应用系统每次建立连接都要占用一个端口,频繁的建立/释放,每个被释放的连接在发出释放请求之后并不是马上就执行,必须经历一个 FIN 阶段的等待直到确认为止。所以在每秒几千次数据库请求的时候,应用服务器端口很有可能被消耗完。 - - **长连接** 长连接即在建立连接后一直打开,直到应用程序关闭才释放。使用长连接的好处是减少每次创建连接带来的开销。 对于客户端来说维持长连接的好处不言自明,但是对于数据库服务器来说,过多的长连接则是灾难。 -如果滥用长连接的话,可能会使用过多的MySQL服务器连接。现代的操作系统可以拥有几千个MySQL连接,但很有可能绝大部分都是睡眠(sleep)状态的,这样的工作方式不够高效,而且连接占据内存,也会导致内存的浪费。 - -MYSQL的TCP连接支持长连接,所以每次操作完数据库,可以不必直接关掉连接,而是等待下次使用的时候在复用这个连接。 - -所有的Socket长连接都是通过TCP自带的ping来维持心跳(TCP保活),从而保持连接状态,而我们熟悉的`websocket`,也正是通过TCP的心跳来维持连接不被中断。 +如果滥用长连接的话,可能会使用过多的 MySQL 服务器连接。现代的操作系统可以拥有几千个 MySQL 连接,但很有可能绝大部分都是睡眠(sleep)状态的,这样的工作方式不够高效,而且连接占据内存,也会导致内存的浪费。 +MYSQL 的 TCP 连接支持长连接,所以每次操作完数据库,可以不必直接关掉连接,而是等待下次使用的时候在复用这个连接。 +所有的 Socket 长连接都是通过 TCP 自带的 ping 来维持心跳(TCP 保活),从而保持连接状态,而我们熟悉的`websocket`,也正是通过 TCP 的心跳来维持连接不被中断。 **连接池** @@ -339,23 +306,18 @@ MYSQL的TCP连接支持长连接,所以每次操作完数据库,可以不必 [参考](https://xmmarlowe.github.io/2021/05/21/%E6%95%B0%E6%8D%AE%E5%BA%93/MySQL%E9%95%BF%E8%BF%9E%E6%8E%A5%E3%80%81%E7%9F%AD%E8%BF%9E%E6%8E%A5%E3%80%81%E8%BF%9E%E6%8E%A5%E6%B1%A0/) -**对此,研发工程师、系统运维工程师、DBA需要保持沟通,确定合理的连接策略,千万不要不假思索就采用长连接。** +**对此,研发工程师、系统运维工程师、DBA 需要保持沟通,确定合理的连接策略,千万不要不假思索就采用长连接。** - - - -MySQL的最大连接数由 `max_connections` 参数控制,在5.7和8.0中默认是151,最大可以达到16384(2^14)。 +MySQL 的最大连接数由 `max_connections` 参数控制,在 5.7 和 8.0 中默认是 151,最大可以达到 16384(2^14)。 对于海量连接的数据库,如果设置的太小,连接满了之后后面的新连接就会报`too many connections`。 +MySQL Server 其实默认允许的最大客户端连接数为 max_connections + 1 ,这其中额外可以登陆的 1 一个连接仅仅允许拥有 super 权限的用户进行连接。 -MySQL Server其实默认允许的最大客户端连接数为 max_connections + 1 ,这其中额外可以登陆的1一个连接仅仅允许拥有super权限的用户进行连接。 +MySQL 如此设计其实是为了当数据库出现连接数打满的情况下,可以使用同时拥有 super、process 权限的高权限数据库账号登陆数据库,将问题会话或者一些空闲连接进行 kill,紧急处理故障。所以通常避免让业务账号具备 super 和 process 的管理权限。 -MySQL如此设计其实是为了当数据库出现连接数打满的情况下,可以使用同时拥有super、process权限的高权限数据库账号登陆数据库,将问题会话或者一些空闲连接进行kill,紧急处理故障。所以通常避免让业务账号具备super和process的管理权限。 - - -在MySQL8.0里,则引入了admin port的概念,顾名思义,就是单独开一个端口给管理员用,该特性从8.0.14开始引入。这个需要单独设置。 -可以说这是个对运维非常有用,关键时候可以救命的特性。这个feature由facebook贡献给上游。主要包括以下几个参数设置: +在 MySQL8.0 里,则引入了 admin port 的概念,顾名思义,就是单独开一个端口给管理员用,该特性从 8.0.14 开始引入。这个需要单独设置。 +可以说这是个对运维非常有用,关键时候可以救命的特性。这个 feature 由 facebook 贡献给上游。主要包括以下几个参数设置: ```shell @@ -371,19 +333,11 @@ create_admin_listener_thread=1 mysql -u root -P 33062 --protocol tcp -p'zhtj6668182' ``` - - - - - **基于此,数据库账号权限一定要做好明确的规划,业务账号仅仅拥有对应业务数据库的读写权限、高权限数据库账号用于运维管理。** - MySQL 官网给出了一个最大连接数推荐计算方式,`Max_used_connections / max_connections * 100% ≈ 85%` - - -MySQL的 **状态变量** 显示MySQL服务实例的运行状态信息,这些状态信息是动态的,包括MySQL服务器连接的会话状态、变量信息等。默认情况下状态变量都是以大写字母开头。 +MySQL 的 **状态变量** 显示 MySQL 服务实例的运行状态信息,这些状态信息是动态的,包括 MySQL 服务器连接的会话状态、变量信息等。默认情况下状态变量都是以大写字母开头。 ```shell show status; @@ -391,22 +345,22 @@ show session status; show global status; ``` -| 状态变量 | 含义 | -| --------------------------------- | ------------------------------------------------------------ | -| Connections | 状态变量:MySQL服务从初始化开始成功建立连接的数量,该值不断累加 | -| Max_used_connections | 状态变量:MySQL服务从启动开始,同一时刻并发连接的最大值,如果该值很大,则有可能系统并发较高,可以考虑调大max_connections | -| Connection_errors_max_connections | 状态变量:当MySQL的最大并发连接数超过设置的max_connections变量的值,被拒绝的次数会记录到这个状态值里 | -| Threads_connected | 状态变量: MySQL server当前打开的连接数 | +| 状态变量 | 含义 | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| Connections | 状态变量:MySQL 服务从初始化开始成功建立连接的数量,该值不断累加 | +| Max_used_connections | 状态变量:MySQL 服务从启动开始,同一时刻并发连接的最大值,如果该值很大,则有可能系统并发较高,可以考虑调大 max_connections | +| Connection_errors_max_connections | 状态变量:当 MySQL 的最大并发连接数超过设置的 max_connections 变量的值,被拒绝的次数会记录到这个状态值里 | +| Threads_connected | 状态变量: MySQL server 当前打开的连接数 | ### 控制参数 -| 配置 | 含义 | -| -------------------- | ------------------------------------------------------------ | -| max_connections | 配置参数:MySQL server层面限制**总的所有账号一起**最大的可连接的数量,默认151,最大值为100000 | -| max_user_connections | 配置参数:代表允许单个用户的连接数最大值,即并发值。默认为0,表示不限制 | -| wait_timeout | 配置参数:即MySQL长连接(非交互式)的最大生命时长,默认是8小时,根据业务特点配置 | -| interactive_timeout | 配置参数:即MySQL长连接长连接(交互式)的最大生命时长,默认是8小时,根据业务特点配置 | -| connect_timeout | 配置参数:获取MySQL连接是多次握手的结果,除了用户名和密码的匹配校验外,还有IP->HOST->DNS->IP验证,任何一步都可能因为网络问题导致线程阻塞。为了防止线程浪费在不必要的校验等待上,超过connect_timeout的连接请求将会被拒绝。默认是10秒 | +| 配置 | 含义 | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| max_connections | 配置参数:MySQL server 层面限制**总的所有账号一起**最大的可连接的数量,默认 151,最大值为 100000 | +| max_user_connections | 配置参数:代表允许单个用户的连接数最大值,即并发值。默认为 0,表示不限制 | +| wait_timeout | 配置参数:即 MySQL 长连接(非交互式)的最大生命时长,默认是 8 小时,根据业务特点配置 | +| interactive_timeout | 配置参数:即 MySQL 长连接长连接(交互式)的最大生命时长,默认是 8 小时,根据业务特点配置 | +| connect_timeout | 配置参数:获取 MySQL 连接是多次握手的结果,除了用户名和密码的匹配校验外,还有 IP->HOST->DNS->IP 验证,任何一步都可能因为网络问题导致线程阻塞。为了防止线程浪费在不必要的校验等待上,超过 connect_timeout 的连接请求将会被拒绝。默认是 10 秒 | #### 线程缓存 @@ -414,13 +368,11 @@ show global status; 缓存的线程数量由 [thread_cache_size](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_thread_cache_size) 大小决定。 -当服务器不断有大量连接创建、关闭的场景下,使用线程缓存能够重用缓存起来的线程,避免了大量连接线程的反复创建销毁带来的CPU上下文切换性能消耗,但是仍然无法解决高连接数带来的线程数过高的问题。 - +当服务器不断有大量连接创建、关闭的场景下,使用线程缓存能够重用缓存起来的线程,避免了大量连接线程的反复创建销毁带来的 CPU 上下文切换性能消耗,但是仍然无法解决高连接数带来的线程数过高的问题。 如果是短连接,适当设置大一点。因为短连接往往需要不停创建,不停销毁,如果大一点,连接线程都处于取用状态,不需要重新创建和销毁,所以对性能肯定是比较大的提升。 对 -于长连接,不能保证连接的稳定性,所以设置这参数还是有一定必要,可能连接池的问题,会导致连接数据库的不稳定性,也会出现频繁的创建和销毁,但这个情况比较少,如果是长连接,可以设置成小一点,一般在50-100左右。 - +于长连接,不能保证连接的稳定性,所以设置这参数还是有一定必要,可能连接池的问题,会导致连接数据库的不稳定性,也会出现频繁的创建和销毁,但这个情况比较少,如果是长连接,可以设置成小一点,一般在 50-100 左右。 ```shell # 查看线程缓存大小 @@ -440,14 +392,12 @@ show status like 'connections' show global status like 'Threads_%'; ``` -| 配置 | 含义 | -| ----------------- | ------------------------------------------------------------ | -| Threads_cached | 状态变量:当前线程池中缓存有多少空闲线程 | -| Threads_connected | 状态变量:当前的连接数 ( 因为一个连接就需要一个线程,所以也可以看成当前被使用的线程数 ) | -| Threads_created | 状态变量:开启以来累计已经创建过的线程总数 | -| Threads_running | 状态变量:当前激活的线程数 ( Threads_connected 中的线程有些可能处于休眠状态 ) | - - +| 配置 | 含义 | +| ----------------- | --------------------------------------------------------------------------------------- | +| Threads_cached | 状态变量:当前线程池中缓存有多少空闲线程 | +| Threads_connected | 状态变量:当前的连接数 ( 因为一个连接就需要一个线程,所以也可以看成当前被使用的线程数 ) | +| Threads_created | 状态变量:开启以来累计已经创建过的线程总数 | +| Threads_running | 状态变量:当前激活的线程数 ( Threads_connected 中的线程有些可能处于休眠状态 ) | #### 主机缓存 @@ -456,69 +406,54 @@ MySQL 服务会在内存中维护 `host cache` ,`host cache` 包含很多关 `host cache` 的内容是公开的,可以用 `select` 语句进行查询,以便诊断当前的连接问题. ``` -select * from performance_schema.host_cache ; +select * from performance_schema.host_cache ; ``` > 提示: > -> host cache仅用于非localhost的tcp连接:对于 loopback 环回地址的tcp连接,或者socket file连接,命名管道,共享内存等。 +> host cache 仅用于非 localhost 的 tcp 连接:对于 loopback 环回地址的 tcp 连接,或者 socket file 连接,命名管道,共享内存等。 > -> 执行flush hosts可以刷新host_cache,刷新后会清除内存中的主机缓存 - +> 执行 flush hosts 可以刷新 host_cache,刷新后会清除内存中的主机缓存 +1. 当有一个新的客户端连接进来时,MySQL Server 会为这个 IP 在 host cache 中建立一个新的记录,包括 IP,主机名和 client lookup validation flag,分别对应 host_cache 表中的 IP,HOST 和 HOST_VALIDATED 这三列。第一次建立连接因为只有 IP,没有主机名,所以 HOST 将设置为 NULL,HOST_VALIDATED 将设置为 FALSE。 +2. MySQL Server 检测 HOST_VALIDATED 的值,如果为 FALSE,它会试图进行**DNS 反向解析**,如果解析成功,它将更新 HOST 的值为主机名,并将 HOST_VALIDATED 值设为 TRUE。如果没有解析成功,判断失败的原因是永久的还是临时的,如果是永久的,则 HOST 的值依旧为 NULL,且将 HOST_VALIDATED 的值设置为 TRUE,后续连接不再进行解析,如果该原因是临时的,则 HOST_VALIDATED 依旧为 FALSE,后续连接会再次进行 DNS 解析。 +解析成功的标志并不只是通过 IP,获取到主机名即可,这只是其中一步,还有一步是通过解析后的主机名来反向解析为 IP,判断该 IP 是否与原 IP 相同,如果相同,才判断为解析成功,才能更新 host cache 中的信息。 -1. 当有一个新的客户端连接进来时,MySQL Server会为这个IP在host cache中建立一个新的记录,包括IP,主机名和client lookup validation flag,分别对应host_cache表中的IP,HOST和HOST_VALIDATED这三列。第一次建立连接因为只有IP,没有主机名,所以HOST将设置为NULL,HOST_VALIDATED将设置为FALSE。 - -2. MySQL Server检测HOST_VALIDATED的值,如果为FALSE,它会试图进行**DNS反向解析**,如果解析成功,它将更新HOST的值为主机名,并将HOST_VALIDATED值设为TRUE。如果没有解析成功,判断失败的原因是永久的还是临时的,如果是永久的,则HOST的值依旧为NULL,且将HOST_VALIDATED的值设置为TRUE,后续连接不再进行解析,如果该原因是临时的,则HOST_VALIDATED依旧为FALSE,后续连接会再次进行DNS解析。 - - - -解析成功的标志并不只是通过IP,获取到主机名即可,这只是其中一步,还有一步是通过解析后的主机名来反向解析为IP,判断该IP是否与原IP相同,如果相同,才判断为解析成功,才能更新host cache中的信息。 - - - -缺点:当有一个新的客户端连接进来时,MySQL Server都要建立一个新的记录,如果DNS解析很慢,无疑会影响性能。如果被允许访问的主机很多,也会影响性能,这个与host_cache_size有关,这个参数是5.6.5引入的。5.6.8之前默认是128,5.6.8之后默认是-1,基于max_connections的值动态调整。所以如果被允许访问的主机很多,基于LRU算法,先前建立的连接可能会被挤掉,这些主机重新进来时,会再次进行DNS查询。 - -优点:通常情况下,主机名是不变的,而IP是多变的。如果一个客户端的IP经常变化,那基于IP的授权将是一个繁琐的过程。因为你很难确定IP什么时候变化。而基于主机名,只需一次授权。而且,基于host cache中的失败信息,可在一定程度上阻止外界的暴力破解攻击。 - +缺点:当有一个新的客户端连接进来时,MySQL Server 都要建立一个新的记录,如果 DNS 解析很慢,无疑会影响性能。如果被允许访问的主机很多,也会影响性能,这个与 host_cache_size 有关,这个参数是 5.6.5 引入的。5.6.8 之前默认是 128,5.6.8 之后默认是-1,基于 max_connections 的值动态调整。所以如果被允许访问的主机很多,基于 LRU 算法,先前建立的连接可能会被挤掉,这些主机重新进来时,会再次进行 DNS 查询。 +优点:通常情况下,主机名是不变的,而 IP 是多变的。如果一个客户端的 IP 经常变化,那基于 IP 的授权将是一个繁琐的过程。因为你很难确定 IP 什么时候变化。而基于主机名,只需一次授权。而且,基于 host cache 中的失败信息,可在一定程度上阻止外界的暴力破解攻击。 [参考](https://www.cnblogs.com/ivictor/p/5311607.html) +### MySQL 线程池 - -### MySQL线程池 - -在线程池方案下,通常在MySQL server服务端实现,MySQL 通过预先创建一定数量的线程,在监听到有新的请求时,线程池直接从现有的线程中分配一个线程来提供服务,服务结束后这个线程不会直接销毁,而是又去处理其他的请求。 +在线程池方案下,通常在 MySQL server 服务端实现,MySQL 通过预先创建一定数量的线程,在监听到有新的请求时,线程池直接从现有的线程中分配一个线程来提供服务,服务结束后这个线程不会直接销毁,而是又去处理其他的请求。 这样就避免了线程和内存对象频繁创建和销毁,减少了上下文切换,提高了资源利用率,从而在一定程度上提高了系统的性能和稳定性。 -**线程池技术限制了并发线程数,相当于限制了MySQL的runing线程数,无论系统目前有多少连接或者请求,超过最大设置的线程数的都需要排队,让系统保持高性能水平,从而防止DB出现雪崩,对底层DB起到保护作用。** - -> Thread pool,在Percona,MariaDB,Oracle MySQL企业版,以及阿里云polarDB中提中,提供了**线程池**特性。 - -### MySQL连接池 +**线程池技术限制了并发线程数,相当于限制了 MySQL 的 runing 线程数,无论系统目前有多少连接或者请求,超过最大设置的线程数的都需要排队,让系统保持高性能水平,从而防止 DB 出现雪崩,对底层 DB 起到保护作用。** -MySQL连接池,连接池通常实现在client端,是指应用(客户端)预先创建一定的连接,利用这些连接服务于客户端所有的DB请求。如果某一个时刻,空闲的连接数小于DB的请求数,则需要将请求排队,等待空闲连接处理。 +> Thread pool,在 Percona,MariaDB,Oracle MySQL 企业版,以及阿里云 polarDB 中提中,提供了**线程池**特性。 -通过连接池的连接复用,避免连接的频繁创建和释放,从而减少请求的平均响应时间,并且在请求繁忙时,通过请求排队,可以缓冲应用对DB的冲击。常见的MySQL连接池Tomcat、WildFly(JBoss)、 c3p0、 Druid等。 +### MySQL 连接池 -在很多公司,有不少程序员写代码,懒得用数据库连接池,所以就在每次数据操作时,临时连接数据库,使用完后直接关闭,这显然不好。 +MySQL 连接池,连接池通常实现在 client 端,是指应用(客户端)预先创建一定的连接,利用这些连接服务于客户端所有的 DB 请求。如果某一个时刻,空闲的连接数小于 DB 的请求数,则需要将请求排队,等待空闲连接处理。 +通过连接池的连接复用,避免连接的频繁创建和释放,从而减少请求的平均响应时间,并且在请求繁忙时,通过请求排队,可以缓冲应用对 DB 的冲击。常见的 MySQL 连接池 Tomcat、WildFly(JBoss)、 c3p0、 Druid 等。 -[MySQL线程池与连接池](https://fengzhao.notion.site/MySQL-c5d5f4871cca4ea3ac434fa826095a1c) - +在很多公司,有不少程序员写代码,懒得用数据库连接池,所以就在每次数据操作时,临时连接数据库,使用完后直接关闭,这显然不好。 +[MySQL 线程池与连接池](https://fengzhao.notion.site/MySQL-c5d5f4871cca4ea3ac434fa826095a1c) ### 线程处理和线程事务 ```sql -- 查看所有连接线程,其中ID为线程ID -SELECT * FROM information_schema.`PROCESSLIST` +SELECT * FROM information_schema.`PROCESSLIST` -- 在MySQL中有两个kill命令: -- 一个是kill query +线程id,表示终止这个线程中正在执行的语句; @@ -542,7 +477,6 @@ SELECT * FROM information_schema.INNODB_TRX; 收到 kill 以后,线程做什么? - 在全局锁和表锁中,当对一个表做增删改查操作时,会在表上加 MDL 读锁。所以,session B 虽然处于 blocked 状态,但还是拿着一个 MDL 读锁的。 如果线程被 kill 的时候,就直接终止,那之后这个 MDL 读锁就没机会被释放了。这样看来,kill 并不是马上停止的意思,而是告诉执行线程说,这条语句已经不需要继续执行了,可以开始“执行停止的逻辑了”。 @@ -555,40 +489,38 @@ SELECT * FROM information_schema.INNODB_TRX; 3. 语句从开始进入终止逻辑,到终止逻辑完全完成,是有一个过程的。 - - ### **查询缓存** MySQL 的查询,主要处理过程都是从硬盘中读取数据加载到内存,然后通过网络发给客户端, -MySQL 以前有一个查询缓存 Query Cache,从 MySQL8.0 开始,不再使用这个查询缓存,随着技术的进步,经过时间的考验,MySQL的工程团队发现启用缓存的好处并不多。所以在 8.0 中移除了这个特性。 +MySQL 以前有一个查询缓存 Query Cache,从 MySQL8.0 开始,不再使用这个查询缓存,随着技术的进步,经过时间的考验,MySQL 的工程团队发现启用缓存的好处并不多。所以在 8.0 中移除了这个特性。 -在解析一个查询语句前,如果查询缓存是打开的,那么MySQL会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。 +在解析一个查询语句前,如果查询缓存是打开的,那么 MySQL 会检查这个查询语句是否命中查询缓存中的数据。如果当前查询恰好命中查询缓存,在检查一次用户权限后直接返回缓存中的结果。 这种情况下,查询不会被解析,也不会生成执行计划,更不会执行。 -MySQL将缓存存放在一个引用表(不要理解成table,可以认为是类似于HashMap的数据结构),通过一个哈希值索引,这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。 +MySQL 将缓存存放在一个引用表(不要理解成 table,可以认为是类似于 HashMap 的数据结构),通过一个哈希值索引,这个哈希值通过查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息计算得来。 -**所以两个查询在任何字符上的不同(例如:空格、注释),都会导致缓存不会命中。如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql库中的系统表,其查询结果都不会被缓存。** +**所以两个查询在任何字符上的不同(例如:空格、注释),都会导致缓存不会命中。如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、mysql 库中的系统表,其查询结果都不会被缓存。** **比如函数 NOW() 或者 CURRENT_DATE() 会因为不同的查询时间,返回不同的查询结果。再比如包含 CURRENT_USER 或者 CONNECION_ID() 的查询语句会因为不同的用户而返回不同的结果,将这样的查询结果缓存起来没有任何的意义。** 既然是缓存,就会失效,那查询缓存何时失效呢? -MySQL的查询缓存系统会跟踪查询中涉及的每个表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 +MySQL 的查询缓存系统会跟踪查询中涉及的每个表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。 -正因为如此,在任何的写操作时,MySQL必须将对应表的所有缓存都设置为失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统消耗,甚至导致系统僵死一会儿。 +正因为如此,在任何的写操作时,MySQL 必须将对应表的所有缓存都设置为失效。如果查询缓存非常大或者碎片很多,这个操作就可能带来很大的系统消耗,甚至导致系统僵死一会儿。 而且查询缓存对系统的额外消耗也不仅仅在写操作,读操作也不例外: -1. 任何的查询语句在开始之前都必须经过检查,即使这条SQL语句永远不会命中缓存 +1. 任何的查询语句在开始之前都必须经过检查,即使这条 SQL 语句永远不会命中缓存 2. 如果查询结果可以被缓存,那么执行完成后,会将结果存入缓存,也会带来额外的系统消耗 首先,查询缓存的效果取决于缓存的命中率,只有命中缓存的查询效果才能有改善,因此无法预测其性能。 其次,查询缓存的另一个大问题是它受到单个互斥锁的保护。在具有多个内核的服务器上,大量查询会导致大量的互斥锁争用。 -通过基准测试发现,大多数工作负载最好禁用查询缓存(5.6的默认设置):query_cache_type = 0 +通过基准测试发现,大多数工作负载最好禁用查询缓存(5.6 的默认设置):query_cache_type = 0 如果你认为会从查询缓存中获得好处,请按照实际情况进行测试。 @@ -598,40 +530,36 @@ MySQL的查询缓存系统会跟踪查询中涉及的每个表,如果这些表 ==**最后的忠告是不要轻易打开查询缓存,特别是写密集型应用。**== -如果你实在是忍不住,可以将query_cache_type设置为DEMAND,这时只有加入SQL_CACHE的查询才会走缓存,其他查询则不会,这样可以非常自由地控制哪些查询需要被缓存。 +如果你实在是忍不住,可以将 query_cache_type 设置为 DEMAND,这时只有加入 SQL_CACHE 的查询才会走缓存,其他查询则不会,这样可以非常自由地控制哪些查询需要被缓存。 对于一些热点数据,现在比较流行的做法是引入外部的缓存中间件,比如 redis 等,这个以后展开再讲。 ### **语法解析和预处理** -MySQL通过关键字将SQL语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。 - -比如SQL中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据MySQL规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。 - +MySQL 通过关键字将 SQL 语句进行解析,并生成一颗对应的解析树。这个过程解析器主要通过语法规则来验证和解析。 -SQL解析与优化是属于编译器编译原理方面的知识,与C语言这类编程语言的解析上是类似的。SQL解析主要包含:词法分析、语义语法分析、优化和执行代码生成。 +比如 SQL 中是否使用了错误的关键字或者关键字的顺序是否正确等等。预处理则会根据 MySQL 规则进一步检查解析树是否合法。比如检查要查询的数据表和数据列是否存在等等。 -- 词法分析Lexical Analysis:将用户输入的SQL语句拆解成单词(Token)序列,并识别出关键字、标识、常量等。 +SQL 解析与优化是属于编译器编译原理方面的知识,与 C 语言这类编程语言的解析上是类似的。SQL 解析主要包含:词法分析、语义语法分析、优化和执行代码生成。 -- 语法分析Syntax Analysis:分析器对词法分析器解析出来的单词(Token)序列在语法上是否满足SQL语法规则。构建一颗语法分析树。 +- 词法分析 Lexical Analysis:将用户输入的 SQL 语句拆解成单词(Token)序列,并识别出关键字、标识、常量等。 -- 语义分析Semantic Analysis:语义分析是SQL解析过程的一个逻辑阶段,主要任务是在语法正确的基础上进行上下文有关性质的审查,在SQL解析过程中该阶段完成表名、操作符、类型等元素的合法性判断,同时检测语义上的二义性。 +- 语法分析 Syntax Analysis:分析器对词法分析器解析出来的单词(Token)序列在语法上是否满足 SQL 语法规则。构建一颗语法分析树。 +- 语义分析 Semantic Analysis:语义分析是 SQL 解析过程的一个逻辑阶段,主要任务是在语法正确的基础上进行上下文有关性质的审查,在 SQL 解析过程中该阶段完成表名、操作符、类型等元素的合法性判断,同时检测语义上的二义性。 -SQL解析由词法分析和语法、语义分析两个部分组成。词法分析主要是把输入转化成若干个Token,其中Token包含key和非key。比如,一个简单的SQL如下所示: +SQL 解析由词法分析和语法、语义分析两个部分组成。词法分析主要是把输入转化成若干个 Token,其中 Token 包含 key 和非 key。比如,一个简单的 SQL 如下所示: ```SQL SELECT age FROM user ``` - - ### **查询优化** - 经过前面的步骤生成的语法树被认为是合法的了,并且由优化器将其转化成查询计划。 +#### 优化器概述 **为什么数据库要进行查询优化?**” @@ -641,46 +569,53 @@ SELECT age FROM user C 语言是过程化语言,已经指定好了需要执行的每一个步骤;但 SQL 是描述性语言,只指定了 WHAT,而没有指定 HOW 。这样它的优化空间就大了。 - -查询优化的输入为查询树,对查询树依次从**逻辑**和**物理**两个层面进行优化,最终输出是查询的物理执行计划。 +查询优化的输入为查询树,对查询树依次从 **逻辑** 和 **物理** 两个层面进行优化,最终输出是查询的物理执行计划。 查询优化主要有三个重要的过程: - **逻辑优化** 逻辑优化的输入是查询树,输出是经过逻辑优化后的查询树。逻辑优化是找出与查询等价但执行效率更高的关系代数表达式。 - 实现逻辑优化的方法主要是**查询重写**,根据特定的重写规则对查询树做逻辑等价变化。 - + 实现逻辑优化的方法主要是 **查询重写** ,根据特定的重写规则对查询树做逻辑等价变化。 - **代价估计** 经过逻辑优化过程,可以得到基于等价规则重写过的逻辑执行计划。将逻辑执行执行转换为实际执行的物理计划前,还需要确认各种具体实现方式: 比如,两表连接时应该具体采用何种连接算法,多表连接时还需要确定表之间的连接顺序。 - **物理优化** 物理层面的优化则是枚举各种物理执行计划,根据代价估计模型择出代价最小的物理执行计划. - +关系演算是纯描述性的语言,关系代数包含了一些基本的关系操作,SQL 主要借鉴的是关系演算,也包含了关系代数的一部分特点。 -#### 查询算子 +关系代数有投影、选择、连接、并集、差集,重命名,一共 6 个基本操作。另外,结合实际应用在这些基本操作之上又扩展出了外连接、半连接、聚集操作、分组操作等。 -SQL查询的执行过程,就像工厂的加工流水线,加工过程中的每一种工序都对应一种运算。这些运算可以被抽象为关系代数运算,**查询算子** 是指这些关系代数运算 +SQL 语句虽然是描述性的,但是我们可以把它转化成一个关系代数表达式。而关系代数中呢,又有一些等价的规则,这样我们就能结合这些等价规则对关系代数表达式进行等价的转换。进行等价转换的目的是找到性能更好的代数表达式。 -每种查询算子可能存在不同的物理实现方式,不同实现方式适合不同的场景。 +**数据库有哪些方法来实现一个左外连接呢?** 它可以用嵌套循环连接、哈希连接、归并连接等。 + +- 内连接、外连接是连接操作。这些就是 **逻辑操作符** +- 嵌套循环连接、归并连接,哈希连接等。这些就是 **物理操作符**。 +优化器作为数据库的大脑,也需要建立代价模型,对物理操作符计算代价,然后筛选出最优的物理操作符来。 + +因此,基于代价的优化是建立在物理操作符上的优化,所以也叫物理优化。物理优化就是建立在物理操作符上的优化。 +#### 查询算子 -| **查询算子** | **实现方式** | | -| -------------------------------- | ------------------------------------------------------------ | ---- | -| **排序算子** | 内存排序、外部归并排序 | | -| **选择算子** | 线性扫描、索引扫描 | | -| **连接算子** | 嵌套循环连接、块嵌套循环连接、索引嵌套循环连接、排序归并连接、哈希连接 | | -| **去重算子、聚集算子、集合算子** | 排序、哈希 | | +SQL 查询的执行过程,就像工厂的加工流水线,加工过程中的每一种工序都对应一种运算。这些运算可以被抽象为关系代数运算,**查询算子** 是指这些关系代数运算 +每种查询算子可能存在不同的物理实现方式,不同实现方式适合不同的场景。 +| **查询算子** | **实现方式** | | +| -------------------------------- | ---------------------------------------------------------------------- | --- | +| **排序算子** | 内存排序、外部归并排序 | | +| **选择算子** | 线性扫描、索引扫描 | | +| **连接算子** | 嵌套循环连接、块嵌套循环连接、索引嵌套循环连接、排序归并连接、哈希连接 | | +| **去重算子、聚集算子、集合算子** | 排序、哈希 | | ==多数情况下,一条查询可以有很多种执行方式,最后都返回相应的结果。优化器的作用就是找到这其中最好的执行计划。== -MySQL采用了基于开销成本的优化器,以确定处理查询的最解方式,也就是说执行查询之前,都会先选择一条自以为最优的方案,然后执行这个方案来获取结果。 +MySQL 采用了基于开销成本的优化器,以确定处理查询的最解方式,也就是说执行查询之前,都会先选择一条自以为最优的方案,然后执行这个方案来获取结果。 -在很多情况下,MySQL能够计算最佳的可能查询计划,但在某些情况下,MySQL没有关于数据的足够信息,或者是提供太多的相关数据信息,估测就不那么友好了。 +在很多情况下,MySQL 能够计算最佳的可能查询计划,但在某些情况下,MySQL 没有关于数据的足够信息,或者是提供太多的相关数据信息,估测就不那么友好了。 **对于一些执行起来十分耗费性能的语句,MySQL 还是依据一些规则,竭尽全力的把这个很糟糕的语句转换成某种可以比较高效执行的形式,这个过程也可以被称作查询重写**。 @@ -690,59 +625,49 @@ MySQL采用了基于开销成本的优化器,以确定处理查询的最解方 一旦这些可能性被列举出来,优化器就会选择成本最低的计划并将其交付执行。虽然成本模型通常被设计为最大化吞吐量(即每秒查询),但它也可以被设计为支持其他期望指标的查询行为,例如最小化延迟(即检索第一行的时间)或最小化内存使用。 - - > CBO(Cost-Based Optimization) -> -> CBO: Cost-Based Optimization也即"基于代价的优化器",该优化器通过根据优化规则对关系表达式进行转换,生成多个执行计划,然后CBO会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能“执行计划”的“代价”,即COST,从中选用COST最低的执行方案,作为实际运行方案。 -> -> CBO依赖数据库对象的统计信息,统计信息的准确与否会影响CBO做出最优的选择。 -> -> 以Oracle数据库为例,统计信息包括SQL执行路径的I/O、网络资源、CPU的使用情况。 -> -> 目前各大数据库和大数据计算引擎都倾向于使用CBO,例如从Oracle 10g开始,Oracle已经彻底放弃RBO,转而使用CBO;而Hive在0.14版本中也引入了CBO。 - +> +> CBO: Cost-Based Optimization 也即"基于代价的优化器",该优化器通过根据优化规则对关系表达式进行转换,生成多个执行计划,然后 CBO 会通过根据统计信息(Statistics)和代价模型(Cost Model)计算各种可能“执行计划”的“代价”,即 COST,从中选用 COST 最低的执行方案,作为实际运行方案。 +> +> CBO 依赖数据库对象的统计信息,统计信息的准确与否会影响 CBO 做出最优的选择。 +> +> 以 Oracle 数据库为例,统计信息包括 SQL 执行路径的 I/O、网络资源、CPU 的使用情况。 +> +> 目前各大数据库和大数据计算引擎都倾向于使用 CBO,例如从 Oracle 10g 开始,Oracle 已经彻底放弃 RBO,转而使用 CBO;而 Hive 在 0.14 版本中也引入了 CBO。 #### 优化器和查询成本 -一般来说一个sql查询可以有不同的执行方案,可以选择走某个索引进行查询,也可以选择全表扫描。 +一般来说一个 sql 查询可以有不同的执行方案,可以选择走某个索引进行查询,也可以选择全表扫描。 +SQL 优化器,其中最重要的一个组件是查询优化器,是数据库系统的重要组成部分。特别是对于现代大数据系统,执行计划的搜索空间异常庞大,研究人员研究了许多方法对执行计划空间进行裁剪,以减少搜索空间的代价。 -SQL优化器,其中最重要的一个组件是查询优化器,是数据库系统的重要组成部分。特别是对于现代大数据系统,执行计划的搜索空间异常庞大,研究人员研究了许多方法对执行计划空间进行裁剪,以减少搜索空间的代价。 +在当今数据库系统领域,查询优化器可以说是必备组件,不管是关系型数据库系统 Oracle、MySQL,流处理领域的 Flink、Storm,批处理领域的 Hive、Spark SQL,还是文本搜索领域的 Elasticsearch 等,都会内嵌一个查询优化器。 -在当今数据库系统领域,查询优化器可以说是必备组件,不管是关系型数据库系统Oracle、MySQL,流处理领域的Flink、Storm,批处理领域的Hive、Spark SQL,还是文本搜索领域的Elasticsearch等,都会内嵌一个查询优化器。 - -有的数据库系统会采用自研的优化器,而有的则会采用开源的查询优化器插件,比如Apache Calcite就是一个优秀的开源查询优化器插件。而像Oracle数据库的查询优化器,则是Oracle公司自研的一个核心组件,负责解析SQL,其目的是按照一定的原则来获取目标SQL在当前情形下执行的最高效执行路径。 +有的数据库系统会采用自研的优化器,而有的则会采用开源的查询优化器插件,比如 Apache Calcite 就是一个优秀的开源查询优化器插件。而像 Oracle 数据库的查询优化器,则是 Oracle 公司自研的一个核心组件,负责解析 SQL,其目的是按照一定的原则来获取目标 SQL 在当前情形下执行的最高效执行路径。 [参考](https://www.cnblogs.com/JasonCeng/p/14199298.html) - - **查询优化器** 则会比较并选择其中成本最低的方案去执行查询。 +查询处理的代价主要是指查询对各种计算资源的消耗,包括磁盘 I/O 占用、执行查询所用的 CPU 时间,如果是分布式数据库的话还需要考虑数据通信代价 - -查询处理的代价主要是指查询对各种计算资源的消耗,包括磁盘I/O占用、执行查询所用的CPU时间,如果是分布式数据库的话还需要考虑数据通信代价 - -在大型数据库系统中,磁盘I/O代价是最主要的代价,可以**使用磁盘I/O**的块数作为代价度量的指标。 - - +在大型数据库系统中,磁盘 I/O 代价是最主要的代价,可以**使用磁盘 I/O**的块数作为代价度量的指标。 查询成本分大体为两种: -- **I/O成本**:磁盘读写的开销。一个查询或一个写入,都要从磁盘中读写数据,要一定的IO开销。 +- **I/O 成本**:磁盘读写的开销。一个查询或一个写入,都要从磁盘中读写数据,要一定的 IO 开销。 -- **CPU成本**:关联查询,条件查找,都要CPU来进行计算判断,一定的计算开销。 +- **CPU 成本**:关联查询,条件查找,都要 CPU 来进行计算判断,一定的计算开销。 -MySQL使用的`InnoDB`引擎会把数据和索引都存储到磁盘上,当查询的时候需要先把数据先加载到内存中在进行下一步操作,这个加载的时间就是`I/O`成本。 +MySQL 使用的`InnoDB`引擎会把数据和索引都存储到磁盘上,当查询的时候需要先把数据先加载到内存中在进行下一步操作,这个加载的时间就是`I/O`成本。 -当数据被加载到内存中后,CPU会计算查询条件匹配,对数据排序等等操作,这一步所消耗的时间就是CPU成本。 +当数据被加载到内存中后,CPU 会计算查询条件匹配,对数据排序等等操作,这一步所消耗的时间就是 CPU 成本。 -**但是查询优化器并不会真正的去执行sql,只会去根据优化的结果去预估一个成本。** +**但是查询优化器并不会真正的去执行 sql,只会去根据优化的结果去预估一个成本。** -==**InnoDB引擎规定读取一个页面花费的成本默认约是0.25,读取以及检测一条记录是否符合搜索条件的成本默认约是0.1。**== +==**InnoDB 引擎规定读取一个页面花费的成本默认约是 0.25,读取以及检测一条记录是否符合搜索条件的成本默认约是 0.1。**== -为什么都是约呢,因为MySQL内部的计算成本比较复杂这里提取了两个主要的计算参数。 +为什么都是约呢,因为 MySQL 内部的计算成本比较复杂这里提取了两个主要的计算参数。 ```sql ## MySQL server 层面的各种开销 @@ -774,7 +699,6 @@ mysql> select * from mysql.engine_cost; mysql> ``` - 在 MySQL 可以通过查询当前会话的`last_query_cost`的值来得到其计算当前查询的成本。 ```sql @@ -796,24 +720,22 @@ mysql> 有非常多的原因会导致`MySQL`选择错误的执行计划,比如统计信息不准确、不会考虑不受其控制的操作成本(用户自定义函数、存储过程)。 -**MySQL认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但 MySQL 选择它认为成本小的,但成本小并不意味着执行时间短)等等。** - +**MySQL 认为的最优跟我们想的不一样(我们希望执行时间尽可能短,但 MySQL 选择它认为成本小的,但成本小并不意味着执行时间短)等等。** #### 优化器追踪 -MySQL优化器可以生成`explain`执行计划,我们可以通过执行计划查看是否使用了索引,使用了哪种索引? +MySQL 优化器可以生成`explain`执行计划,我们可以通过执行计划查看是否使用了索引,使用了哪种索引? 但是它只能展示`SQL`语句的执行计划,无法展示为什么一些其他的执行计划未被选择,比如说明明有索引,但是为什么查询时未使用索引等。 -好在MySQL提供了一个好用的工具`optimizer trace`(优化器追踪),可以帮助我们查看优化器生成执行计划的整个过程,以及做出的各种决策,包括访问表的方法、各种开销计算、各种转换等。 +好在 MySQL 提供了一个好用的工具`optimizer trace`(优化器追踪),可以帮助我们查看优化器生成执行计划的整个过程,以及做出的各种决策,包括访问表的方法、各种开销计算、各种转换等。 -众所周知,MySQL是基于成本的优化器(CBO),每个执行计划的成本大致反应了该计划查询所需要的资源。 +众所周知,MySQL 是基于成本的优化器(CBO),每个执行计划的成本大致反应了该计划查询所需要的资源。 -CBO选择目标SQL执行计划的判断原则是成本,从目标SQL的诸多执行计划中选取成本值最小的执行路径为其执行计划,各执行路径的成本值是根据目标SQL中涉及到的表、索引、列等相关对象的统计信息计算出来的,实际反应执行目标SQL所要消耗的I/O、CPU和网络资源的一个估计值。 +CBO 选择目标 SQL 执行计划的判断原则是成本,从目标 SQL 的诸多执行计划中选取成本值最小的执行路径为其执行计划,各执行路径的成本值是根据目标 SQL 中涉及到的表、索引、列等相关对象的统计信息计算出来的,实际反应执行目标 SQL 所要消耗的 I/O、CPU 和网络资源的一个估计值。 优化器会为每个操作标上成本,这些成本的基准单位或最小值是从磁盘读取随机数据页的成本,其他操作的成本都是它的倍数。所以优化器可以根据每个执行计划的所有操作为其计算出总的成本,然后从众多执行计划中,选取成本最小的来最终执行。 - ```SQL CREATE TABLE `user` ( @@ -855,10 +777,10 @@ INSERT INTO user(id,name,gender) VALUES (27,'小明',1), (28,'小明',1); ---这条语句用的是`idx_gender_name`这个联合索引 +--这条语句用的是`idx_gender_name`这个联合索引 explain select * from user where gender=1 and name='小明'; ---这条语句用的是`idx_name`这个索引 +--这条语句用的是`idx_name`这个索引 explain select * from user where gender=0 and name='张三'; @@ -866,8 +788,6 @@ explain select * from user where gender=0 and name='张三'; -- 到这里,使用现有工具,我们已经无法排查分析,MySQL优化器为什么使用了(name)上的索引,而没有使用(gender,name)上的联合索引。 ``` - - 相关变量 ```sql @@ -882,7 +802,7 @@ mysql> show variables like '%optimizer_trace%'; | optimizer_trace_offset | -1 | +------------------------------+----------------------------------------------------------------------------+ 5 rows in set (0.00 sec) -mysql> +mysql> -- 可用SET语句操作,用如下命令即可打开OPTIMIZER TRACE SET OPTIMIZER_TRACE="enabled=on",END_MARKERS_IN_JSON=on; @@ -904,53 +824,39 @@ SET optimizer_trace_offset=-30, optimizer_trace_limit=30; SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE limit 30; ``` - optimizer_trace -- enabled:启用/禁用optimizer_trace功能。默认关闭,也建议关闭,因为它会产生额外的性能开销。根据相关评测,当打开`optimizer trace`时,约有不到10%的性能下降。 - -- one_line:决定了跟踪信息的存储方式,为on表示使用单行存储,否则以JSON树的标准展示形式存储。 - - - - - - - +- enabled:启用/禁用 optimizer_trace 功能。默认关闭,也建议关闭,因为它会产生额外的性能开销。根据相关评测,当打开`optimizer trace`时,约有不到 10%的性能下降。 +- one_line:决定了跟踪信息的存储方式,为 on 表示使用单行存储,否则以 JSON 树的标准展示形式存储。 +在 Optimizer Trace 的输出中,主要分为三个部分: -在Optimizer Trace的输出中,主要分为三个部分: +- join_preparation SQL 准备阶段 -- join_preparation SQL准备阶段 + 完成 SQL 的准备工作,在这个阶段,SQL 语句会被格式化输出,通配符\*会被具体字段代替,但不会进行等价改写动作。 - 完成SQL的准备工作,在这个阶段,SQL语句会被格式化输出,通配符*会被具体字段代替,但不会进行等价改写动作。 - -- join_optimization SQL优化阶段 - - 完成SQL语句的逻辑与物理优化的过程,这其中的优化步骤比较多。在展开具体内容之前,先解释下”select #”的问题。在输出中经常会看到有”select#:N”的字样,它表示当前跟踪的结构体是属于第几个SELECT。如果语句中使用多个SELECT语句拼接(如UNION)或者有嵌套子查询中有SELECT,会产生多个序号。 - -- join_execution SQL执行阶段 +- join_optimization SQL 优化阶段 + 完成 SQL 语句的逻辑与物理优化的过程,这其中的优化步骤比较多。在展开具体内容之前,先解释下”select #”的问题。在输出中经常会看到有”select#:N”的字样,它表示当前跟踪的结构体是属于第几个 SELECT。如果语句中使用多个 SELECT 语句拼接(如 UNION)或者有嵌套子查询中有 SELECT,会产生多个序号。 +- join_execution SQL 执行阶段 #### 统计数据 - 既然是基于统计数据来进行标记成本,就总会有样本无法正确反映整体的情况,这也是 MySQL 优化器有时做出错误优化的重要原因之一。 +MySQL 统计信息是指数据库通过采样、统计出来的表、索引的相关信息,例如,表的记录数、聚集索引 page 个数、字段的 Cardinality....。 -MySQL统计信息是指数据库通过采样、统计出来的表、索引的相关信息,例如,表的记录数、聚集索引 page 个数、字段的Cardinality....。 - -MySQL在生成执行计划时,需要根据索引的统计信息进行估算,计算出最低代价(或者说是最小开销)的执行计划,MySQL支持有限的索引统计信息,因存储引擎不同而统计信息收集的方式也不同。 +MySQL 在生成执行计划时,需要根据索引的统计信息进行估算,计算出最低代价(或者说是最小开销)的执行计划,MySQL 支持有限的索引统计信息,因存储引擎不同而统计信息收集的方式也不同。 -根据统计数据是否可以持久化,MySQL提供了两种统计方式: +根据统计数据是否可以持久化,MySQL 提供了两种统计方式: -- **统计数据存储在磁盘上。** +- **统计数据存储在磁盘上。** -- 统计数据存储在内存中,当服务器关闭时这些这些统计数据就都被清除掉了。   +- 统计数据存储在内存中,当服务器关闭时这些这些统计数据就都被清除掉了。 -MySQL给我们提供了系统变量innodb_stats_persistent来控制到底采用哪种方式去存储统计数据。 +MySQL 给我们提供了系统变量 innodb_stats_persistent 来控制到底采用哪种方式去存储统计数据。 ```shell # 在MySQL 5.6.6之前,innodb_stats_persistent的值默认是OFF,InnoDB的统计数据默认是存储到内存的 @@ -960,9 +866,9 @@ innodb_stats_persistent=ON ``` - mysql.innodb_table_stats - 存储了表的总行数,主键`clustered index`上page数,非主键索引的page总数。 + 存储了表的总行数,主键`clustered index`上 page 数,非主键索引的 page 总数。 -InnoDB默认是以表为单位来收集和存储统计数据的,也就是说我们可以把某些表的统计数据(以及该表的索引统计数据)存储在磁盘上,把另一些表的统计数据存储在内存中。 +InnoDB 默认是以表为单位来收集和存储统计数据的,也就是说我们可以把某些表的统计数据(以及该表的索引统计数据)存储在磁盘上,把另一些表的统计数据存储在内存中。 我们可以在创建和修改表的时候通过指定(STATS_PERSISTENT,STATS_AUTO_RECALC,STATS_SAMPLE_PAGES)属性来指明该表的统计数据存储方式,以及其他属性。 @@ -973,8 +879,6 @@ ALTER TABLE table2 Engine=InnoDB, STATS_PERSISTENT = (1|0); 持久化的统计数据存储在 `mysql.innodb_index_stats` 和 `mysql.innodb_table_stats` 中: - - - database_name 数据库名 - table_name 表名 - last_update 本条记录最后更新时间 @@ -982,51 +886,19 @@ ALTER TABLE table2 Engine=InnoDB, STATS_PERSISTENT = (1|0); - clustered_index_size 表的聚簇索引占用的页面数量 - sum_of_other_index_sizes 表的其他索引占用的页面数量 -在InnoDB存储引擎下,其对表中记录数量的统计值n_rows是不准确的。其统计方法是先通过算法选取若干个(聚簇索引的)叶子节点页面,然后计算叶子节点页面中记录数量的均值,最后将均值乘以(聚簇索引的)叶子节点的数量得到n_rows值。故其不是一个精确值,而是一个估计值。 - -在计算均值过程中,如果选取的叶子节点越多,则n_rows值越准确。故在MySQL中,可通过系统变量`innodb_stats_persistent_sample_pages`来设置在计算永久性的统计数据时统计过程所需的页面采样数量。显然`innodb_stats_persistent_sample_pages`值越大,统计过程所需耗时也就越多。 +在 InnoDB 存储引擎下,其对表中记录数量的统计值 n_rows 是不准确的。其统计方法是先通过算法选取若干个(聚簇索引的)叶子节点页面,然后计算叶子节点页面中记录数量的均值,最后将均值乘以(聚簇索引的)叶子节点的数量得到 n_rows 值。故其不是一个精确值,而是一个估计值。 +在计算均值过程中,如果选取的叶子节点越多,则 n_rows 值越准确。故在 MySQL 中,可通过系统变量`innodb_stats_persistent_sample_pages`来设置在计算永久性的统计数据时统计过程所需的页面采样数量。显然`innodb_stats_persistent_sample_pages`值越大,统计过程所需耗时也就越多。 ##### 更新统计数据 -当系统变量innodb_stats_auto_recalc值为ON,即可实现统计数据的自动更新。具体地,一般当表中变化的记录数超过一定阈值,MySQL会自动开始重新进行统计。只不过该统计是异步的,所以即使满足重新统计的条件也不会立即开始计算,有可能会延迟几秒才开始 - -前面我们说了,统计数据是以表为单位进行统计的,故我们还可以通过STATS_AUTO_RECALC属性来显式地设置表是否自动更新统计数据。具体地,当值为0意为不会进行自动更新;当值为1意为会进行自动更新。更多地,我们一般很少会在建表时指定该属性,则该表就默认使用我们上面提到的系统变量innodb_stats_auto_recalc的配置 - -#### **优化器的功能** - -不止是数据库要进行优化,基本上所有的编程语言在编译的时候都要优化。 - -比如在编译 C 语言的时候,可以通过编译选项 -o 来指定进行哪个级别的优化,只是查询数据库的查询优化和 C 语言的优化还有些区别。 - -C 语言是过程化语言,已经指定好了需要执行的每一个步骤;但 SQL 是描述性语言,只指定了 WHAT,而没有指定 HOW。这样它的优化空间就大了。 - -通常来说分成两个层面, - -- 一个是基于规则的优化。基于规则的优化也可以叫**逻辑优化**(或者规则优化) -- 另一个是基于代价的优化。基于代价的优化也可以叫**物理优化**(或者代价优化)。 - - - -关系演算是纯描述性的语言,关系代数包含了一些基本的关系操作,SQL 主要借鉴的是关系演算,也包含了关系代数的一部分特点。 - -关系代数有投影、选择、连接、并集、差集,重命名,一共 6 个基本操作。另外,结合实际应用在这些基本操作之上又扩展出了外连接、半连接、聚集操作、分组操作等。 - -**SQL 语句虽然是描述性的,但是我们可以把它转化成一个关系代数表达式。而关系代数中呢,又有一些等价的规则,这样我们就能结合这些等价规则对关系代数表达式进行等价的转换。进行等价转换的目的是找到性能更好的代数表达式** - +当系统变量 innodb_stats_auto_recalc 值为 ON,即可实现统计数据的自动更新。具体地,一般当表中变化的记录数超过一定阈值,MySQL 会自动开始重新进行统计。只不过该统计是异步的,所以即使满足重新统计的条件也不会立即开始计算,有可能会延迟几秒才开始 +前面我们说了,统计数据是以表为单位进行统计的,故我们还可以通过 STATS_AUTO_RECALC 属性来显式地设置表是否自动更新统计数据。具体地,当值为 0 意为不会进行自动更新;当值为 1 意为会进行自动更新。更多地,我们一般很少会在建表时指定该属性,则该表就默认使用我们上面提到的系统变量 innodb_stats_auto_recalc 的配置 -**数据库有哪些方法来实现一个左外连接呢?**它可以用嵌套循环连接、哈希连接、归并连接等。 +#### **优化器配置** -- 内连接、外连接是连接操作。这些就是**逻辑操作符** -- 嵌套循环连接、归并连接,哈希连接等。这些就是**物理操作符**。 - -优化器作为数据库的大脑,也需要建立代价模型,对物理操作符计算代价,然后筛选出最优的物理操作符来。 - -因此,基于代价的优化是建立在物理操作符上的优化,所以也叫物理优化。物理优化就是建立在物理操作符上的优化。 - - -1. 不改变语义的情况下,重写sql。重写后的 sql 更简单,更方便制定执行计划。 +1. 不改变语义的情况下,重写 sql。重写后的 sql 更简单,更方便制定执行计划。 2. 根据成本分析,制定执行计划。 ```sql @@ -1061,7 +933,7 @@ SET [GLOBAL|SESSION] optimizer_switch='command[,command]...'; -- command语法如下: -- default --重置为默认 --- opt_name=default --选项默认 +-- opt_name=default --选项默认 -- opt_name=off --关掉某项优化 -- opt_name=on --开启某项优化 @@ -1083,26 +955,27 @@ SET [GLOBAL|SESSION] optimizer_switch='command[,command]...'; ### 派生条件下推 -MySQL 8.0.22及更高版本支持符合条件的子查询的派生条件下推。 +MySQL 8.0.22 及更高版本支持符合条件的子查询的派生条件下推。 + +对于如 -对于如 ```SQL SELECT * FROM (SELECT i,j FROM t1) AS dt WHERE i > constant ``` -在许多情况下,可以将外部`WHERE`条件下推到派生表,相当于改写成如下SQL +在许多情况下,可以将外部`WHERE`条件下推到派生表,相当于改写成如下 SQL ```SQL SELECT * FROM (SELECT i, j FROM t1 WHERE i > constant) AS dt ``` + 这减少了派⽣表返回的⾏数,从⽽加快查询的速度。 ==派生条件下推一句话理解即为:外查询与派生表相关的条件会被推入到派生表中作为条件,以减少处理的数据行数,加速查询速度== https://dev.mysql.com/doc/refman/8.0/en/derived-condition-pushdown-optimization.html - -当派生表无法合并到外部查询中时(例如:如果派生表使用聚合),将外部WHERE条件下推到派生表应该会减少需要处理的行数,从而加快查询的执行。 +当派生表无法合并到外部查询中时(例如:如果派生表使用聚合),将外部 WHERE 条件下推到派生表应该会减少需要处理的行数,从而加快查询的执行。 ### 索引下推 @@ -1112,7 +985,6 @@ https://dev.mysql.com/doc/refman/8.0/en/derived-condition-pushdown-optimization. 数据库中有个概念叫**谓词下推** - 如果一个谓词在执行计划中即使处在不同的位置也不改变执行结果,那么我们就尽量把它保持在下层,因为它有"过滤"的作用。在下层结点把数据过滤掉,有助于降低上层结点的计算量。当然对于一些比较执着的谓词`SQL`的书写者把它安排在了上层,我们在生成执行计划的时候就可以考虑是否能把它推下去。这就需要进行甄别,哪些谓词是可以推下去的,而哪些谓词是无法推下去的。 - 过滤条件:处在 WHERE 关键字后面的约束条件是过滤条件。 @@ -1120,23 +992,20 @@ https://dev.mysql.com/doc/refman/8.0/en/derived-condition-pushdown-optimization. 如果两个表要做的是内连接,实际上它是无需区分连接条件还是过滤条件的。因为即使是处在 ON 关键字后面的连接条件,也只有过滤的作用,内连接不会去补 NULL 值,所以在内连接的时候过滤条件和连接条件是等价的。 - - -==谓词下推的基本思想是将过滤表达式尽可能移动至靠近数据源的位置,以使真正执行时能直接跳过无关的数据。用在SQL优化上来说,就是先过滤再做聚合等操作。== +==谓词下推的基本思想是将过滤表达式尽可能移动至靠近数据源的位置,以使真正执行时能直接跳过无关的数据。用在 SQL 优化上来说,就是先过滤再做聚合等操作。== `predicate push down` 翻译为谓词下推,这个翻译很准确,明确的告诉了我们这个操作是一个什么动作。 -`predicate(谓词)`即条件表达式,在SQL中,谓词就是返回`boolean`值即`true`或`false`的函数,或是隐式转换为`bool`的函数。 +`predicate(谓词)`即条件表达式,在 SQL 中,谓词就是返回`boolean`值即`true`或`false`的函数,或是隐式转换为`bool`的函数。 `SQL`中的谓词主要有 `LIKE、BETWEEN、IS NULL、IS NOT NULL、IN、EXISTS`。其结果为布尔值,即`true`或`false`。 -`predicate pushdown`是将SQL语句中的部分语句( predicates 谓词部分) 可以被 "pushed" 下推到数据源或者靠近数据源的部分。 +`predicate pushdown`是将 SQL 语句中的部分语句( predicates 谓词部分) 可以被 "pushed" 下推到数据源或者靠近数据源的部分。 通过尽早过滤掉数据,这种处理方式能大大减少数据处理的量,降低资源消耗,在同样的服务器环境下,极大地减少了查询/处理时间。 在`Hive SQL`和`Spark SQL`等一系列`SQL ON Hadoop`的解析语法树时都在谓词下推方面作出了优化,其实在使用`SQL`的过程,我们心中记住这是一种将过滤尽可能在靠近数据源(取数据)的时候完成的一种操作,是数据库的一种经典的优化手段。 - 在传统数据库的查询系统中谓词下推作为优化手段很早就出现了,谓词下推的目的就是通过将一些过滤条件尽可能的在最底层执行可以减少每一层交互的数据量,从而提升性能。例如下面这个例子: ```SQL @@ -1152,25 +1021,21 @@ select count(1) from (select * from A where a>10)A1 Join (select * from B wh ``` -### 查询重写REWRITE +### 查询重写 REWRITE 查询重写(query rewrite):按照一系列关系代数表达式的等价规则,对查询的关系代数表达式进行等价转换,从而提高查询执行效率。 - - 关系表达式的等价(equivalent):对于两个关系代数表达式,如果使用相同的关系对表达式中的关系进行替换,总能得到完全相同的结果,则称这两个关系表达式等价 基于关系代数等价规则做等价变换的优化,就是基于规则的优化。 - -在内核中要实现这些改写算法是极具挑战性的。在业务层改写 SQL 很简单,因为我们的任务只是改对一条已知的 SQL。在内核层改写非常困难,因为我们的任务是给定任意一条SQL,如果可以改写要尽可能的改写,同时改写必须是正确的。这其实对改写算法提出了两点要求: +在内核中要实现这些改写算法是极具挑战性的。在业务层改写 SQL 很简单,因为我们的任务只是改对一条已知的 SQL。在内核层改写非常困难,因为我们的任务是给定任意一条 SQL,如果可以改写要尽可能的改写,同时改写必须是正确的。这其实对改写算法提出了两点要求: - 正确:改写后的`SQL`语义不能改变。 - 完备:可以改写的`SQL`要能够改写。 正确性非常容易理解,如果一个改写的结果是错误的,这对业务的价值完全是负面的。完备性同样重要,它要求一个改写算法具有较好的通用性,不能只处理一些简单的场景,而不处理复杂的场景。通用性差对业务的价值是有限的。当然,我们很难把一个改写算法做到完全的完备。因为总有一些非常复杂的情形是很难处理的,强行改写可能会引入正确性问题。在实现一个改写算法的过程中,我们会在确保正确的前提下,尽可能的做到完备。 - #### 外连接消除 外连接操作可分为左外连接,右外连接和全外连接。连接过程中,外连接左右顺序不能变换,这限制了优化器对连接顺序的选择。 @@ -1187,7 +1052,7 @@ SELECT L.ID, L.C1, R.C2 FROM L, R WHERE L.ID = R.ID AND R.C2 = 'XXX'; -- 此时,优化器的查询重写功能可以将外连接改写为内连接。这有助于优化器考虑更多的join顺序和算法。 ``` -`WHERE cno IS NOT NULL`这样的条件可以让外连接和内连接的结果相同,因为这个约束条件是严格strict的。"严格"的精确定义是,对于一个表达式,如果输入参数是 NULL 值,那么输出也一定是 NULL 值,就可以说这个表达式是严格的。 +`WHERE cno IS NOT NULL`这样的条件可以让外连接和内连接的结果相同,因为这个约束条件是严格 strict 的。"严格"的精确定义是,对于一个表达式,如果输入参数是 NULL 值,那么输出也一定是 NULL 值,就可以说这个表达式是严格的。 从 SQL 语义的角度出发,对于一个表达式,如果输入参数是 NULL 值,输出结果是 NULL 值或者 FALSE,那么就可以认为这个表达式是严格的。如果在约束条件里有这种严格的表达式,由于输入是 NULL 值,输出是 NULL 值或者 FALSE,那么含有 NULL 值的元组就会被过滤掉。 @@ -1197,23 +1062,19 @@ SELECT L.ID, L.C1, R.C2 FROM L, R WHERE L.ID = R.ID AND R.C2 = 'XXX'; - R.C2 出现 `>`, `<`, `==`, `IS NOT NULL` 等判断表达式的一侧。这类表达式一侧参数为`NULL`时,判断结果必然为`unknown/false`,所以它们是空值拒绝条件。 - 外连接消除就是将一个`outer join`转换成`inner join` 因为这个查询的结果中不可能出现 R 为 `NULL` 的行,假设有这样的行,它也一定会被 where R.C2 = 'XXX' 过滤掉。这使得使用 `outer join` 和使用 `inner join` 的效果等价。 ==外连接消除最终目的是给优化器带来更多的灵活性,`inner join` 可以选择的优化路径比 `outer join` 多得多。== - -优化器在选择多表连接顺序时,可以有更多更灵活的选择,从而可以选择更好的表连接顺序,加快查询执行的速度;表的一些连接算法(如块嵌套连接和索引循环连接等)。可以减少不必要的I/O开销,能加快算法执行的速度。 - +优化器在选择多表连接顺序时,可以有更多更灵活的选择,从而可以选择更好的表连接顺序,加快查询执行的速度;表的一些连接算法(如块嵌套连接和索引循环连接等)。可以减少不必要的 I/O 开销,能加快算法执行的速度。 外连接消除总结: - 注意外连接与内连接的语义差别; - 外连接优化的条件:空值拒绝; -- 外连接优化的本质:语义上是外连接,但WHER条件使得外连接可以蜕化为内连接。 - +- 外连接优化的本质:语义上是外连接,但 WHER 条件使得外连接可以蜕化为内连接。 #### 连接消除 @@ -1239,24 +1100,20 @@ select t1.*,t3.* from t1 join t3 on (a1=a3) limit 1; #### 连接顺序交换 -SQL语句中会指定表的具体顺序,有一些简单的情况,通过观察就能看出交换顺序之后仍然等价,例如: +SQL 语句中会指定表的具体顺序,有一些简单的情况,通过观察就能看出交换顺序之后仍然等价,例如: - 内连接 交换内连接的两个表的连接顺序,通常而言不会影响执行结果 - 左外连接 交换左外连接的两个表的连接顺序而且把左外连接改变成右外连接,那么也不会影响执行结果 +#### 查看优化器重写后的 SQL -#### 查看优化器重写后的SQL - -SQL语句在被服务器执行时,并不一定就会按照我们写的顺序执行,MySQL优化器会重写SQL,如何才能看到优化器重写后的SQL呢?这就要用到explain extended和show warnings了。 - - -explain extended sql语句,然后show warnings查看。 - -explain extended会输出sql的执行计划,查询记录的方式(全表扫描、全索引扫描、索引范围扫描等)、是否用上索引 +SQL 语句在被服务器执行时,并不一定就会按照我们写的顺序执行,MySQL 优化器会重写 SQL,如何才能看到优化器重写后的 SQL 呢?这就要用到 explain extended 和 show warnings 了。 -show warnings会看到优化器重写后的sql +explain extended sql 语句,然后 show warnings 查看。 +explain extended 会输出 sql 的执行计划,查询记录的方式(全表扫描、全索引扫描、索引范围扫描等)、是否用上索引 +show warnings 会看到优化器重写后的 sql #### **条件化简** @@ -1277,11 +1134,9 @@ show warnings会看到优化器重写后的sql **常量传递** ```sql - a = 5 AND b > a 就可以被转换为: a = 5 AND b > 5 + a = 5 AND b > a 就可以被转换为: a = 5 AND b > 5 ``` - - **子查询优化** 我们查询中的 select 列 from 表 中,有时候,列和表可能是我们其他查询中出来的。这种列和表是用 select 语句表现出来的就叫子查询。外层 select 就叫外层查询。 @@ -1325,7 +1180,7 @@ SELECT product_id, product_name, sale_price WHERE sale_price > (SELECT AVG(sale_price)FROM Product); - + -- 标量子查询的书写位置并不仅仅局限于 WHERE 子句中,通常任何可以使用单一值的位置都可以使用。 -- 也就是说,能够使用常数或者列名的地方,无论是 SELECT 子句、GROUP BY 子句、HAVING 子句,还是ORDER BY 子句,几乎所有的地方都可以使用。 ``` @@ -1355,12 +1210,10 @@ WHERE sale_price > (SELECT AVG(sale_price)FROM Product);  SELECT * FROM e1 WHERE (m1, n1) IN (SELECT m2, n2 FROM e2); ``` - - 不相关子查询 如果子查询可以单独运行出结果,而不依赖于外层查询的值,我们就可以把这个子查询称之为不相关子查询。我们前边介绍的那些子查询全部都可以看作不相关子查询。 - - 相关子查询 如果子查询的执行需要依赖于外层查询的值,我们就可以把这个子查询称之为相关子查询。 @@ -1381,6 +1234,7 @@ WHERE sale_price > (SELECT AVG(sale_price)FROM Product); -- 员工多,而相应的职位(如销售员、经理、部门经理等)少,因此首先想到的思路是对职位分组,这样就能分别得到各个职位的平均工资,再比较每个人的工资和他对应职位的平均工资,大于则被筛选出来。 ``` + [参考](https://www.cnblogs.com/heenhui2016/p/10574695.html) []() @@ -1398,8 +1252,8 @@ SELECT * FROM t1 WHERE t1.a IN (SELECT t2.b FROM t2 WHERE id < 10); -- 我们可能会认为这样执行的 ---SELECT t2.b FROM t2 WHERE id < 10; --- 结果:1,2,3,4,5,6,7,8,9 +--SELECT t2.b FROM t2 WHERE id < 10; +-- 结果:1,2,3,4,5,6,7,8,9 -- select * from t1 where t1.a in(1,2,3,4,5,6,7,8,9); -- 实际上在MySQL内部,优化器可能会改写成如下SQL @@ -1440,8 +1294,7 @@ select * from t1 where exists(select b from t2 where id < 10 and t1.a=t2.b);  -- 驱动表算法  ``` - -**ANY/ALL子查询优化** +**ANY/ALL 子查询优化** 如果 ANY/ALL 子查询是不相关子查询的话, 它们在很多场合都能转换成我们熟悉的方式去执行: @@ -1452,8 +1305,7 @@ select * from t1 where exists(select b from t2 where id < 10 and t1.a=t2.b); > ALL (SELECT inner_expr ...) > (SELECT MAX(inner_expr) ...) --大于所有的,等价于大于最大的 ``` - -### IN和EXISTS区别 +### IN 和 EXISTS 区别 SQL 的保留字中,有很多都被归为谓词一类。例如,`=`,`<`,`>`,`<>` 等比较谓词,以及 `BETWEEN` 、`LIKE` 、`IN` 、`IS NULL` 等。 @@ -1467,10 +1319,10 @@ SQL 的保留字中,有很多都被归为谓词一类。例如,`=`,`<`,`>`,` 也就是说,我们平时使用的 `WHERE` 子句,其实也可以看成是由多个谓词组合而成的新谓词。只有能让 `WHERE` 子句的返回值为真的命题,才能从表(命题的集合)中查询到。 - 不管子查询是相关的还是不相关的, 都可以把 `IN` 子查询尝试转为 `EXISTS` 子查询其实对于任意一个 `IN` 子查询来说, 都可以被转为 `EXISTS` 子查询 通用的例子如下: + ```SQL outer_expr IN (SELECT inner_expr FROM ... WHERE subquery_where) -- 可以被转换为: @@ -1481,67 +1333,43 @@ EXISTS (SELECT inner_expr FROM ... WHERE subquery_where AND outer_expr=inner_exp ``` + 在 `MySQL5.5` 以及之前的版本没有引进 `semi-join` 和物化的方式优化子查询时,优化器都会把 `IN` 子查询转换为 `EXISTS` 子查询,所以当时好多声音都是建议大家把子查询转为连接,不过随着 `MySQL` 的发展,最近的版本中引入了非常多的子查询优化策略,内部的转换工作优化器会为大家自动实现。 [https://blog.csdn.net/weixin_47184173/article/details/117411011](https://blog.csdn.net/weixin_47184173/article/details/117411011) -[为什么300的并发能把支持最大连接数4000数据库压死?](https://www.notion.so/300-4000-5eb680aec38d4cc8adda2f70d33af246) - - +[为什么 300 的并发能把支持最大连接数 4000 数据库压死?](https://www.notion.so/300-4000-5eb680aec38d4cc8adda2f70d33af246) ### 如何处理查询? -有时候,各种查询语句经常会直接查询超大表,甚至单表就超过整个服务器的所有物理内存了。那会不会一下子就打挂MySQL呢? +有时候,各种查询语句经常会直接查询超大表,甚至单表就超过整个服务器的所有物理内存了。那会不会一下子就打挂 MySQL 呢? 显然不会。客户端执行查询要求返回的结果集是很普遍的情况。 +## MySQL 线程状态 - - - -## MySQL线程状态 - - - - - - - -| 状态 | 含义 | -| ---------------------------------------- | ------------------------------------------------------------ | +| 状态 | 含义 | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `After create` | 当线程在创建表的函数末尾创建表(包括内部临时表)时,会发生这种情况。即使由于某些错误而无法创建表,也会使用此状态 | -| altering table | 服务器正在执行就地 ALTER TABLE | -| Analyzing | 线程正在计算MyISAM表键分布(例如,for ANALYZE TABLE)。 | -| checking permissions | 线程正在检查服务器是否具有执行语句所需的权限 | -| Checking table | 该线程正在执行表检查操作 | -| cleaning up | 该线程已经处理了一个命令,并准备释放内存并重置某些状态变量 | -| **`closing tables`** | | -| committing alter table to storage engine | | -| converting HEAP to MyISAM | | -| copy to tmp table | | -| Copying to group table | | -| Copying to tmp table | | -| **`Copying to tmp table on disk`** | | -| Creating index | | -| Creating sort index | | -| creating table | | -| **`Creating tmp table`** | | - - - - - - - - - - - - - +| altering table | 服务器正在执行就地 ALTER TABLE | +| Analyzing | 线程正在计算 MyISAM 表键分布(例如,for ANALYZE TABLE)。 | +| checking permissions | 线程正在检查服务器是否具有执行语句所需的权限 | +| Checking table | 该线程正在执行表检查操作 | +| cleaning up | 该线程已经处理了一个命令,并准备释放内存并重置某些状态变量 | +| **`closing tables`** | | +| committing alter table to storage engine | | +| converting HEAP to MyISAM | | +| copy to tmp table | | +| Copying to group table | | +| Copying to tmp table | | +| **`Copying to tmp table on disk`** | | +| Creating index | | +| Creating sort index | | +| creating table | | +| **`Creating tmp table`** | | ## 存储引擎 -为了方便管理,人们把连接管理、查询缓存、语法解析、查询优化这些并不涉及真实数据存储的功能划分为MySQL Server的功能,把真实存取数据的功能划分为存储引擎的功能。所以在MySQL server完成了查询优化后,只需按照生成的执行计划调用底层存储引擎提供的API,获取到数据后返回给客户端就好了。 +为了方便管理,人们把连接管理、查询缓存、语法解析、查询优化这些并不涉及真实数据存储的功能划分为 MySQL Server 的功能,把真实存取数据的功能划分为存储引擎的功能。所以在 MySQL server 完成了查询优化后,只需按照生成的执行计划调用底层存储引擎提供的 API,获取到数据后返回给客户端就好了。 -MySQL中提到了存储引擎的概念,简而言之,存储引擎就是指表的类型。其实存储引擎以前叫做表处理器,后来改名为存储引擎,它的功能就是接收上层传下来的指令,然后对表中的数据进行提取或写入操作。 +MySQL 中提到了存储引擎的概念,简而言之,存储引擎就是指表的类型。其实存储引擎以前叫做表处理器,后来改名为存储引擎,它的功能就是接收上层传下来的指令,然后对表中的数据进行提取或写入操作。