干后端这几年,MySQL 的日常工作中我听到最多的两句话,一句是“连不上数据库了”,另一句是“这条 SQL 怎么这么慢”。表面上看这是两个独立的问题,一个发生在连接阶段,一个发生在查询阶段,但如果你把从连接数据库到查询返回这条完整链路摊开看,会发现很多看似诡异的报错,其实都对应链路中某一个很具体的环节。今天这篇我就用自己实际排查和测试积累的经验,把这个全过程从头到尾拆一遍,保证讲的都是你平时能直接用上的东西。
这篇文章适合谁看?刚入门的朋友可以把它当成一条主线,把零散的 MySQL 知识点串起来;写过一两年 SQL 的同学,则可以用它来对照自己的知识体系,看看连接、认证、优化器、执行器这些环节里,还有哪些细节是你之前没注意到的。我尽量不堆概念,每到一个关键点都会说清楚“为什么”,也会把我踩过的坑直接摆出来。
1. 连接之前:先把 MySQL 的“门牌号”和“钥匙”准备好
1.1 端口、地址、监听:确认 MySQL 到底在哪里等你
很多人第一次装 MySQL,装完就急着写代码,结果程序一跑直接报 2003 Can't connect。这个错误绝大多数情况下不是用户名密码的问题,而是客户端压根没找到服务器。MySQL 默认监听在 3306 端口,这是它的“门牌号”。你可以用一条命令确认它有没有在听:
netstat -tlnp | grep 3306能看到0.0.0.0:3306或者::3306这样的输出,说明 MySQL 正在所有网卡上监听,外部机器有权限的话就能连进来。如果只看到127.0.0.1:3306,那说明它只允许本机连接,外部连不上是正常的。如果你想让它对局域网开放,需要去改配置文件里的bind-address参数:
bind-address = 0.0.0.0改完重启 MySQL 服务。这里要提醒一句,bind-address设为0.0.0.0和 MySQL 的用户权限是两套体系,前者决定“端口开不开放”,后者决定“谁能用哪个账号从哪个 IP 登录”,两个都得配好才行。另外 MySQL 还有一个走 Unix Socket 的本地连接方式,通常在配置里有socket = /var/run/mysqld/mysqld.sock这样的路径。命令行不带-h参数登录时,用的往往就是 Socket 而不是 TCP,这也是很多新手搞不明白“为什么本地能连,远程不能连”的原因之一。
1.2 账号与权限:连得上不代表你有资格查
确认端口通之后,下一个坑就是权限。MySQL 的账号是由user和host共同决定的,比如'app'@'%'和'app'@'localhost'是两个完全不同的账号。%表示任意主机,但不包括localhost,这是新手很容易踩的坑:明明创建了'app'@'%',本地用mysql -uapp -p登录却提示 Access denied,因为本地那条连接实际匹配的是'app'@'localhost',而你没建这个账号。
创建账号和授权的标准姿势我建议分两步走:
CREATE USER 'app'@'%' IDENTIFIED BY 'StrongP@ssw0rd'; GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app'@'%';这里只给了业务上需要的DML权限,如果你是开发环境,可以再考虑加CREATE、ALTER这类DDL权限,但生产环境一定要克制。授权之后不需要刻意执行FLUSH PRIVILEGES,因为用CREATE USER/GRANT语句修改权限表时,MySQL 会自动重新加载权限,这条命令是从老版本流传下来的习惯,在 5.7 之后基本用不上了。
还有一个容易被忽略的点:MySQL 8.0 默认的认证插件是caching_sha2_password,而 5.7 时代很多客户端默认使用的是mysql_native_password。你用 5.7 时代的老客户端去连 8.0 库,经常会报Authentication plugin 'caching_sha2_password' cannot be loaded。解决办法当然是把客户端升级到支持新插件的版本,而不是反过来把数据库的认证方式降级,除非你有非常特殊的历史原因,否则别为了省事破坏默认安全配置。
2. 三种最常见的连接方式,从命令行到驱动
2.1 命令行客户端:排查问题时的最后底线
我在定位线上问题的时候,不管手边有没有图形工具,第一反应永远是先用命令行连一下。因为命令行最接近协议本身,报错信息也最原始。基本用法如下:
mysql -h10.0.0.5 -P3306 -uapp -p-h是主机地址,-P是端口,-u是用户,-p表示需要交互式输入密码。如果你还想指定数据库,直接在命令末尾加上库名:
mysql -h10.0.0.5 -P3306 -uapp -p mydb连接成功后,我习惯先执行几条“体检”命令:
SELECT VERSION(); SELECT CURRENT_USER(); SHOW DATABASES;CURRENT_USER()会告诉你当前实际匹配到的账号是'app'@'xxx',这对排查那种“我以为我是用 A 账号登录,实际命中的是 B 账号”的问题非常有帮助。另外提醒一下,命令行连接时密码直接跟在-p后面(如-p123456)是极不安全的做法,因为进程列表里能直接看到密码,一定要用交互式输入,或者用MYSQL_PWD环境变量兜底,连这个我都不推荐,直接用-p回车后输入最稳妥。
2.2 图形化工具:Navicat、DBeaver 的连接配置
图形工具是日常开发效率神器,Navicat 和 DBeaver 我都用过,配置逻辑大同小异:连接名随意填,主机、端口、用户名、密码必填,数据库名可以等连上再选。第一次连接时如果勾选“保存密码”,相当于把密码以可逆加密的形式存在本地,公网机器上我建议不要勾。
这类工具在连接时还有一个隐藏参数经常被忽略,就是字符集。很多老项目用的库表还是latin1编码,而新工具默认请求utf8mb4,连接上去后查中文可能会乱码。正确的做法是先执行SHOW VARIABLES LIKE 'character_set_database';看清库的实际编码,再把工具连接里的编码设成一致。千万别在不知道库编码的情况下直接改表结构,那不是修乱码,是给自己挖坑。
图形工具还有一个好处,就是能可视化看执行计划。点一下“解释当前语句”就能看到EXPLAIN的输出,比命令行敲EXPLAIN再人工对齐列舒服很多。不过建议你不要过分依赖工具,还是要能手动读懂执行计划的每一列,这样排查问题才不受工具限制。
2.3 代码里的连接串:JDBC 和 PyMySQL 的经典配置
写 Java 的同学对 JDBC URL 应该很熟,它的通用格式是:
jdbc:mysql://10.0.0.5:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8这里面有三个参数我建议每一次都显式写上:useSSL、serverTimezone、characterEncoding。不写serverTimezone在 JDBC 8.x 下通常会报时区相关的异常,因为驱动拿不到 JVM 默认时区;不写characterEncoding则可能出现中文乱码。还有两个容易被忽略但线上很有用的参数:
connectTimeout=3000&socketTimeout=5000connectTimeout是 TCP 建立连接的最大等待时间,socketTimeout是连接建立后等待数据返回的最大时间。不配这两个参数,一旦数据库负载过高或者网络抖动,你的应用线程会一直卡在数据库调用上,最后把线程池打满。我见过不止一次线上事故,就是“数据库慢,应用线程全部堆积”,最后数据库恢复了应用也恢复不了。
Python 这边我用得比较多的是PyMySQL,典型写法:
import pymysql conn = pymysql.connect( host='10.0.0.5', port=3306, user='app', password='StrongP@ssw0rd', database='mydb', charset='utf8mb4', cursorclass=pymysql.cursors.DictCursor )注意charset传utf8mb4而不是utf8,utf8在 MySQL 里其实是utf8mb3,遇到 emoji 这类四字节字符会直接报错或者存成乱码。Django 项目如果在 Python 3 下用 PyMySQL,还必须在__init__.py里加一行pymysql.install_as_MySQLdb(),否则 ORM 会找不到 MySQLdb 模块。
2.4 连接串里的隐藏参数:SSL、时区、字符集一个都不能省
前面提到useSSL,这里展开说一下。MySQL 8.0 服务端默认是支持 SSL 的,但很多老项目的客户端默认不启用,如果你在 JDBC 连接串里什么都不写,驱动通常会告警但不会强拦。真正麻烦的是另一种情况:你显式设置了useSSL=true,但客户端没有信任服务端证书,这时候会直接报SSL connection error。解决思路有两种,第一种是正经办法,给客户端配置信任证书;第二种是排障时临时用verifyServerCertificate=false跳过证书校验,但这是测试环境干的事,生产环境别学。
从实践角度,我建议内网环境直接useSSL=false走明文字符流,因为内网本身有网络隔离和防火墙策略保护;公网或跨机房传输,必须开启 SSL,而且要配证书校验。盲目关 SSL 或者盲目开 SSL 都可能出问题,关键是搞清楚自己的传输链路有没有被嗅探的风险。另外,characterEncoding=utf8和charset=utf8mb4是两码事,前者是 Java 驱动生成的字符编码,后者是连接握手时告诉服务端“我用什么字符集交互”,两边要搭配对,才能避免中文显示不出来。
3. 连接这一步,数据库内部是怎么接待你的
3.1 TCP 握手和 MySQL 协议握手包
别看命令行敲一下回车就出现Enter password,这一瞬间底层已经经历了好几个阶段。首先是 TCP 三次握手,这和其他网络应用没有区别。三次握手完成后,MySQL 服务端会主动发一个握手包,包含协议版本号、服务端版本号、连接 ID、认证插件名、密码盐值等信息。客户端拿到这些信息后,会根据认证插件计算出加密后的密码摘要,再回传给服务端。
这里有个有趣的点,就是 MySQL 默认的 3306 端口实际上是“协议端口”,不是“HTTP 端口”,所以你不能用浏览器去访问数据库。连接建立后,双方还会协商能力标志位,比如是否支持 SSL、是否支持压缩传输、字符集是哪种。如果你在客户端指定了utf8mb4,就是在这个阶段告诉服务端的。连接 ID 也很重要,它就是你在SHOW PROCESSLIST里看到的Id列,以后排查“这个连接是谁、在干什么、卡了多久”全靠它。
3.2 身份认证:从 mysql_native_password 到 caching_sha2_password
MySQL 5.7 最常用的是mysql_native_password,它的原理是用SHA1对密码做两次摘要,然后和服务端存储的摘要比对。MySQL 8.0 换成了caching_sha2_password,从名字就能看出它引入了缓存机制,安全性更高,但代价是老客户端不兼容。
实际排查中我会先用这条命令看当前账号用的什么插件:
SELECT user, host, plugin FROM mysql.user;如果输出显示caching_sha2_password,而客户端报Authentication plugin相关错误,那就是客户端版本太老。这一类问题我推荐先升级驱动或客户端,而不是立刻执行ALTER USER ... IDENTIFIED WITH mysql_native_password;去降级。因为降级影响的是整个账号的安全性,你为了一台老机器妥协,等于让所有新机器都承担旧协议的风险。我见过不少团队因为图省事把生产库所有账号都降级了,后来做安全审计时不得不匆匆改回,中间还引发了业务抖动,真不划算。
另外,SSL 在这里的作用也体现出来了。caching_sha2_password在首次认证时需要一条安全通道来传输密码明文,如果连接没走 SSL,它需要通过 RSA 公钥加密来传。这也是为什么在某些特殊网络环境下,MySQL 8 首次连接会特别慢或者报 RSA 相关的错误。如果你确实没法开 SSL,可以在客户端开启allowPublicKeyRetrieval=true(JDBC 参数),但前提是你确认网络环境安全。
3.3 连接建立后的会话配置,以及为什么需要连接池
连接认证通过后,MySQL 会为这个连接分配一个线程,同时设置一堆会话级变量。这些变量只对当前连接生效,比如wait_timeout、transaction_isolation、sql_mode。每个新连接都是“干净”的,不会继承上一个连接改过的变量。这也是为什么在生产环境里“连接池里的连接如果被某个请求改了隔离级别,必须记得复位”的原因,否则下一个请求复用这个连接时,可能拿到完全不符合预期的隔离级别。
说到连接池,我多说两句。每建立一个 MySQL 连接,背后都涉及 TCP 握手、认证、权限检查、线程创建,代价很高。如果你的应用是请求来了就新建连接、请求结束就断开,高并发下数据库会被“连接风暴”直接打垮。所以 Java 应用几乎都会用 HikariCP 或 Druid,Python 那边也用 DBUtils。连接池的核心参数就那么几个:maximumPoolSize最大连接数、minimumIdle最小空闲数、connectionTimeout获取连接的等待时间。
连接池大小不是越大越好,我踩过这个坑。曾有一台小机器我配了 200 个最大连接,数据库瞬间打满,CPU 和内存双双报警。比较稳妥的估算公式是:(CPU核心数 * 2) + 磁盘数,比如四核机器配 8~12 个连接通常够用。连接池还有一个关键职责是“保活”,它会定期执行轻量级查询(比如SELECT 1)来确认连接还活着,避免数据库主动断开后应用拿到一个失效连接。你在日志里看到偶尔的SELECT 1,别慌,那是连接池在探活。
4. 从 SQL 语句到结果集:查询的全过程拆解
4.1 客户端发送 SQL:协议层做了什么
连接建立好,接下来就是发查询。MySQL 客户端发送一条 SQL 时,最常用的是COM_QUERY命令,也就是直接把 SQL 文本发给服务端。如果用预编译语句,则是COM_STMT_PREPARE先发送 SQL 模板,服务端解析后返回一个语句 ID,之后每次执行用COM_STMT_EXECUTE传参数。这也是为什么 JDBC 的PreparedStatement能防止 SQL 注入的核心原因:模板和参数是分离的,参数根本不会拼进 SQL 里。
这里有个容易被忽略的性能点:如果一条 SQL 只是参数不同,但整体结构相同,用预编译语句可以省去重复解析的开销。不过 MySQL 自身有查询缓存相关机制,只是在 8.0 里已经彻底移除了查询缓存,因为它的命中率低且全局竞争严重。所以别指望“同样的 SQL 跑第二次就快”,在 MySQL 8.0 里每次查询都是真正的执行。
4.2 词法分析和语法解析:把字符串变成内部数据结构
服务端收到 SQL 文本后,第一步是词法分析。它会把字符串拆成一个个“单词”,区分出关键字(SELECT、FROM、WHERE)、标识符(表名、列名)、常量(字符串、数字)和运算符。拆完之后再做语法解析,把这些单词按照 MySQL 的语法规则组装成一棵“语法树”。这一步如果出错,会提示You have an error in your SQL syntax,后面的语法高亮和报错位置就是从这里来的。
很多人写 SQL 时不太在意语法树,但理解它对排查问题很有用。比如你写了WHERE status = 'active',如果status列是整数类型,MySQL 在词法解析后做语义分析时,会把字符串'active'转成数字,转换失败就变成 0,然后给你返回一堆意料之外的行。这类“逻辑错误”不是语法错误,它报错都不会报,只能靠你经验判断。
4.3 预处理与权限核实:表存在不代表你能查
语法树生成后,MySQL 还要做语义检查,也就是预处理阶段。它会检查 SQL 里引用的表是否存在、列是否存在、列名有没有歧义,还会解析*到底代表哪些列。如果视图涉及底层表,也是在这个阶段展开的。也就是说,你写错列名时,MySQL 不是等到执行才报错,而是在预处理阶段就拦下来了。
紧接着是权限验证。MySQL 会根据当前用户对库、表、列、存储过程等对象的权限,判断这次查询是否被允许。这里要注意,MySQL 的权限检查是“分步”的,比如SELECT时需要表级别的SELECT权限,但如果查询涉及存储函数,可能还需要EXECUTE权限。权限检查失败会报SELECT command denied to user,此时别急着改密码,先想清楚当前账号到底被授予了什么。
4.4 查询优化器:怎么选索引、怎么定执行计划
预处理通过后,就轮到优化器做主了。MySQL 优化器的目标是找到一个“代价最低”的执行计划。它会分析语法树、统计信息、索引分布,计算各种访问路径的代价,包括走哪个索引、是否做排序、怎么关联多张表、是否要临时表。我们平时执行EXPLAIN看到的输出,就是优化器最终选定的执行计划。
举一个最常见的场景:SELECT * FROM orders WHERE user_id = 123 ORDER BY create_time DESC。如果user_id有索引,create_time没有索引,优化器可能选择走user_id索引拿到数据,再对create_time做一次文件排序;如果user_id选择性不高,可能直接选择全表扫描反而更快。优化器到底怎么选,跟你统计信息是否准确非常相关。表数据量变化大时,ANALYZE TABLE重新收集统计信息,往往能解决“ SQL 一样,但这两天突然变慢”的问题。
优化器不是万能的,它也会犯傻。比如它可能低估了一个大IN列表的代价,或者对关联顺序判断失误。这时候就需要人工介入,用STRAIGHT_JOIN强制关联顺序,或者给优化器提示USE INDEX/FORCE INDEX。但这些手段是最后的应急方案,不要一开始就用,否则表结构一变,这些硬编码很可能变成负优化。还有一个老生常谈的点:EXPLAIN并不代表语句真正执行时的所有细节,比如它不会告诉你运行时的行锁等待、临时表落盘等等,但它仍然是入门诊断的第一工具。
4.5 执行器与存储引擎:最后真正干活的角色
拿到执行计划后,执行器才开始真正干活。你可以把优化器理解成“制定作战计划”,执行器是“前线指挥”,存储引擎是“真正跑腿的士兵”。执行器会通过存储引擎的接口逐行读取数据,然后进行条件过滤、投影、排序、聚合、联表等操作。如果涉及事务,行锁、MVCC、undo log 这些也是在存储引擎层完成的。InnoDB 是绝大部分业务表的默认引擎,所以网上讨论存储引擎时基本都在说 InnoDB。
执行过程中有几个细节值得了解。比如全表扫描时,执行器会一行一行调用存储引擎接口,所以 InnoDB 也有“预读”机制,一次从磁盘读入一批页到缓冲池,减少 IO 次数。MySQL 8.0 还加入了“并行扫描”的能力,不过默认并不总是启用。再比如,如果ORDER BY或者GROUP BY无法用到索引,执行器会把中间结果写到临时表,数据量大时临时表会从内存转到磁盘,也就是Using temporary; Using filesort。看到EXPLAIN的Extra列里有这个,就意味着这条查询的排序或分组有优化空间。
返回结果集也不是一次全给完的。执行器会把结果逐条写入网络发送缓冲区,然后由服务端陆续发给客户端。客户端收到多少取决于网络包大小,如果单行数据过大,超了max_allowed_packet,就会报Packet too large。所以如果你查大字段(比如TEXT、BLOB)时遇到这个报错,优先考虑是不是这个参数太小,而不是数据库坏了。对应用来说,读取大结果集时也要注意分批处理,别一次性fetch几十万行到内存里。
5. 连接和查询中最常遇到的拦路虎,附排查顺序
5.1 连不上数据库:4 个错误码一次说清
我做了一个比较全面的故障速查表,按我自己的排查顺序排的,遇到连不上问题可以直接对着看:
| 错误码 | 错误含义 | 常见原因 | 排查方向 |
|---|---|---|---|
| 2003 | Can't connect to MySQL server | 网络不通、端口未监听、防火墙拦截 | ping主机、telnet ip 3306、检查服务状态 |
| 1045 | Access denied for user | 用户名、密码错误,或账号不存在 | 确认CURRENT_USER(),检查mysql.user |
| 1130 | Host is not allowed to connect | 账号的 host 限制包含不了当前来源 IP | 改用'user'@'%'或'user'@'具体IP' |
| 2059 | Authentication plugin 错误 | 客户端版本与 8.0 认证插件不匹配 | 升级客户端,或暂时降级认证插件 |
最典型的场景是 2003 和 1130 搞混:2003 是“找不到服务器”,1130 是“找到服务器但你不被允许进”。我从日志里看到过很多次,用户明明改的是账号权限,却一直在调防火墙。正确顺序一定是:先确认网络通不通,再看账号 host,最后看密码。别一上来就重置密码,那样会把问题搞得更乱。
SSL 相关报错我也把它归到连接阶段的疑难杂症里。它通常表现为“某些客户端能连、某些不能连”,或者在公网环境连接极慢。原因绝大多数是证书链不被信任、主机名和证书不匹配、TLS 版本协商失败。排障时先看客户端日志里有没有SSL字样,再用mysql --ssl-mode=REQUIRED测试服务端 SSL 是否可用,一步步缩小范围。
5.2 连上了但查询很慢:先从 EXPLAIN 下手
连接没问题,SQL 却慢,这是我日常花时间最多的地方。我处理这类问题的固定套路是“三段式”:先看SHOW PROCESSLIST确认语句在做什么,再执行EXPLAIN看执行计划,最后看慢查询日志确认耗时趋势。
SHOW PROCESSLIST的State列非常关键。常见状态包括Sending data、Sorting result、Waiting for table metadata lock、Waiting for handler commit等。如果你看到大量线程处于Sending data里时间很长,多半是执行器确实在扫描大量行;如果是Waiting for table metadata lock,说明有DDL语句卡住了后面的所有查询,这时候要找持有锁的会话。Waiting for handler commit则大概率是磁盘 IO 慢或者 binlog 刷盘慢。
EXPLAIN里我最先看的是type列,它从好到差大致是const、eq_ref、ref、range、index、ALL。如果出现ALL,意味着全表扫描,需要确认是否能走索引;再看possible_keys和key,前者代表可能用到的索引,后者代表实际用的索引,两列不一致时就要思考优化器为什么没选你预期的索引。Extra里的Using filesort、Using temporary也需要重点关注。
我看过一条典型的慢查询:SELECT * FROM order_detail WHERE order_id IN (SELECT id FROM orders WHERE user_id = 123456)。这条 SQL 的执行计划里,子查询可能被改写成semi-join,如果优化器没选好,效率会很低。这个场景下EXISTS和IN的结果很多时候是等价的,但代价可能完全不同,不能用“哪个关键字快”一概而论,一定要结合执行计划判断。同样地,去重也想提醒一下,SELECT DISTINCT和GROUP BY在某些情况下可以互相替代,但如果DISTINCT导致Using temporary,那就要考虑是不是可以通过索引来消除排序。
5.3 索引失效:几个让优化器“眼瞎”的典型写法
索引建了却没走,很多人第一个反应是“MySQL 优化器太笨”,其实大部分情况是写法有问题。我归纳了几个最常见的失效场景,全部是我真实遇到过的:
- 对索引列做函数运算,比如
WHERE DATE(create_time) = '2025-01-01',这时索引列被函数包裹,优化器无法按索引范围扫描。正确写法是WHERE create_time >= '2025-01-01 00:00:00' AND create_time < '2025-01-02 00:00:00'。 - 隐式类型转换,比如
WHERE phone = 13800138000,而phone是 VARCHAR。MySQL 会把字符串列转成数字,导致无法走索引。 - 左前置模糊匹配,
LIKE '%abc'无法使用索引,LIKE 'abc%'可以。模糊查询如果非用前置匹配不可,建议考虑全文索引或者搜索引擎。 - 多列索引时没有遵循最左前缀原则,比如在
(a, b, c)联合索引上只查c,索引当然用不上。
要想验证是不是索引问题,我推荐一个习惯:写EXPLAIN看执行计划时,顺便看key_len列。key_len表示优化器实际使用的索引长度,同样是联合索引(a, b, c),如果key_len比预期短,说明只用了部分列,那后面的列其实没有参与过滤,这时候就要检查查询条件是否满足最左前缀。
5.4 存储过程和批量更新对连接的影响
有些同学会把复杂业务封装到存储过程里,然后应用层像调远程函数一样CALL。我建议先想清楚一个问题:存储过程虽然能减少网络往返,但它在数据库里执行意味着把 CPU 压力放在数据库上,而数据库恰恰是横向扩展最困难的组件。应用服务器不够了可以随便加,但数据库瓶颈就不那么好解决。所以存储过程适合那些强一致、需要事务边界包裹的批量逻辑,不适合当“万能函数库”来用。
批量更新场景也常和连接、事务绑在一起。默认情况下,InnoDB 行锁是“加锁即生效,事务结束才释放”,如果一条更新语句涉及很多行,持锁时间会很长。曾经有人用一个循环,每次更新查一次数据库,把 10 万条数据逐条更新,结果全程跑了二十分钟。后来我改成批量拼接UPDATE ... CASE WHEN或者分批次提交,时间降到了几十秒。这里的关键不是 SQL 本身多华丽,而是减少事务持锁时间,避免把其他查询全堵住。
5.5 系统表的排查姿势:processlist 和 performance_schema
排查连接和查询问题时,光靠EXPLAIN还不够,你得会用系统表。SHOW PROCESSLIST看的是瞬时状态,而performance_schema有历史累计数据。比如我想看哪种 SQL 是吃 CPU 大户,可以查:
SELECT DIGEST_TEXT, COUNT_STAR, SUM_TIMER_WAIT FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;这条语句能按“归一化后的 SQL 摘要”统计各类语句的总执行时间,秒级定位到最耗时的 SQL 类别。相比慢查询日志,它的数据是内存表,查询更快,也不会产生大量落盘日志。当然慢查询日志也有不可替代的作用,尤其是你想看某条 SQL 在不同时间点的耗时曲线时,日志里每一行都有时间戳。
performance_schema里我还经常用events_waits_summary_global_by_event_name看 IO 等待,用table_io_waits_summary_by_table看哪张表的读写最频繁。这些数据在排障时是“证据链”,能帮你把“数据库慢”这个模糊描述,转化成一个可执行的具体问题,比如“某张表的 scan 行数过大,需要加索引”或者“某类 SQL 的锁等待时间太长,需要缩短事务”。
6. 个人保留的几点经验,写在最后
这条链路走完,你会发现自己再遇到“数据库慢”“连不上”这类问题时,脑海里会有一个清晰的坐标:问题出在连接阶段、解析阶段、优化器阶段,还是执行器阶段?这比抓瞎翻日志要高效太多。
分享两个我一直坚持的小习惯。第一,所有代码里的连接串,必须显式配置超时时间和字符集,不依赖默认值。默认值在当前版本下或许没问题,但数据库一升级、网络拓扑一调整,问题就会毫无预兆地爆发。第二,每写完一条有一定复杂度的 SQL,至少跑一次EXPLAIN,看一眼type、key、rows这前三列。这个习惯坚持半年,你对“索引到底怎么生效”的理解会远超大多数同事。
最后补一个我踩过很多次才长记性的点:不要在事务里做远程调用或者等待外部接口返回。曾经有人在一个事务里调用支付回调接口,一等就是几秒,结果数据库连接被占住不放,连接池被迅速耗尽,整个应用跟着崩。数据库事务是宝贵资源,里面只放数据库操作,任何外部 IO(HTTP、Redis、MQ)都要拿到事务外面去做。这条原则理解了,比我前面讲的很多参数都管用。