### MySQL源码学习:关于慢查询日志中的Rows_examined=0
在MySQL数据库的性能优化过程中,慢查询日志(Slow Query Log)是开发者与DBA的重要工具。它记录了执行时间超过阈值的SQL语句,帮助定位性能瓶颈。然而,在分析慢查询日志时,我们可能会遇到一个看似矛盾的现象:某些查询被记录为慢查询(即执行时间超过`long_query_time`),但`Rows_examined`字段的值却为0。这一现象容易引发困惑——为什么没有扫描任何行的查询会被判定为“慢”?本文将从MySQL源码层面深入剖析这一现象的成因,并结合实际案例与优化建议,为读者提供全面的技术解析。
#### 一、慢查询日志与Rows_examined的基础概念
慢查询日志是MySQL提供的一种性能监控机制,通过配置`slow_query_log`和`long_query_time`参数,可以记录执行时间超过指定阈值的SQL语句。每条日志记录包含以下关键信息:
-
# Query_time
: 查询总执行时间(秒) -
# Lock_time
: 等待锁的时间(秒) -
# Rows_sent
: 返回给客户端的行数 -
# Rows_examined
: 服务器层扫描的行数
其中,`Rows_examined`通常用于衡量查询的I/O开销。理论上,该值越大,查询可能越低效。但当`Rows_examined=0`时,意味着查询未通过全表扫描或索引扫描获取数据,这似乎与“慢查询”的直观认知矛盾。
#### 二、Rows_examined=0的典型场景分析
通过实际案例与源码追踪,我们发现`Rows_examined=0`的慢查询通常出现在以下场景中:
##### 场景1:纯计算或元数据操作
某些SQL语句不涉及数据行扫描,例如:
SELECT 1 + 1;
SELECT DATABASE();
这类查询仅在服务器层执行计算或获取元数据,无需访问存储引擎,因此`Rows_examined`自然为0。但若查询包含复杂函数或触发隐式转换,可能导致执行时间超限。
##### 场景2:未命中任何行的条件查询
考虑以下查询:
SELECT * FROM users WHERE id = -1; -- 假设id为无符号整数
由于条件`id=-1`与列类型不匹配(无符号整数无法为负),存储引擎可能直接返回空结果集,未扫描任何行。但若查询涉及大量索引检查或锁等待,仍可能被记录为慢查询。
##### 场景3:DDL操作与事务等待
DDL语句(如`ALTER TABLE`)或长时间运行的事务可能导致其他查询阻塞。例如:
-- 会话1执行耗时操作
BEGIN;
ALTER TABLE large_table ADD COLUMN new_col INT;
-- 会话2的查询被阻塞
SELECT * FROM large_table WHERE id = 100; -- 可能因锁等待成为慢查询
此时,会话2的查询可能因等待锁而超时,但实际未扫描任何行(`Rows_examined=0`)。
##### 场景4:存储引擎层优化
某些存储引擎(如InnoDB)可能通过覆盖索引或提前终止扫描优化查询。例如:
-- 假设name列有索引
SELECT name FROM users WHERE id = 100 AND name = 'Alice';
若索引包含`(id, name)`,存储引擎可能直接通过索引定位数据,无需回表扫描,导致`Rows_examined=0`。但若索引选择不当或数据分布不均,查询仍可能低效。
#### 三、源码级解析:Rows_examined的统计机制
要深入理解`Rows_examined=0`的成因,需从MySQL源码中追踪该值的统计逻辑。关键代码位于`sql/sql_class.h`和`storage/innobase/handler/ha_innodb.cc`中。
##### 1. 服务器层统计入口
在`THD`类(线程描述符)中,`examined_rows`变量记录扫描行数。其增量逻辑在`JOIN::execute`和`execute_sqliter_select`等函数中触发。例如:
// sql/sql_select.cc
void JOIN::execute() {
...
if (table->file->ha_rnd_next(&row)) { // 全表扫描时递增
examined_rows++;
}
...
}
若查询未通过`ha_rnd_next`或`ha_index_read`等函数访问数据,`examined_rows`不会增加。
##### 2. InnoDB存储引擎的交互
InnoDB通过`handler`接口与服务器层交互。在`ha_innodb.cc`中,`rows_examined`的统计依赖于以下方法:
-
ha_innobase::rnd_next()
: 全表扫描时递增 -
ha_innobase::index_read()
: 索引扫描时递增
若查询通过“零行扫描”优化(如索引条件过滤、唯一键查找等),这些方法可能不被调用,导致`Rows_examined=0`。
##### 3. 锁等待与时间统计
慢查询的判定基于`Query_time`,而该时间包含锁等待、网络延迟等非数据扫描开销。例如,在`lock.cc`中:
// storage/innobase/lock/lock0lock.cc
void lock_wait_suspend_thread(...) {
...
thd->set_time_after_lock(); // 增加锁等待时间
...
}
即使`Rows_examined=0`,长时间的锁等待仍可能导致`Query_time`超限。
#### 四、实际案例分析与优化建议
##### 案例1:隐式类型转换导致全表扫描
查询:
SELECT * FROM orders WHERE customer_id = '123'; -- customer_id为INT类型
现象:`Rows_examined`为表总行数(全表扫描),但若优化器错误选择执行计划,可能因锁等待或数据量大成为慢查询。若优化器成功使用索引,则可能`Rows_examined=0`(但实际较少见)。
优化:显式类型转换:
SELECT * FROM orders WHERE customer_id = CAST('123' AS UNSIGNED);
##### 案例2:未使用索引的OR条件
查询:
SELECT * FROM products WHERE name = 'Apple' OR category_id = 5;
现象:若`name`和`category_id`无联合索引,可能导致全表扫描。但若数据分布特殊(如无匹配行),可能`Rows_examined=0`。
优化:重写为UNION或使用索引合并:
SELECT * FROM products WHERE name = 'Apple'
UNION
SELECT * FROM products WHERE category_id = 5;
##### 案例3:事务隔离级别与锁竞争
场景:在RR隔离级别下,长时间运行的事务持有间隙锁,导致其他查询阻塞。
优化:缩短事务时间、降低隔离级别或优化锁粒度。
#### 五、总结与最佳实践
1. **综合分析慢查询**:不要仅依赖`Rows_examined`,需结合`Query_time`、`Lock_time`和执行计划(`EXPLAIN`)综合判断。
2. **监控锁等待**:通过`performance_schema`或`information_schema.INNODB_LOCKS`定位锁竞争源。
3. **优化执行计划**:使用索引提示(如`FORCE INDEX`)或重写查询避免全表扫描。
4. **定期维护统计信息**:执行`ANALYZE TABLE`更新索引统计,避免优化器误判。
5. **升级MySQL版本**:新版本可能优化零行扫描的统计逻辑,减少误报。
### 关键词
MySQL源码、慢查询日志、Rows_examined、锁等待、执行计划、存储引擎、性能优化
### 简介
本文深入探讨MySQL慢查询日志中`Rows_examined=0`的成因,通过源码分析、实际案例与优化建议,揭示纯计算操作、未命中行查询、锁等待及存储引擎优化等场景下的现象本质,并提供综合性能调优方法。