位置: 文档库 > 数据库 > MySQL源码学习:关于慢查询日志中的Rows_examined=0

MySQL源码学习:关于慢查询日志中的Rows_examined=0

冬雪白头 上传于 2020-08-20 02:14

### 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`的成因,通过源码分析、实际案例与优化建议,揭示纯计算操作、未命中行查询、锁等待及存储引擎优化等场景下的现象本质,并提供综合性能调优方法。

《MySQL源码学习:关于慢查询日志中的Rows_examined=0.doc》
将本文的Word文档下载到电脑,方便收藏和打印
推荐度:
点击下载文档