一、从全表扫描的触发条件入手
为什么会出现 MySQL 全表扫描
在 MySQL 的执行计划中,全表扫描通常意味着查询条件没有被有效的索引所利用,数据库需要逐行检查整张表来定位符合条件的行。此时的性能瓶颈往往来自于低选择性、无法利用前缀索引、或是对列进行了不可对索引友好的运算。理解触发点,是把问题聚焦在索引设计与查询改写的第一步。
另一种常见原因是 不等式或范围条件无法快速定位范围,例如在 where 子句中使用函数、类型不匹配、隐式类型转换等,都会让索引失效或变得低效。避免函数在列上的使用,是提高走索引概率的关键策略。
此外,排序、分组或分页操作若与现有索引不吻合,也可能导致 MySQL 放弃使用索引,改为全表扫描。掌握这些触发点,便能把注意力放到后续的索引设计与查询改写上。
二、从索引设计入手的实战策略
建立高效的单列与复合索引
为高基数、筛选性强的列建立单列索引,是最基础也是最直观的优化路径。对常用的筛选条件优先建立单列索引,能显著提升 WHERE 条件的命中率。
-- 单列索引示例
CREATE INDEX idx_users_email ON users(email);-- 复合索引示例,通常应将选择性最高的列放在前面
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
在一些查询中,前缀列的选择性决定了复合索引是否生效。如果查询常用的条件是 user_id 与 status 的组合,那么上述 idx_orders_user_status 索引能够覆盖这两个条件的过滤,从而避免回表。
为了覆盖更多场景,可以使用 覆盖索引,即索引本身就包含了查询所需的所有列,从而使查询不需要额外的回表操作。合理设计包含在索引中的列,是提升性能的关键。
三、查询改写的实战技巧
把不利于走索引的表达改写为可利用索引的表达
在实际查询中,避免在 where 条件中对列进行函数运算或隐式类型转换,这会让索引变成不可用。应将条件改写成一个对索引友好的形式,例如将日期比较中的函数计算移出、确保列类型与常量类型一致。
下面的示例对比,展示了如何通过改写让 MySQL 更容易走索引:
原始写法(可能导致全表扫描):
SELECT * FROM orders
WHERE DATE(created_at) = '2024-01-01' AND status = 'PAID';
改写后的更易走索引的写法(假设 created_at 针对日期字段有索引):
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02' AND status = 'PAID';
在涉及多列条件时,按索引前缀顺序构造查询条件,以提升执行计划命中索引的概率。若某列的选择性更高,应优先放在复合索引的前面。
为了验证改写效果,可以使用 EXPLAIN 查看执行计划,确认是否有索引被实际使用。
EXPLAIN SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02' AND status = 'PAID';四、覆盖索引与列裁剪的实践
使用覆盖索引减少回表开销
覆盖索引的本质是在索引中包含查询所需的列,避免访问数据表本身。通过覆盖索引实现“只读索引”的查询,可以显著降低 I/O 成本,尤其在读多写少的场景中效果显著。
举例来说,如果经常需要按 user_id、status 过滤,并同时返回 user_id 与 status 的两列,可以创建覆盖这两列的复合索引,并直接从该索引返回结果。
CREATE INDEX idx_orders_user_status_cover ON orders(user_id, status, amount, order_date);
查询时,确保 SELECT 的列尽量在索引覆盖范围内,例如:
SELECT user_id, status, amount FROM orders
WHERE user_id = 123 AND status = 'PAID';
注意覆盖的边界条件,当查询需要其他未在索引中的列时,仍然可能回表。因此,覆盖索引的设计需要结合常见查询字段的组合来确定。
五、查询计划分析与优化流程
使用 EXPLAIN 了解执行计划与索引命中情况
在优化时,分析执行计划是第一步。MySQL 的 EXPLAIN 能显示每个表的扫描类型、使用的索引、行数估算等关键信息。通过对比不同写法的 EXPLAIN 结果,可以直观看出是否实现了“从索引设计到查询改写的实战优化方法”。
常见的 EXPLAIN 指标包括 type、possible_keys、key、rows、Extra 等。对于 type,尽量避免 NULL、ALL 类型,优先走 index 或 range;possible_keys 与 key 的对比能直接体现索引是否被使用。
EXPLAIN FORMATS=JSON SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02' AND status = 'PAID';
在 MySQL 8.0 及以上版本,可以使用 EXPLAIN ANALYZE 来获得实际执行时间和行数信息,帮助判断实际的执行路径是否符合期望。
EXPLAIN ANALYZE SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02' AND status = 'PAID';六、常见坑点与避免策略
函数、隐式类型转换、LIKE 的影响以及数据分布
一个常见的坑是对列使用 函数、表达式或隐式类型转换,从而导致索引失效。应保持列的数据类型与查询中的常量类型一致,尽量避免在 where 条件中对列进行函数处理。
此外,LIKE 的前缀匹配(如 LIKE '%abc')对大多数场景都不友好,因为它会阻止使用前缀索引。若需要按文本前缀匹配,尽量采取以前缀字段建立独立索引或全文索引的方案。

-- 不良写法,可能导致全表扫描
SELECT * FROM products WHERE name LIKE '%phone';
相对地,若改为前缀匹配且有前缀索引,则可以显著提升性能:
-- 改进策略示例:建立 name 的前缀索引
CREATE INDEX idx_products_name_prefix ON products(name(10)); -- 取前缀作为索引键
SELECT * FROM products WHERE name LIKE 'Phone%';
最后,数据分布对索引选择有决定性影响。高基数、均匀分布的列通常更适合做索引;而低基数或极端偏斜的列,可能需要通过组合索引或分区等手段来改善。


