ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

MySQL索引优化与性能分析实战指南

MySQL索引优化与性能分析实战指南

1. MySQL性能分析与索引优化实战指南

作为一名常年与MySQL打交道的数据库工程师,我处理过太多因索引不当导致的性能问题。记得去年双十一大促前,某电商平台的订单查询接口突然从200ms飙升到8秒,整个技术团队熬了三个通宵才定位到问题根源——一个缺失的联合索引。今天我就结合这类实战案例,系统梳理MySQL性能分析的核心方法和索引优化的黄金法则。

2. MySQL性能分析三板斧

2.1 慢查询日志:捕捉性能杀手

慢查询日志是DBA的"听诊器",通过修改my.cnf配置启用:

slow_query_log = 1 slow_query_log_file = /var/log/mysql/mysql-slow.log long_query_time = 1 # 超过1秒的查询 log_queries_not_using_indexes = 1 # 记录未走索引的查询

关键分析工具是mysqldumpslow,这个Perl脚本能归类慢查询:

mysqldumpslow -s t -t 10 /var/log/mysql/mysql-slow.log

输出示例会显示:

Count: 5 Time=3.21s (16s) Lock=0.00s (0s) Rows=1000.0 (5000) SELECT * FROM orders WHERE user_id=N AND status='pending'

这表示该查询执行了5次,平均耗时3.21秒,且每次返回1000行——显然需要优化。

注意:生产环境建议将long_query_time设为0.5秒以下,很多性能问题就藏在500ms-1s这个区间的查询中

2.2 EXPLAIN执行计划解密

拿到慢查询后,用EXPLAIN看执行计划:

EXPLAIN SELECT * FROM products WHERE category_id=5 AND price>100;

重点关注的列:

  • type:从优到差依次为 system > const > eq_ref > ref > range > index > ALL
  • key:实际使用的索引
  • rows:预估扫描行数
  • Extra:常见危险信号:
    • Using filesort:需要额外排序
    • Using temporary:用了临时表
    • Using where:存储引擎层过滤

2.3 性能模式(Performance Schema)

MySQL 5.7+推荐使用performance_schema,它能监控更细粒度的性能数据:

-- 查看最耗时的SQL事件 SELECT sql_text, SUM_TIMER_WAIT/1000000000 AS latency_sec FROM performance_schema.events_statements_summary_by_digest ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

3. 索引设计核心原理

3.1 B+树索引的物理结构

MySQL索引采用B+树结构,其特点:

  • 所有数据存储在叶子节点,非叶子节点只存键值
  • 叶子节点通过指针连接形成链表
  • 3层B+树可支撑约2000万数据(假设每页16KB,主键8B)

通过innodb_page_size可查看页大小:

SHOW VARIABLES LIKE 'innodb_page_size';

3.2 索引选择性与基数

选择性计算公式:

选择性 = 不重复值数量 / 总记录数

高选择性字段更适合建索引。通过SHOW INDEX查看基数:

SHOW INDEX FROM users;

Cardinality列显示估算的唯一值数量,这个值通过采样统计得来,有时需要手动更新:

ANALYZE TABLE users;

3.3 聚簇索引与二级索引

InnoDB的聚簇索引特点:

  • 主键作为聚簇索引
  • 若未定义主键,选择第一个UNIQUE NOT NULL索引
  • 都没有则隐式创建6字节的row_id

二级索引查询需要回表:

-- 假设name是二级索引 SELECT * FROM users WHERE name='张三';

执行过程:

  1. 在name索引树找到'张三'的主键ID
  2. 用主键ID到聚簇索引查找完整记录

4. 实战索引优化策略

4.1 联合索引的最左前缀原则

创建联合索引:

ALTER TABLE orders ADD INDEX idx_status_created(status, created_at);

以下查询能利用索引:

SELECT * FROM orders WHERE status='shipped'; SELECT * FROM orders WHERE status='shipped' AND created_at>'2023-01-01';

但以下查询不能完全利用索引:

SELECT * FROM orders WHERE created_at>'2023-01-01';

经验:将高选择性字段放在联合索引左侧,但也要考虑查询频率。我曾将user_id放在status前,虽然user_id选择性更高,但80%查询都带status条件,调整后QPS提升了3倍

4.2 索引下推(ICP)

MySQL 5.6引入的索引下推优化,可以在索引遍历时就进行条件过滤:

-- 假设有索引(name, age) SELECT * FROM users WHERE name LIKE '张%' AND age=20;

无ICP时:先通过name找到所有'张'姓记录,再回表查age=20 有ICP时:在索引内部就过滤age=20,减少回表次数

通过optimizer_switch控制:

SET optimizer_switch='index_condition_pushdown=on';

4.3 覆盖索引优化

当索引包含所有查询字段时,无需回表:

-- 创建覆盖索引 ALTER TABLE products ADD INDEX idx_category_price_name(category_id, price, name); -- 以下查询可以直接从索引获取数据 SELECT name, price FROM products WHERE category_id=5;

通过EXPLAIN的Extra列看到Using index即表示使用了覆盖索引。

5. 索引失效的七大陷阱

5.1 隐式类型转换

常见于字符串字段用数字查询:

-- mobile是varchar类型 SELECT * FROM users WHERE mobile=13800138000; -- 索引失效 SELECT * FROM users WHERE mobile='13800138000'; -- 走索引

5.2 函数操作索引列

-- 假设created_at有索引 SELECT * FROM orders WHERE DATE(created_at)='2023-01-01'; -- 失效 -- 应改为范围查询 SELECT * FROM orders WHERE created_at>='2023-01-01 00:00:00' AND created_at<'2023-01-02 00:00:00';

5.3 前导通配符LIKE

SELECT * FROM products WHERE name LIKE '%手机%'; -- 全表扫描 SELECT * FROM products WHERE name LIKE '小米%'; -- 可能走索引

对于全文搜索需求,应该使用FULLTEXT索引:

ALTER TABLE products ADD FULLTEXT INDEX ft_idx_name(name); SELECT * FROM products WHERE MATCH(name) AGAINST('手机');

5.4 OR条件不当使用

-- 假设name有索引而age没有 SELECT * FROM users WHERE name='张三' OR age=20; -- 全表扫描 -- 优化方案1:改用UNION SELECT * FROM users WHERE name='张三' UNION SELECT * FROM users WHERE age=20; -- 优化方案2:为age添加索引

6. 高级索引调优技巧

6.1 索引跳跃扫描(MySQL 8.0+)

对于联合索引(a,b),即使查询条件没有a,也可能走索引:

-- 有索引(gender, age) SELECT * FROM users WHERE age>20; -- 执行计划可能显示type=range,key=idx_gender_age

原理是优化器会枚举gender的所有值(如'M','F'),相当于执行:

SELECT * FROM users WHERE gender='M' AND age>20 UNION ALL SELECT * FROM users WHERE gender='F' AND age>20;

6.2 降序索引优化

MySQL 8.0支持真正的降序索引:

ALTER TABLE orders ADD INDEX idx_created_desc(created_at DESC);

对于以下查询效率更高:

SELECT * FROM orders ORDER BY created_at DESC LIMIT 100;

6.3 不可见索引

测试索引删除影响时,可以先设为不可见:

ALTER TABLE users ALTER INDEX idx_name INVISIBLE; -- 观察业务影响后决定是否删除 ALTER TABLE users ALTER INDEX idx_name VISIBLE;

7. 索引监控与维护

7.1 索引使用统计

通过sys库查看索引使用情况:

SELECT * FROM sys.schema_index_statistics WHERE table_schema='your_db';

7.2 冗余索引检测

使用pt-index-usage工具分析:

pt-index-usage /var/log/mysql/mysql-slow.log --host=localhost --user=root

7.3 索引碎片整理

定期优化表减少碎片:

-- 查看碎片率 SELECT table_name, index_name, ROUND(stat_value * @@innodb_page_size / 1024 / 1024, 2) AS size_mb, ROUND(stat_value * 100 / table_rows, 2) AS frag_ratio FROM mysql.innodb_index_stats WHERE stat_name='size' AND database_name='your_db'; -- 重建索引 ALTER TABLE orders ENGINE=InnoDB;

8. 真实案例:电商系统索引优化

某电商平台商品表结构:

CREATE TABLE products ( id BIGINT PRIMARY KEY, category_id INT, merchant_id INT, price DECIMAL(10,2), stock INT, is_on_sale TINYINT, created_at DATETIME, modified_at DATETIME, INDEX idx_category (category_id), INDEX idx_merchant (merchant_id) );

问题查询:

SELECT * FROM products WHERE category_id=3 AND is_on_sale=1 AND price BETWEEN 100 AND 500 ORDER BY modified_at DESC LIMIT 20 OFFSET 0;

优化步骤:

  1. 分析发现需要扫描3万行然后排序
  2. 创建联合索引:
ALTER TABLE products ADD INDEX idx_cate_sale_price_mod( category_id, is_on_sale, price, modified_at );
  1. 修改查询为:
SELECT * FROM products WHERE category_id=3 AND is_on_sale=1 AND price>=100 AND price<=500 ORDER BY modified_at DESC LIMIT 20 OFFSET 0;

优化后执行时间从1200ms降到28ms

返回列表