MySQL 用了索引还是很慢的原因分析及解决方案

 更新时间:2026年05月20日 09:21:14   作者:Java  
本文给大家分享即使查询使用了索引,仍然可能很慢,主要原因包括索引设计问题、SQL写法问题、数据分布问题、表/O开销大、数据库配置问题和硬件资源瓶颈等,感兴趣的朋友跟随小编一起看看吧

即使查询使用了索引,仍然可能很慢,主要原因可以归纳为 6 大类:

类别具体原因影响
索引设计问题索引选择性差、索引列顺序不当、索引冗余扫描行数过多
SQL 写法问题SELECT *LIKE '%xxx'、函数操作、类型转换索引失效或回表过多
数据分布问题数据量过大、数据倾斜严重、热点数据集中即使走索引也慢
表结构问题字段过大、行过长、未分区I/O 开销大
数据库配置问题缓冲池过小、连接数不足、参数配置不当整体性能下降
硬件资源瓶颈磁盘 I/O、内存不足、CPU 瓶颈物理层面慢

一句话总结:索引只是加速查询的必要条件而非充分条件,需要结合索引设计、SQL 优化、表结构设计、数据库配置和硬件资源进行系统性优化。

一、索引设计问题

1. 索引选择性差

索引选择性对查询性能的影响。选择性越高,索引的效果越好;选择性越低,索引的效果越差。

  • 高选择性:例如用户表的 user_id 字段,100 万用户有 100 万个不同的 user_id,选择性为 1.0,查询时可以快速定位到唯一行。
  • 低选择性:例如性别字段 gender,只有 "男" 和 "女" 两个值,100 万行数据的选择性仅为 0.000002,查询 "男" 性用户时需要扫描约 50 万行,数据库优化器可能直接选择全表扫描。

判断标准:一般建议索引选择性大于 0.1,可以通过以下 SQL 计算:

​
-- 计算字段的选择性
SELECT
    COUNT(DISTINCT column_name) / COUNT(*) AS selectivity
FROM table_name;
-- 示例:gender 字段的选择性
SELECT
    COUNT(DISTINCT gender) / COUNT(*) AS selectivity
FROM users;
-- 结果:0.000002(非常低,不适合单独建索引)
​

2. 索引列顺序不当(最左前缀原则)

-- 创建联合索引
CREATE INDEX idx_name_age_gender ON users(name, age, gender);
-- ✅ 走索引:符合最左前缀
SELECT * FROM users WHERE name = '张三';
SELECT * FROM users WHERE name = '张三' AND age = 25;
SELECT * FROM users WHERE name = '张三' AND age = 25 AND gender = '男';
-- ❌ 不走索引:违反最左前缀
SELECT * FROM users WHERE age = 25;              -- 缺少 name
SELECT * FROM users WHERE gender = '男';         -- 缺少 name 和 age
SELECT * FROM users WHERE age = 25 AND gender = '男'; -- 缺少 name
-- ⚠️ 部分走索引:只有 name 走索引,后面的列无法利用索引排序
SELECT * FROM users WHERE name = '张三' AND gender = '男'; -- age 跳过了

核心原则:联合索引要遵循 "最左前缀原则",索引列的顺序非常重要。将区分度高、经常用于查询条件的列放在左边。

3. 索引冗余

-- ❌ 冗余索引示例
CREATE INDEX idx_name ON users(name);                    -- 单列索引
CREATE INDEX idx_name_age ON users(name, age);           -- 联合索引
-- idx_name 是冗余的,因为 idx_name_age 可以覆盖 name 单列查询
-- ✅ 优化后:删除冗余索引
DROP INDEX idx_name ON users;
-- 只保留联合索引 idx_name_age

二、SQL 写法问题

1.SELECT *导致大量回表

 SELECT * 查询的回表过程。如果查询只需要部分字段,但使用了 SELECT *,就会导致不必要的回表操作,严重影响性能。

  • 回表过程:先在二级索引(如 name 索引)中找到满足条件的主键 ID,然后根据主键 ID 到聚簇索引中查找完整的行数据。
  • 性能影响:如果查询返回 1000 行数据,就需要进行 1000 次回表操作,每次回表都是一次随机 I/O,性能开销巨大。

优化方案:使用覆盖索引,只查询索引列,避免回表。

-- ❌ 慢查询:需要回表
SELECT * FROM users WHERE name = '张三';
-- ✅ 优化:使用覆盖索引,无需回表
-- 假设有索引 idx_name_age(name, age)
SELECT name, age FROM users WHERE name = '张三';

2.LIKE查询导致索引失效

-- ✅ 走索引:前缀匹配
SELECT * FROM users WHERE name LIKE '张%';
-- ❌ 不走索引:后缀匹配或包含匹配
SELECT * FROM users WHERE name LIKE '%张';    -- 后缀匹配
SELECT * FROM users WHERE name LIKE '%张%';   -- 包含匹配
-- ✅ 优化方案 1:使用覆盖索引
-- 即使 LIKE '%张%' 不走索引,如果只查询索引列,可能走索引扫描
SELECT name FROM users WHERE name LIKE '%张%';
-- ✅ 优化方案 2:使用全文索引
ALTER TABLE users ADD FULLTEXT INDEX ft_name(name);
SELECT * FROM users WHERE MATCH(name) AGAINST('张');
-- ✅ 优化方案 3:使用搜索引擎(如 Elasticsearch)

3. 对索引列使用函数或计算

-- ❌ 不走索引:对索引列使用函数
SELECT * FROM users WHERE DATE(create_time) = '2024-01-01';
SELECT * FROM users WHERE YEAR(create_time) = 2024;
SELECT * FROM users WHERE SUBSTRING(name, 1, 1) = '张';
-- ✅ 走索引:使用范围查询或等值查询
SELECT * FROM users WHERE create_time >= '2024-01-01'
                       AND create_time < '2024-01-02';
-- ❌ 不走索引:索引列参与计算
SELECT * FROM users WHERE age + 1 = 26;
-- ✅ 走索引:调整计算方式
SELECT * FROM users WHERE age = 25;

4. 隐式类型转换

-- 假设 user_id 是 VARCHAR 类型
CREATE INDEX idx_user_id ON users(user_id);
-- ❌ 不走索引:字符串与数字比较,发生隐式类型转换
SELECT * FROM users WHERE user_id = 123;
-- 等价于:SELECT * FROM users WHERE CAST(user_id AS SIGNED) = 123;
-- ✅ 走索引:类型一致
SELECT * FROM users WHERE user_id = '123';

核心原则:索引列的类型必须与查询条件的类型完全一致,否则会发生隐式类型转换,导致索引失效。

三、数据分布问题

1. 数据量过大

-- 查询表的总行数和大小
SELECT
    table_name,
    table_rows,
    ROUND(data_length / 1024 / 1024, 2) AS data_size_mb,
    ROUND(index_length / 1024 / 1024, 2) AS index_size_mb
FROM information_schema.tables
WHERE table_schema = 'your_database';

优化方案:

  • 分区表:按时间或范围分区,减少单次查询扫描的数据量
  • 分表分库:将大表拆分为多个小表
  • 归档历史数据:将历史数据迁移到归档表
  • 使用覆盖索引:减少回表次数

2. 数据倾斜严重

-- 查看数据分布
SELECT gender, COUNT(*) as count
FROM users
GROUP BY gender;
-- 假设结果:
-- gender | count
-- -------|--------
-- 男     | 999000
-- 女     |   1000
-- 其他   |      0
-- 查询 "男" 性用户时,即使有索引,也可能走全表扫描
-- 因为优化器认为扫描 999000 行和全表扫描差不多

优化方案:

  • 对于极端倾斜的数据,考虑不建索引或使用复合索引
  • 使用 FORCE INDEX 强制走索引(谨慎使用)
-- 强制使用索引
SELECT * FROM users FORCE INDEX(idx_gender) WHERE gender = '男';

四、表结构问题

1. 字段过大

-- ❌ 慢查询:大字段导致行过长
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,           -- 大字段
    author VARCHAR(100),
    create_time DATETIME
);
-- ✅ 优化:将大字段拆分到单独的表
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(255),
    author VARCHAR(100),
    create_time DATETIME
);
CREATE TABLE article_contents (
    article_id INT PRIMARY KEY,
    content LONGTEXT,
    FOREIGN KEY (article_id) REFERENCES articles(id)
);

2. 未使用合适的字段类型

-- ❌ 不合理:使用 VARCHAR 存储 IP 地址
CREATE TABLE logs (
    id INT PRIMARY KEY,
    ip VARCHAR(15)
);
-- ✅ 优化:使用 INT UNSIGNED 存储 IP 地址
CREATE TABLE logs (
    id INT PRIMARY KEY,
    ip INT UNSIGNED
);
-- 插入时转换
INSERT INTO logs (ip) VALUES (INET_ATON('192.168.1.1'));
-- 查询时转换
SELECT INET_NTOA(ip) FROM logs;

五、数据库配置问题

1. 缓冲池配置不当

-- 查看当前 InnoDB 缓冲池大小
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 建议设置为服务器内存的 70%-80%
-- 例如:16GB 内存的服务器,设置为 12GB
SET GLOBAL innodb_buffer_pool_size = 12884901888;  -- 12GB

2. 其他重要参数

-- 查看当前配置
SHOW VARIABLES LIKE 'innodb_io_capacity';        -- 磁盘 I/O 能力
SHOW VARIABLES LIKE 'innodb_read_io_threads';    -- 读线程数
SHOW VARIABLES LIKE 'innodb_write_io_threads';   -- 写线程数
SHOW VARIABLES LIKE 'max_connections';           -- 最大连接数
-- 根据服务器配置调整
-- SSD 硬盘可以设置更高的 innodb_io_capacity

六、使用 EXPLAIN 分析执行计划

-- 查看执行计划
EXPLAIN SELECT * FROM users WHERE name = '张三';
-- 关键字段解读
-- id: 查询标识符
-- select_type: 查询类型(SIMPLE, PRIMARY, SUBQUERY 等)
-- table: 访问的表
-- type: 访问类型(从好到差:system > const > eq_ref > ref > range > index > ALL)
-- possible_keys: 可能使用的索引
-- key: 实际使用的索引
-- key_len: 使用的索引长度
-- rows: 预估扫描的行数
-- Extra: 额外信息(Using index, Using where, Using filesort 等)

重点关注:

  • type 字段:如果是 ALL,说明是全表扫描;如果是 index,说明是索引扫描
  • rows 字段:预估扫描的行数,越大越慢
  • Extra 字段:Using filesort(文件排序)、Using temporary(临时表)都是性能杀手

到此这篇关于MySQL 用了索引还是很慢,可能是什么原因?的文章就介绍到这了,更多相关mysql索引还是很慢内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 使用sysbench来测试MySQL性能的详细教程

    使用sysbench来测试MySQL性能的详细教程

    这篇文章主要介绍了使用sysbench来测试MySQL性能的详细教程,包括介绍了从sysbench的编译安装到初始化测试库环境等一系列操作,需要的朋友可以参考下
    2015-05-05
  • SQL中Limit的用法及注意事项

    SQL中Limit的用法及注意事项

    LIMIT关键字是SQL中一个非常有用的工具,它可以用来限制查询结果返回的记录数量,实现数据的分页,或者从复杂查询中获取特定的记录,本文给大家介绍SQL中Limit的用法,感兴趣的朋友一起看看吧
    2025-04-04
  • MySQL备份与恢复方案之mysqldump与xtrabackup详解

    MySQL备份与恢复方案之mysqldump与xtrabackup详解

    本文介绍了两种常用的MySQL备份工具:mysqldump和xtrabackup,mysqldump是逻辑备份工具,适用于小型数据库和开发测试环境,而xtrabackup是物理备份工具,适用于生产环境中的大型数据库
    2026-02-02
  • Mysql optimize table 时报错:Temporary file write fail的解决

    Mysql optimize table 时报错:Temporary file write fail的解决

    这篇文章主要介绍了Mysql optimize table 时报错:Temporary file write fail的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • mysql递归查询语法WITH RECURSIVE的使用

    mysql递归查询语法WITH RECURSIVE的使用

    本文主要介绍了mysql递归查询语法WITH RECURSIVE的使用,WITH RECURSIVE用于执行递归查询,特别适合处理层级结构或递归数据,具有一定的参考价值,感兴趣的可以了解一下
    2025-05-05
  • 简述MySQL与Oracle的区别

    简述MySQL与Oracle的区别

    Oracle是大型数据库而Mysql是中小型数据库,Oracle市场占有率达40%,Mysql只有20%左右,同时Mysql是开源的而Oracle价格非常高
    2017-06-06
  • MySQL事务的ACID特性以及并发问题方案

    MySQL事务的ACID特性以及并发问题方案

    这篇文章主要介绍了MySQL事务的ACID特性以及并发问题方案,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-07-07
  • 忘记mysql数据库root用户密码重置方法[图文]

    忘记mysql数据库root用户密码重置方法[图文]

    忘记mysql数据库root用户密码重置方法,需要的朋友可以参考下。
    2011-11-11
  • Mysql 通过binlog日志恢复数据的实现示例

    Mysql 通过binlog日志恢复数据的实现示例

    本文主要介绍了Mysql 通过binlog日志恢复数据的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-03-03
  • mysql事务的基本要素与事务隔离级别详解

    mysql事务的基本要素与事务隔离级别详解

    这篇文章主要介绍了mysql事务的基本要素与事务隔离级别详解,事务是一种机制、一个操作序列,包含了一组数据库操作命令,并且把所有的命令作为一个整体一起向系统提交或撤销操作请求,需要的朋友可以参考下
    2023-08-08

最新评论