MySQL索引优化:从慢查询到10倍提速,我都做了什么

索引就像图书馆的检索系统,设计得好,找书会很快;设计的不好,反而会越用越慢。

什么是索引?

想象一下,你要在100万人的花名册里面找到"张三":

  • 没有索引:从头到尾逐页翻找,可能要找50万次
  • 有索引:相当于有目录,直接查目录,2-3次就找到了

索引的本质:索引是一种数据结构,帮助数据库快速定位数据,避免全表扫描。

sql 复制代码
-- 创建用户表的正确方式
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    age INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_username (username)  -- 为username字段创建索引
);

这里为username字段创建了普通索引,当按用户名查询时会使用这个索引,所以查询速度会提升。

sql 复制代码
-- 查看索引使用情况
EXPLAIN SELECT * FROM users WHERE username = '张三';

使用EXPLAIN可以查看MySQL如何执行查询,是否使用了索引。

EXPLAIN结果关键字段解读:

字段 说明 理想值
type 查询类型 const, eq_ref, ref
key 实际使用的索引 显示索引名称
key_len 使用的索引长度 越长越好
rows 预估扫描行数 越少越好
Extra 额外信息 Using index

type字段详细说明:

  • const:通过主键或唯一索引查询,最多返回一行
  • eq_ref:联表查询时使用主键或唯一索引
  • ref:使用普通索引查询
  • range:使用索引进行范围查询
  • index:全索引扫描
  • ALL:全表扫描(需要优化)

索引的底层原理

MySQL索引主要使用B+树结构,就像一本多层目录的书:

  • 根节点:最顶层目录
  • 中间节点:章节目录
  • 叶子节点:具体的页码

为什么用B+树?

  • 平衡:左右子树高度差不超过1,查询稳定
  • 有序:数据按顺序存储,范围查询效率高
  • 扇出高:每个节点可以存储很多指针,减少IO次数

常见问题

问题1:索引越多越好吗?

sql 复制代码
-- 错误示范:盲目创建索引
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    category VARCHAR(50),
    price DECIMAL(10,2),
    status TINYINT,
    -- 问题:每个索引都要维护,写操作变慢
    INDEX idx_name (name),           -- 可能很少按name单独查询
    INDEX idx_category (category),   -- 可能很少按category单独查询
    INDEX idx_price (price),         -- 可能很少按price单独查询
    INDEX idx_status (status),       -- 状态只有几个值,索引效果差
    INDEX idx_name_category (name, category)  -- 与单列索引重复
);

这里创建了5个索引,但很多可能用不上,反而影响性能

每个索引的代价:

  • 占用磁盘空间:每个索引都是一个B+树
  • 降低写性能:INSERT/UPDATE/DELETE需要更新所有索引
  • 增加优化器负担:需要评估多个索引的选择

正确做法:按实际查询需求创建

sql 复制代码
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    category VARCHAR(50),
    price DECIMAL(10,2),
    status TINYINT DEFAULT 1,
    -- 根据业务查询模式创建索引
    INDEX idx_category_status_price (category, status, price),  -- 联合索引
    INDEX idx_name (name)  -- 只有经常单独按name查询才需要
);

使用联合索引覆盖多个查询条件,比多个单列索引更高效

问题2:在低选择性字段上创建索引

sql 复制代码
-- 选择性太低的索引
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    gender ENUM('男','女'),        -- 只有2个可能值
    status TINYINT DEFAULT 1,      -- 1激活, 0禁用, 2删除
    department VARCHAR(50),
    INDEX idx_gender (gender),
    INDEX idx_status (status) 
);

gender和status字段值重复度高,创建索引效果很差

假设表有10000条数据:

  • gender索引:2个不同值
  • status索引:3个不同值
  • 查询时可能返回大量数据,索引效果差

正确做法:选择高区分度字段

sql 复制代码
CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(100),
    gender ENUM('男','女'),
    status TINYINT DEFAULT 1,
    department VARCHAR(50),
    employee_code VARCHAR(20) UNIQUE,  -- 员工编号,唯一性高
    email VARCHAR(100),                -- 邮箱,区分度高
    
    -- 创建有价值的索引
    UNIQUE INDEX uk_employee_code (employee_code),
    INDEX idx_email (email),
    INDEX idx_department_gender (department, gender) -- 联合索引选择性较好
);

employee_code和email字段值几乎不重复,索引效果很好

问题3:索引列参与计算或函数

sql 复制代码
-- 创建测试表
CREATE TABLE orders (
    id INT PRIMARY KEY,
    order_date DATE,                -- 日期字段,有索引
    amount DECIMAL(10,2),           -- 金额字段,有索引
    customer_name VARCHAR(100),     -- 客户名,有索引
    INDEX idx_order_date (order_date),
    INDEX idx_amount (amount),
    INDEX idx_customer_name (customer_name)
);

错误的查询方式:索引列参与计算

sql 复制代码
SELECT * FROM orders WHERE YEAR(order_date) = 2024;        -- 索引失效!
SELECT * FROM orders WHERE amount * 1.1 > 1000;           -- 索引失效!
SELECT * FROM orders WHERE UPPER(customer_name) = 'JOHN'; -- 索引失效!
SELECT * FROM orders WHERE order_date + INTERVAL 1 DAY > '2024-01-01'; -- 索引失效!

在索引列上使用函数或计算,MySQL无法使用索引,会导致全表扫描

正确的查询方式

sql 复制代码
SELECT * FROM orders 
WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01';  -- 使用索引

SELECT * FROM orders WHERE amount > 1000 / 1.1;                  -- 使用索引

SELECT * FROM orders WHERE customer_name = 'john';               -- 使用索引

SELECT * FROM orders WHERE order_date > '2024-01-01' - INTERVAL 1 DAY;  -- 使用索引

保持索引列干净,把计算移到等号右边,MySQL就能使用索引

问题4:最左前缀原则

什么是联合索引? 联合索引也叫复合索引,是在多个列上创建的索引。

sql 复制代码
-- 创建联合索引
CREATE TABLE sales (
    id INT PRIMARY KEY,
    region VARCHAR(50),      -- 地区
    city VARCHAR(50),        -- 城市
    sale_date DATE,          -- 销售日期
    amount DECIMAL(10,2),
    INDEX idx_region_city_date (region, city, sale_date)  -- 联合索引
);

这个联合索引按region→city→sale_date的顺序组织数据

为什么使用联合索引? 1.减少索引数量 :一个联合索引替代多个单列索引 2.覆盖更多查询 :支持多种查询条件组合 3.避免回表 :如果查询字段都在索引中,不需要访问数据行 4.排序优化:天然支持按索引顺序排序

联合索引的性价比体现在:

  • 存储成本:1个联合索引 < 3个单列索引
  • 查询性能:联合索引可以一次性满足复杂查询
  • 维护成本:只需要维护1个索引结构
sql 复制代码
-- 能充分利用联合索引的查询
SELECT * FROM sales WHERE region = '北京';  -- 使用索引
SELECT * FROM sales WHERE region = '北京' AND city = '朝阳区'; -- 使用索引
SELECT * FROM sales WHERE region = '北京' AND city = '朝阳区' AND sale_date = '2024-01-01'; -- 使用索引
SELECT * FROM sales WHERE region = '北京' ORDER BY city;  -- 使用索引

这些查询都能充分利用联合索引,因为条件从最左列开始

sql 复制代码
-- 不能使用或不能充分利用联合索引的查询
SELECT * FROM sales WHERE city = '朝阳区'; -- 无法使用索引
SELECT * FROM sales WHERE sale_date = '2024-01-01'; -- 无法使用索引
SELECT * FROM sales WHERE region = '北京' AND sale_date = '2024-01-01';  -- 只能使用region部分
SELECT * FROM sales WHERE city = '朝阳区' AND sale_date = '2024-01-01';  -- 无法使用索引

缺少最左列region,索引无法使用或只能部分使用

最佳实践

实践1:选择合适的索引类型

1.主键索引(聚集索引)- 最重要的索引

sql 复制代码
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,  -- 自动创建聚集索引
    name VARCHAR(50)
);

主键索引决定数据物理存储顺序,一个表只能有一个

2. 唯一索引 - 保证数据唯一性

sql 复制代码
CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(100) UNIQUE,      -- 唯一索引
    phone VARCHAR(20) UNIQUE        -- 唯一索引
);

唯一索引既保证数据唯一,又提供查询加速

3. 联合索引 - 多条件查询的最佳选择

sql 复制代码
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    status TINYINT,
    created_at DATETIME,
    INDEX idx_user_status_created (user_id, status, created_at)
);

联合索引的顺序很重要,应该把最常用的等值查询条件放在前面

4. 前缀索引 - 处理长文本字段

sql 复制代码
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(500),
    content TEXT,
    INDEX idx_title_prefix (title(50))  -- 只索引前50个字符
);

对于长文本字段,可以只索引前N个字符,平衡性能与存储

实践2:覆盖索引

什么是覆盖索引? 当查询的所有字段都包含在索引中时,MySQL只需要访问索引而不需要回表查询数据行。

sql 复制代码
-- 创建测试表
CREATE TABLE user_activities (
    id INT PRIMARY KEY,
    user_id INT,
    activity_type VARCHAR(50),
    activity_time DATETIME,
    description TEXT,  -- 大文本字段
    INDEX idx_user_activity_time (user_id, activity_type, activity_time)
);
sql 复制代码
-- 需要回表查询的例子
SELECT * FROM user_activities 
WHERE user_id = 123 AND activity_type = 'login';

执行过程:

  • 1.在索引idx_user_activity_time中找到匹配记录
  • 2.获取对应的主键id
  • 3.通过主键id到数据行中读取所有字段(包括大文本description)
  • 4.返回结果
sql 复制代码
-- 覆盖索引的例子
SELECT user_id, activity_type, activity_time 
FROM user_activities 
WHERE user_id = 123 AND activity_type = 'login';

执行过程:

  • 1.在索引idx_user_activity_time中找到匹配记录
  • 2.直接返回索引中的字段值(user_id, activity_type, activity_time都在索引中)
  • 3.不需要访问数据行

覆盖索引的优势:

  • 性能提升:避免回表操作,减少IO
  • 减少内存使用:不需要加载整行数据
  • 查询更快:特别是在有TEXT/BLOB字段的表上

实践3:索引维护和监控

1. 查看索引使用情况

sql 复制代码
SELECT 
    TABLE_NAME,
    INDEX_NAME,
    SEQ_IN_INDEX,
    COLUMN_NAME
FROM information_schema.STATISTICS 
WHERE TABLE_SCHEMA = 'your_database' 
AND TABLE_NAME = 'your_table';

查看表的索引结构和字段信息

2. 查找冗余索引

sql 复制代码
SELECT 
    t.TABLE_NAME,
    s.INDEX_NAME,
    GROUP_CONCAT(s.COLUMN_NAME ORDER BY s.SEQ_IN_INDEX) as columns
FROM information_schema.STATISTICS s
JOIN information_schema.TABLES t ON s.TABLE_NAME = t.TABLE_NAME 
WHERE s.TABLE_SCHEMA = 'your_database'
GROUP BY t.TABLE_NAME, s.INDEX_NAME
ORDER BY t.TABLE_NAME, s.INDEX_NAME;

找出可能重复或冗余的索引

3. 重建索引优化性能

sql 复制代码
OPTIMIZE TABLE your_table;
-- 或者
ALTER TABLE your_table ENGINE=InnoDB;

重建表可以消除索引碎片,提高性能

不同业务的不同策略

场景1:电商商品搜索优化

sql 复制代码
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(200),
    category_id INT,
    brand_id INT,
    price DECIMAL(10,2),
    status TINYINT DEFAULT 1,  -- 1上架 0下架
    stock_count INT,
    created_at DATETIME,
    
    -- 针对电商常见查询模式设计索引
    INDEX idx_category_status_price (category_id, status, price),
    INDEX idx_brand_status (brand_id, status),
    INDEX idx_name_category (name, category_id),
    INDEX idx_created_status (created_at, status)
);

高频查询优化:

查询1:分类页面商品列表

sql 复制代码
SELECT * FROM products 
WHERE category_id = 5 AND status = 1 
ORDER BY price DESC LIMIT 20;

索引使用:idx_category_status_price 说明:等值查询category_id和status,按price排序,完美匹配索引

查询2:品牌商品搜索

sql 复制代码
SELECT * FROM products 
WHERE brand_id = 10 AND status = 1 
ORDER BY created_at DESC LIMIT 10;

索引使用:idx_brand_status 说明:等值查询brand_id和status,索引覆盖查询条件

查询3:商品搜索

sql 复制代码
SELECT * FROM products 
WHERE name LIKE '手机%' AND category_id = 5 AND status = 1;

索引使用:idx_name_category 说明:前缀匹配name,等值查询category_id和status

场景2:社交平台消息系统

sql 复制代码
CREATE TABLE messages (
    id BIGINT PRIMARY KEY,
    from_user_id BIGINT,
    to_user_id BIGINT,
    content TEXT,
    is_read TINYINT DEFAULT 0,
    created_at DATETIME,
    
    -- 针对消息查询模式优化
    INDEX idx_to_user_created (to_user_id, created_at),
    INDEX idx_from_user_created (from_user_id, created_at),
    INDEX idx_conversation (LEAST(from_user_id, to_user_id), GREATEST(from_user_id, to_user_id), created_at)
);

典型查询优化:

查询1:查看收件箱(最新消息在前)

sql 复制代码
SELECT * FROM messages 
WHERE to_user_id = 123 
ORDER BY created_at DESC 
LIMIT 20;

索引使用:idx_to_user_created 说明:等值查询to_user_id,按created_at排序,完美匹配索引

查询2:查看对话历史

sql 复制代码
SELECT * FROM messages 
WHERE LEAST(from_user_id, to_user_id) = 123 
  AND GREATEST(from_user_id, to_user_id) = 456 
ORDER BY created_at;

索引使用:idx_conversation 说明:使用函数索引优化对话查询,避免OR条件

场景3:日志分析系统

sql 复制代码
CREATE TABLE access_logs (
    id BIGINT PRIMARY KEY,
    user_id INT,
    action VARCHAR(50),
    resource_path VARCHAR(500),
    ip_address VARCHAR(45),
    access_time DATETIME,
    response_time INT,
    
    -- 日志分析查询优化
    INDEX idx_access_time (access_time),
    INDEX idx_user_action_time (user_id, action, access_time),
    INDEX idx_action_response (action, response_time)
);

分析查询优化:

查询1:时间范围统计

sql 复制代码
SELECT action, COUNT(*) 
FROM access_logs 
WHERE access_time BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY action;

索引使用:idx_access_time 说明:范围查询access_time,索引快速定位时间范围

查询2:用户行为分析

sql 复制代码
SELECT user_id, action, COUNT(*) 
FROM access_logs 
WHERE user_id = 123 
  AND access_time >= '2024-01-01'
GROUP BY user_id, action;

索引使用:idx_user_action_time 说明:等值查询user_id,范围查询access_time,索引覆盖查询条件

性能优化

索引设计检查清单:

  • 只为高频查询条件创建索引
  • 选择性 > 10% 的字段才考虑索引
  • 复合索引遵循最左前缀原则
  • 避免索引列参与计算或函数
  • 考虑覆盖索引优化
  • 定期监控索引使用情况
  • 删除长时间未使用的索引

查询优化技巧:

  • 使用EXPLAIN分析执行计划
  • 避免SELECT *,只取需要的字段
  • 大数据量表考虑分区
  • 合理使用LIMIT限制结果集
  • 避免在WHERE子句中使用NOT、!=、<>操作

总结

1.理解业务需求 :索引设计要从实际查询模式出发 2.平衡读写性能 :索引加速查询但降低写性能 3.精准设计 :联合索引比多个单列索引更高效 4.关注选择性 :高选择性字段更适合创建索引 5.持续优化:随着业务发展调整索引策略

"为你的查询设计索引,而不是为你的表设计索引"

本文首发于公众号:程序员刘大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!

📌往期精彩

《SpringBoot+Vue3 整合 SSE 实现实时消息推送》

《这20条SQL优化方案,让你的数据库查询速度提升10倍》

《SpringBoot 动态菜单权限系统设计的企业级解决方案》

《Vue3 + ElementPlus 动态菜单实现:一套代码完美适配多角色权限系统》

相关推荐
bcbnb4 小时前
如何解析iOS崩溃日志:从获取到符号化分析
后端
许泽宇的技术分享4 小时前
当AI学会“说人话“:Azure语音合成技术的魔法世界
后端·python·flask
用户69371750013844 小时前
4.Kotlin 流程控制:强大的 when 表达式:取代 Switch
android·后端·kotlin
用户69371750013844 小时前
5.Kotlin 流程控制:循环的艺术:for 循环与区间 (Range)
android·后端·kotlin
vx_bisheyuange4 小时前
基于SpringBoot的宠物商城网站的设计与实现
spring boot·后端·宠物
bcbnb4 小时前
全面解析网络抓包工具使用:Wireshark和TCPDUMP教程
后端
leonardee4 小时前
Spring Security安全框架原理与实战
java·后端
回家路上绕了弯5 小时前
包冲突排查指南:从发现到解决的全流程实战
分布式·后端
爱分享的鱼鱼5 小时前
部署Vue+Java Web应用到云服务器完整指南
前端·后端·全栈
麦麦麦造5 小时前
比 pip 快 100 倍!更现代的 python 包管理工具,替代 pip、venv、poetry!
后端·python