MySQL索引优化是提升数据库性能的关键手段,一个合理的索引设计和使用策略,往往能将查询速度提升几十倍甚至上百倍。然而,索引优化并不简单,既需要扎实的理论基础,也需要丰富的实战经验。
本文总结了21个MySQL索引优化的实战技巧,从索引选择、设计到维护、监控的全生命周期,帮助你解决日常开发中的索引性能问题。
在具体介绍前,让我们先简单回顾索引的基础知识:
MySQL常用的索引类型包括:主键索引、唯一索引、普通索引、联合索引、全文索引等。其中最常用的B+树索引,具有以下特点:
理解这些基础对于后续优化至关重要。接下来,让我们进入正题。
联合索引的顺序直接影响其使用效率。MySQL会从左到右依次使用索引列,如果中间某列没有使用,则后面的列也无法使用索引。
错误示例:
-- 创建索引(name, age, city)
CREATE INDEX idx_user_name_age_city ON user(name, age, city);
-- 以下查询无法充分利用索引
SELECT * FROM user WHERE age = 25 AND city = 'Beijing'; -- name列缺失,只能全表扫描
SELECT * FROM user WHERE name = 'Tom' AND city = 'Beijing'; -- 中间age列缺失,city无法使用索引
优化方法:
-- 假设选择性:city < name < age
CREATE INDEX idx_user_name_age_city ON user(name, age, city);
-- 充分利用索引的查询
SELECT * FROM user WHERE name = 'Tom' AND age = 25;
SELECT * FROM user WHERE name = 'Tom' AND age = 25 AND city = 'Beijing';
回表操作是指通过索引找到对应的行记录指针,再通过指针去查询完整记录的过程。如果查询只需要返回索引包含的列,则可以避免回表,这称为覆盖索引。
优化前:
-- 创建普通索引
CREATE INDEX idx_user_name ON user(name);
-- 需要回表查询
SELECT id, name, age, city FROM user WHERE name = 'Tom';
优化后:
-- 创建包含所需字段的索引
CREATE INDEX idx_user_name_age_city ON user(name, age, city);
-- 使用覆盖索引,无需回表
SELECT name, age, city FROM user WHERE name = 'Tom';
对于CHAR和VARCHAR类型的列,如果整列长度较大,可以只索引开头的部分字符,这样可以大幅减少索引占用空间,提高索引效率。
优化方法:
-- 假设product_desc是较长的产品描述文本
CREATE INDEX idx_product_desc ON product(product_desc(50));
如何确定前缀长度?可以通过计算选择性来确定:
-- 计算不同前缀长度的选择性
SELECT
COUNT(DISTINCT LEFT(product_desc, 10)) / COUNT(*) AS sel_10,
COUNT(DISTINCT LEFT(product_desc, 20)) / COUNT(*) AS sel_20,
COUNT(DISTINCT LEFT(product_desc, 30)) / COUNT(*) AS sel_30,
COUNT(DISTINCT LEFT(product_desc, 40)) / COUNT(*) AS sel_40,
COUNT(DISTINCT LEFT(product_desc, 50)) / COUNT(*) AS sel_50,
COUNT(DISTINCT product_desc) / COUNT(*) AS sel_full
FROM product;
选择一个接近完整列选择性的前缀长度即可。
注意事项: 使用前缀索引后,无法使用该索引做ORDER BY或GROUP BY,也无法使用覆盖索引。
多个单列索引在多条件查询时,MySQL只会选择一个索引。而复合索引可以同时满足多个条件的查询需求。
优化前:
-- 单独创建两个索引
CREATE INDEX idx_user_age ON user(age);
CREATE INDEX idx_user_city ON user(city);
-- MySQL通常只会选择一个索引
SELECT * FROM user WHERE age = 25 AND city = 'Beijing';
优化后:
-- 创建一个复合索引
CREATE INDEX idx_user_age_city ON user(age, city);
-- 可以同时使用age和city条件
SELECT * FROM user WHERE age = 25 AND city = 'Beijing';
LIKE语句使用通配符前缀(如’%abc’)会导致索引失效。但对于右匹配模式(如’abc%’),索引仍然有效。
可以使用索引的查询:
-- 可以使用索引
SELECT * FROM products WHERE product_name LIKE 'iphone%';
无法使用索引的查询:
-- 无法使用索引
SELECT * FROM products WHERE product_name LIKE '%iphone%';
优化方法:
对于需要搜索包含某个关键词的记录,可以考虑全文索引或搜索引擎。对于简单场景,也可以通过字段冗余解决:
-- 添加一个反转字段
ALTER TABLE products ADD product_name_reversed VARCHAR(255);
-- 触发器维护反转值, 此处为了简单表示整体实现思路, 实际通常在代码中进行反转值赋值
DELIMITER //
CREATE TRIGGER product_insert BEFORE INSERT ON products
FOR EACH ROW
BEGIN
SET NEW.product_name_reversed = REVERSE(NEW.product_name);
END; //
DELIMITER ;
-- 创建反转字段的索引
CREATE INDEX idx_product_name_rev ON products(product_name_reversed);
-- 搜索以'phone'结尾的产品
SELECT * FROM products
WHERE product_name_reversed LIKE CONCAT(REVERSE('phone'), '%');
在字段上使用函数会导致索引失效,应该把运算转移到值上。
错误用法:
-- 索引失效
SELECT * FROM orders WHERE YEAR(create_time) = 2023;
优化方法:
-- 可以使用索引
SELECT * FROM orders
WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01';
MySQL在进行查询时,如果字段类型与条件值类型不匹配,会进行隐式类型转换,可能导致索引失效。
错误用法:
-- user_id是varchar类型,但使用了整数条件
CREATE INDEX idx_user_id ON users(user_id);
SELECT * FROM users WHERE user_id = 12345; -- 索引可能失效
优化方法:
-- 确保条件值类型与字段类型一致
SELECT * FROM users WHERE user_id = '12345'; -- 使用字符串类型
否定条件通常会导致索引失效,因为数据库需要检查所有不满足条件的记录。
优化方法:
尽量用肯定表达式替代否定表达式:
-- 优化前:无法充分利用索引
SELECT * FROM products WHERE category_id != 5;
-- 优化后:可以使用索引
SELECT * FROM products WHERE category_id < 5 OR category_id > 5;
大偏移量的LIMIT分页查询效率较低,因为MySQL需要检索前N条记录然后丢弃。
优化前:
-- 性能较差的分页查询
SELECT * FROM products ORDER BY id LIMIT 100000, 10;
优化方法1 - 使用索引覆盖扫描:
-- 先获取ID,再关联查询完整数据
SELECT p.* FROM products p
JOIN (
SELECT id FROM products ORDER BY id LIMIT 100000, 10
) tmp ON p.id = tmp.id;
优化方法2 - 使用上次查询的最大ID:
-- 假设已知上一页的最大ID是100233
SELECT * FROM products WHERE id > 100233 ORDER BY id LIMIT 10;
使用SELECT *会返回所有列,可能破坏覆盖索引的效果,并增加网络和内存开销。
优化前:
-- 可能导致不必要的开销
SELECT * FROM users WHERE name = 'Tom';
优化后:
-- 只返回需要的列,可能利用覆盖索引
SELECT id, name, email FROM users WHERE name = 'Tom';
在优化前,先使用EXPLAIN分析SQL语句的执行计划,了解索引使用情况。
EXPLAIN SELECT * FROM users WHERE name = 'Tom' AND age > 20;
重点关注以下字段:
type
: 从好到差依次是:system > const > eq_ref > ref > range > index > ALLkey
: 实际使用的索引rows
: 预计需要扫描的行数Extra
: 额外信息,如"Using index"表示使用了覆盖索引如果ORDER BY的列与WHERE使用的列不一致,排序无法使用索引,会导致文件排序。
优化前:
-- WHERE和ORDER BY使用不同的列,可能导致文件排序
CREATE INDEX idx_user_name ON users(name);
SELECT * FROM users WHERE name = 'Tom' ORDER BY age;
优化后:
-- 创建联合索引同时包含WHERE和ORDER BY的列
CREATE INDEX idx_user_name_age ON users(name, age);
SELECT * FROM users WHERE name = 'Tom' ORDER BY age;
注意事项: ORDER BY的多个字段需要与索引顺序一致,且排序方向需一致(全ASC或全DESC)。
在大表上直接创建索引可能会导致长时间锁表。可以使用以下方法优化:
方法1 - 使用低峰期操作:
-- 在低峰期执行索引创建
CREATE INDEX idx_order_status ON orders(status);
方法2 - 使用在线DDL(MySQL 8.0+):
-- 使用ALGORITHM和LOCK选项
CREATE INDEX idx_order_status ON orders(status)
ALGORITHM=INPLACE, LOCK=NONE;
方法3 - 使用pt-online-schema-change工具:
pt-online-schema-change --alter "ADD INDEX idx_order_status (status)" \
--host=localhost --user=root --ask-pass --database=mydb --table=orders \
--execute
对于经常需要计算后过滤的场景,可以使用虚拟列并在其上创建索引。
-- 添加虚拟列存储计算结果
ALTER TABLE products
ADD total_value DECIMAL(10,2) AS (price * quantity) VIRTUAL;
-- 在虚拟列上创建索引
CREATE INDEX idx_total_value ON products(total_value);
-- 使用计算列进行查询
SELECT * FROM products WHERE total_value > 10000;
InnoDB不支持显式的哈希索引,但我们可以自己实现:
-- 添加哈希列
ALTER TABLE users ADD name_hash INT UNSIGNED
GENERATED ALWAYS AS (crc32(name)) STORED;
-- 在哈希列上创建索引
CREATE INDEX idx_name_hash ON users(name_hash);
-- 使用哈希索引查询
SELECT * FROM users
WHERE name_hash = crc32('Tom') AND name = 'Tom';
注意最后还需要验证原始值,因为哈希可能冲突。
随着数据变化,索引可能变得碎片化,影响性能。定期优化表和重建索引可以改善性能。
-- 分析表
ANALYZE TABLE orders;
-- 优化表
OPTIMIZE TABLE orders;
-- 或者重建索引
ALTER TABLE orders DROP INDEX idx_status, ADD INDEX idx_status(status);
建议: 设置一个低峰期的定时任务,对重要表执行优化操作。
索引数量过多会影响写性能,建议每个表的索引数量控制在5个以内。
优化方法:
-- 查找未使用的索引
SELECT * FROM schema_unused_indexes; -- Performance Schema
-- 查找重复的索引
SELECT * FROM sys.schema_redundant_indexes; -- Sys Schema
MySQL 8.0+支持降序索引,可以优化混合排序方向的查询。
-- 创建混合排序方向的索引(MySQL 8.0+)
CREATE INDEX idx_user_age_score ON users(age ASC, score DESC);
-- 可以高效执行的查询
SELECT * FROM users ORDER BY age ASC, score DESC;
MySQL 8.0+支持在WHERE条件满足时才为行创建索引记录,减少索引大小。
-- 只为活跃用户创建索引(MySQL 8.0+)
CREATE INDEX idx_active_users ON users(name, email)
WHERE status = 'active';
MySQL维护了索引统计信息,可以帮助优化器选择合适的索引。有时统计信息不准确会导致次优的执行计划。
-- 查看表的统计信息
SHOW TABLE STATUS LIKE 'users';
-- 查看索引的基数
SHOW INDEX FROM users;
-- 刷新统计信息
ANALYZE TABLE users;
有时MySQL优化器的选择不是最优的,可以使用索引提示强制使用特定索引。
-- 强制使用特定索引
SELECT * FROM users FORCE INDEX(idx_name_age)
WHERE name = 'Tom' AND age > 20;
-- 忽略特定索引
SELECT * FROM users IGNORE INDEX(idx_status)
WHERE status = 'active' AND age > 20;
建议: 索引提示应该是最后的手段,通常先尝试优化表结构和索引设计。
索引优化是一个持续的过程,需要结合业务特点、数据分布和查询模式来综合考虑。
优秀的索引设计需要理论知识和实践经验的结合。