多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

MySQL索引优化实战:B+树原理与性能调优

MySQL索引优化实战:B+树原理与性能调优 1. 索引优化实战从原理到落地从事数据库性能调优十年我处理过上百个MySQL索引优化案例。索引优化绝不是简单加个索引就能解决的需要深入理解存储引擎特性、索引数据结构和工作原理。本章将分享实战中验证过的索引优化方法论。1.1 B树索引的底层运作机制MySQL的InnoDB引擎采用B树作为索引结构这种数据结构有三大核心特点有序存储所有叶子节点通过双向链表连接范围查询效率极高。例如查询WHERE id BETWEEN 100 AND 200时只需定位到100对应的叶子节点然后顺序遍历链表即可。高扇出性单个节点可存储大量键值默认16KB页大小通常3-4层就能存储千万级数据。实测在SSD存储环境下三层B树可支撑5亿条记录的高效查询。聚簇索引特性主键索引的叶子节点直接存储行数据二级索引则存储主键值。这导致以下性能特征主键查询只需一次IO二级索引查询需要回表额外IO主键长度影响所有二级索引大小-- 查看索引物理大小单位MB SELECT table_name, index_name, ROUND(stat_value * innodb_page_size / 1024 / 1024, 2) size_mb FROM mysql.innodb_index_stats WHERE stat_name size AND database_name your_db;1.2 联合索引的最左前缀原则联合索引(a,b,c)实际相当于建立了三个索引(a)(a,b)(a,b,c)常见误区查询条件包含b和c但缺少a时索引完全失效范围查询、、BETWEEN右侧的列不能使用索引-- 有效使用索引的情况 SELECT * FROM users WHERE country China -- 使用索引 AND city Beijing -- 使用索引 AND age 18; -- 使用索引age是范围查询但前两列已精确匹配 -- 索引失效的情况 SELECT * FROM users WHERE city Beijing -- 不满足最左前缀 AND age 25;实战经验联合索引的列顺序应该按照区分度从高到低排列。可通过以下SQL计算字段区分度SELECT COUNT(DISTINCT column1)/COUNT(*) as selectivity1, COUNT(DISTINCT column2)/COUNT(*) as selectivity2 FROM your_table;1.3 索引失效的七大陷阱隐式类型转换字段定义为varchar但用数字查询时-- phone是varchar类型 SELECT * FROM users WHERE phone 13800138000; -- 索引失效函数操作字段SELECT * FROM orders WHERE DATE(create_time) 2023-01-01; -- 索引失效 -- 应改为 SELECT * FROM orders WHERE create_time BETWEEN 2023-01-01 00:00:00 AND 2023-01-01 23:59:59;OR条件不当使用-- 优化前全表扫描 SELECT * FROM products WHERE category_id 10 OR price 100; -- 优化后使用UNION SELECT * FROM products WHERE category_id 10 UNION SELECT * FROM products WHERE price 100;! 或 操作符多数情况下导致索引失效LIKE以通配符开头SELECT * FROM articles WHERE title LIKE %优化%; -- 索引失效IS NULL/IS NOT NULL需要单独建立索引索引列参与计算SELECT * FROM orders WHERE total_amount * 0.9 1000; -- 索引失效2. 排序与分组深度调优2.1 排序操作的执行原理MySQL的排序操作分为两种实现方式索引排序最优当ORDER BY字段与索引顺序完全匹配时无需额外排序操作执行计划显示Using index文件排序filesort当无法使用索引排序时需要额外内存/磁盘空间执行计划显示Using filesort-- 查看排序算法详情 EXPLAIN FORMATJSON SELECT * FROM orders WHERE user_id 100 ORDER BY create_time DESC;在JSON输出的filesort_summary部分可以看到sort_modesort_key, additional_fields单路排序或sort_key, packed_additional_fields双路排序sort_buffer_size排序缓冲区大小number_of_tmp_files使用的临时文件数量2.2 分组查询的优化策略GROUP BY操作本质上也包含排序过程优化要点松散索引扫描Loose Index Scan当GROUP BY字段是索引的最左前缀时只需读取部分索引条目即可完成分组执行计划显示Using index for group-by-- 创建适合松散扫描的索引 ALTER TABLE sales ADD INDEX idx_region_product (region, product_id, sale_date); -- 以下查询可以利用松散扫描 SELECT region, product_id, COUNT(*) FROM sales GROUP BY region, product_id;临时表优化当无法使用松散扫描时适当增大tmp_table_size和max_heap_table_size考虑使用SQL_BIG_RESULT提示-- 强制使用磁盘临时表 SELECT SQL_BIG_RESULT region, COUNT(*) FROM large_table GROUP BY region;2.3 分页查询的终极优化方案深度分页如LIMIT 100000, 20是性能杀手优化方案延迟关联SELECT t.* FROM your_table t JOIN ( SELECT id FROM your_table WHERE condition ORDER BY sort_column LIMIT 100000, 20 ) AS tmp ON t.id tmp.id;书签记录法适用于有序数据-- 假设上次查询最后一条记录的id是12345 SELECT * FROM your_table WHERE id 12345 ORDER BY id LIMIT 20;预计算分页实时性要求不高的场景使用物化视图或定时任务预先计算分页结果适合电商商品列表等场景3. 实战调优案例解析3.1 电商订单查询优化原始场景SELECT * FROM orders WHERE user_id 100 AND status completed AND create_time BETWEEN 2023-01-01 AND 2023-06-30 ORDER BY total_amount DESC LIMIT 0, 10;问题诊断现有索引是(user_id, status)create_time条件导致索引后半部分失效ORDER BY需要filesort优化方案创建新索引(user_id, create_time, status)改写查询条件SELECT * FROM orders WHERE user_id 100 AND create_time 2023-01-01 00:00:00 AND create_time 2023-06-30 23:59:59 AND status completed ORDER BY total_amount DESC LIMIT 0, 10;效果对比执行时间从1200ms降至35ms扫描行数从8万行降至150行3.2 社交平台Feed流优化挑战千万级用户关系数据需要实时获取关注用户的动态并按时间排序初始方案SELECT * FROM posts WHERE user_id IN (SELECT to_user_id FROM follows WHERE from_user_id 100) ORDER BY create_time DESC LIMIT 10;问题IN子查询效率低下大量随机IO优化方案使用JOIN替代INSELECT p.* FROM posts p JOIN follows f ON p.user_id f.to_user_id WHERE f.from_user_id 100 ORDER BY p.create_time DESC LIMIT 10;添加覆盖索引ALTER TABLE follows ADD INDEX idx_from_to (from_user_id, to_user_id); ALTER TABLE posts ADD INDEX idx_user_time (user_id, create_time);引入Redis缓存热数据使用ZSET存储用户最新动态定期异步刷新缓存4. 高级调优技巧与参数配置4.1 关键参数调优# InnoDB缓冲池通常分配70-80%的物理内存 innodb_buffer_pool_size 12G # 排序缓冲区每个连接单独分配 sort_buffer_size 4M # 最大可动态扩展到 max_sort_length 1M # 临时表大小 tmp_table_size 64M max_heap_table_size 64M # 连接级内存分配 join_buffer_size 4M重要提示全局参数调整后需要监控SHOW GLOBAL STATUS中的相关计数器Sort_merge_passes文件排序合并次数Sort_range和Sort_scan排序操作类型统计Created_tmp_disk_tables磁盘临时表创建次数4.2 索引性能监控-- 查看索引使用频率 SELECT object_schema, object_name, index_name, count_read, count_fetch FROM performance_schema.table_io_waits_summary_by_index_usage WHERE index_name IS NOT NULL ORDER BY count_read DESC; -- 重置统计 TRUNCATE performance_schema.table_io_waits_summary_by_index_usage;4.3 使用直方图统计信息MySQL 8.0支持列直方图统计优化非索引列查询-- 创建直方图 ANALYZE TABLE orders UPDATE HISTOGRAM ON total_amount WITH 100 BUCKETS; -- 查看直方图 SELECT * FROM information_schema.column_statistics WHERE table_name orders;5. 真实业务场景避坑指南5.1 时间类型存储的坑问题场景CREATE TABLE events ( id BIGINT PRIMARY KEY, event_time DATETIME, INDEX idx_time (event_time) ); -- 查询某天数据 SELECT * FROM events WHERE DATE(event_time) 2023-01-01; -- 索引失效解决方案存储时统一为UTC时间查询使用范围条件SELECT * FROM events WHERE event_time 2023-01-01 00:00:00 AND event_time 2023-01-02 00:00:00;5.2 JSON字段查询优化低效查询SELECT * FROM products WHERE JSON_EXTRACT(specs, $.weight) 10; -- 全表扫描优化方案使用生成列索引ALTER TABLE products ADD COLUMN weight DECIMAL(10,2) GENERATED ALWAYS AS (JSON_EXTRACT(specs, $.weight)) STORED, ADD INDEX idx_weight (weight);8.0.17可使用函数索引ALTER TABLE products ADD INDEX idx_weight ((CAST(JSON_EXTRACT(specs, $.weight) AS DECIMAL(10,2))));5.3 大表ALTER TABLE操作危险操作ALTER TABLE huge_table ADD COLUMN new_flag TINYINT DEFAULT 0;安全方案使用pt-online-schema-change工具或采用影子表方案创建新表结构通过触发器同步数据变更分批迁移历史数据最后原子性切换表名
返回列表