Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL索引优化

MySQL中索引的常见问题与性能优化详解

作者:刘大华

索引就像图书馆的检索系统,设计得好,找书会很快;设计的不好,反而会越用越慢,下面小编就和大家详细介绍一下MySQL中索引的常见问题与性能优化吧

什么是索引

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

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

-- 创建用户表的正确方式
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字段创建了普通索引,当按用户名查询时会使用这个索引,所以查询速度会提升。

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

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

EXPLAIN结果关键字段解读:

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

type字段详细说明:

索引的底层原理

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

为什么用B+树?

常见问题

问题1:索引越多越好吗

-- 错误示范:盲目创建索引
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个索引,但很多可能用不上,反而影响性能

每个索引的代价:

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

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:在低选择性字段上创建索引

-- 选择性太低的索引
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条数据:

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

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:索引列参与计算或函数

-- 创建测试表
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)
);

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

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无法使用索引,会导致全表扫描

正确的查询方式

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:最左前缀原则

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

-- 创建联合索引
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.排序优化:天然支持按索引顺序排序

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

-- 能充分利用联合索引的查询
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;  -- 使用索引

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

-- 不能使用或不能充分利用联合索引的查询
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.主键索引(聚集索引)- 最重要的索引

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

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

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

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

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

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

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. 前缀索引 - 处理长文本字段

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

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

实践2:覆盖索引

什么是覆盖索引?

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

-- 创建测试表
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)
);
-- 需要回表查询的例子
SELECT * FROM user_activities 
WHERE user_id = 123 AND activity_type = 'login';

执行过程:

1.在索引idx_user_activity_time中找到匹配记录

2.获取对应的主键id

3.通过主键id到数据行中读取所有字段(包括大文本description)

4.返回结果

-- 覆盖索引的例子
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.不需要访问数据行

覆盖索引的优势:

实践3:索引维护和监控

1. 查看索引使用情况

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. 查找冗余索引

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. 重建索引优化性能

OPTIMIZE TABLE your_table;
-- 或者
ALTER TABLE your_table ENGINE=InnoDB;

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

不同业务的不同策略

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

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:分类页面商品列表

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:品牌商品搜索

SELECT * FROM products 
WHERE brand_id = 10 AND status = 1 
ORDER BY created_at DESC LIMIT 10;

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

查询3:商品搜索

SELECT * FROM products 
WHERE name LIKE '手机%' AND category_id = 5 AND status = 1;

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

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

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:查看收件箱(最新消息在前)

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:查看对话历史

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:日志分析系统

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:时间范围统计

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:用户行为分析

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,索引覆盖查询条件

性能优化

索引设计检查清单:

查询优化技巧:

总结

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

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

以上就是MySQL中索引的常见问题与性能优化详解的详细内容,更多关于MySQL索引优化的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文