1. 问题现象:主键查询为何全表扫描?
上周排查一个线上慢查询问题时,遇到个有意思的案例:某个根据主键ID查询的简单SQL,执行时竟然走了全表扫描。表数据量在200万左右,全表扫描导致查询耗时从毫秒级暴涨到秒级。更诡异的是,这个SQL在测试环境执行计划完全正常。
问题SQL非常简单:
sql复制SELECT * FROM user_info WHERE user_id = '10086';
user_id字段是表的主键,数据类型为varchar(32),存储的是字符串形式的用户ID。表结构如下:
sql复制CREATE TABLE `user_info` (
`user_id` varchar(32) NOT NULL,
`user_name` varchar(64) DEFAULT NULL,
-- 其他字段...
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 排查过程:从执行计划到字符集
2.1 执行计划分析
首先用EXPLAIN查看执行计划,关键信息如下:
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user_info | ALL | PRIMARY | NULL | NULL | NULL | 198743 | Using where |
确实走了全表扫描(type=ALL),没有使用主键索引。但同样的SQL在测试环境执行计划是:
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | user_info | const | PRIMARY | PRIMARY | 130 | const | 1 | NULL |
2.2 参数对比
排查方向转向环境差异。比较了线上和测试环境的MySQL配置参数:
- 索引统计信息:
ANALYZE TABLE后问题依旧 - 优化器开关:
optimizer_switch配置完全一致 - SQL模式:
sql_mode相同 - 字符集设置:发现关键差异!
测试环境连接字符集:
sql复制SHOW VARIABLES LIKE 'character_set%';
-- character_set_client = utf8mb4
-- character_set_connection = utf8mb4
线上环境连接字符集:
sql复制SHOW VARIABLES LIKE 'character_set%';
-- character_set_client = utf8
-- character_set_connection = utf8
2.3 字符集隐式转换
问题根源浮出水面:字符集不匹配导致隐式类型转换。具体机制:
- 线上环境连接使用
utf8字符集 - 表字段是
utf8mb4字符集 - 当比较
utf8和utf8mb4时,MySQL会将utf8转换为utf8mb4 - 这种转换会使索引失效
验证这个结论很简单,在测试环境临时修改连接字符集:
sql复制SET character_set_client = 'utf8';
SET character_set_connection = 'utf8';
EXPLAIN SELECT * FROM user_info WHERE user_id = '10086';
果然复现了全表扫描的问题。
3. 原理深入:MySQL的字符集转换规则
3.1 字符集转换优先级
MySQL处理字符集转换时遵循"小字符集向大字符集转换"的原则:
utf8是3字节编码,utf8mb4是4字节编码- 比较不同字符集时,MySQL会将"较小"的字符集转换为"较大"的字符集
- 这种转换发生在列值上,而非参数值上
3.2 索引失效的根本原因
当发生WHERE 列 = 值比较时:
- 如果值的字符集与列不同,MySQL会对列值进行转换
- 对列使用函数操作会导致索引失效(经典索引失效场景)
- 虽然字符集转换是隐式的,但效果等同于
CONVERT(列 USING utf8mb4) = 值
3.3 字符集与排序规则
更深层的原因还涉及排序规则(collation):
- 每种字符集有默认的排序规则(如
utf8mb4_general_ci) - 比较不同字符集时,实际比较的是转换后的二进制值
- 这种转换破坏了索引的有序性,导致优化器放弃使用索引
4. 解决方案与最佳实践
4.1 即时修复方案
对于当前问题,有三种解决方案:
- 修改连接字符集(推荐):
sql复制SET character_set_client = 'utf8mb4';
SET character_set_connection = 'utf8mb4';
- 强制类型转换:
sql复制SELECT * FROM user_info WHERE user_id = CONVERT('10086' USING utf8mb4);
- 使用二进制字符串:
sql复制SELECT * FROM user_info WHERE user_id = _utf8mb4 '10086';
4.2 长期最佳实践
-
统一字符集配置:
- 确保应用连接配置
character_set_client=utf8mb4 - JDBC连接串添加:
useSSL=false&characterEncoding=utf8mb4
- 确保应用连接配置
-
表设计规范:
- 新表统一使用
utf8mb4字符集 - 确保
CREATE TABLE和ALTER TABLE显式指定字符集
- 新表统一使用
-
开发规范:
- 避免在SQL中混合不同字符集的字符串
- 框架层统一处理字符串编码
-
监控方案:
sql复制-- 查找可能存在字符集不匹配的表 SELECT table_schema, table_name, column_name, character_set_name FROM information_schema.columns WHERE character_set_name = 'utf8mb4' AND table_schema NOT IN ('mysql', 'information_schema', 'performance_schema');
5. 扩展知识:其他隐式转换场景
5.1 数值与字符串转换
sql复制-- 如果user_id是字符串类型,数字比较会导致索引失效
SELECT * FROM user_info WHERE user_id = 10086;
5.2 日期格式转换
sql复制-- 如果create_time是datetime,使用字符串比较可能导致问题
SELECT * FROM orders WHERE create_time = '2023-05-01';
5.3 不同排序规则比较
sql复制-- 如果collation不匹配也会导致索引失效
SELECT * FROM products
WHERE name COLLATE utf8mb4_general_ci = '手机' COLLATE utf8mb4_bin;
6. 诊断工具与技巧
6.1 执行计划分析要点
- 关注
type列:ALL表示全表扫描,const/eq_ref表示高效索引使用 - 检查
key列:显示实际使用的索引,NULL表示未使用索引 Extra列中的Using where通常伴随全表扫描
6.2 性能诊断工具
-
EXPLAIN FORMAT=JSON:
sql复制EXPLAIN FORMAT=JSON SELECT * FROM user_info WHERE user_id = '10086';输出中包含
attached_condition会显示实际比较条件 -
Optimizer Trace:
sql复制SET optimizer_trace="enabled=on"; SELECT * FROM user_info WHERE user_id = '10086'; SELECT * FROM information_schema.optimizer_trace; -
字符集检查脚本:
sql复制SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema');
7. 生产环境教训总结
-
多环境一致性检查:开发、测试、生产环境的MySQL配置必须保持一致,特别是字符集相关参数
-
SQL审查要点:
- WHERE条件中的类型匹配
- JOIN条件的字符集一致性
- 索引列避免函数操作
-
监控指标:
- 慢查询日志中的全表扫描SQL
- 执行计划突变告警
-
设计阶段规避:
- 新建表统一使用utf8mb4
- 应用连接池统一配置字符集
- 框架层做好字符串类型处理
这个案例给我的深刻教训是:数据库性能问题有时隐藏在意想不到的细节里。字符集这种看似基础的配置,一旦出现不一致就可能引发严重的性能问题。建议将字符集检查纳入数据库上线前的标准检查项。