在千万级数据量的数据库表中执行查询时,扫描路径的选择直接决定了查询的执行效率。如果数据库优化器选择了全表扫描或者低效的索引扫描路径,会导致大量无效IO和CPU消耗,查询耗时成倍增加。扫描路径重写就是通过调整SQL语句结构、补充统计信息、优化索引设计等方式,引导优化器选择更高效的扫描方式,从根源上提升查询性能。

扫描路径重写的核心原理
数据库的查询优化器会根据表的统计信息、索引情况、SQL语句的谓词条件生成多个可能的执行计划,然后选择预估成本最低的路径执行。扫描路径重写的核心就是让优化器能够识别到更优的执行路径,或者手动调整SQL结构绕过优化器的判断偏差。
常见的低效扫描路径包括:全表扫描千万级大表、使用低选择性索引扫描、多表连接时先扫描大表再关联小表、嵌套循环中驱动表选择错误等。重写的目标就是把这些路径替换为索引范围扫描、索引覆盖扫描、小表驱动大表连接等高效路径。
常用扫描路径重写技巧
1. 索引覆盖扫描重写
如果查询只需要返回索引列的数据,不需要回表查询聚簇索引,就可以触发索引覆盖扫描,避免回表带来的IO消耗。如果原SQL没有合适的覆盖索引,需要调整查询列或者补充联合索引。
比如原查询需要返回用户表的id、name、age三个字段,原表只有id的主键索引,查询会走主键索引扫描然后回表取name和age的值:
-- 原查询,需要回表 SELECT id, name, age FROM user_table WHERE age > 18;
我们可以创建包含id、name、age的联合索引,让查询直接走索引覆盖扫描,不需要回表:
-- 创建覆盖索引 CREATE INDEX idx_user_age_name_id ON user_table(age, name, id); -- 重写后查询,走索引覆盖扫描 SELECT id, name, age FROM user_table WHERE age > 18;
2. 谓词下推重写
谓词下推是指把过滤条件尽可能下推到数据扫描的最底层,减少上层处理的数据量。有些情况下优化器没有自动做谓词下推,需要手动调整SQL结构。
比如原查询先关联两张千万级表,再过滤条件:
-- 原查询,先关联再过滤 SELECT a.id, b.order_id FROM user_table a JOIN order_table b ON a.id = b.user_id WHERE a.age > 18 AND b.order_status = 1;
可以手动把过滤条件下推到各自表的扫描阶段,先过滤再关联,减少关联的数据量:
-- 重写后,先过滤再关联 SELECT a.id, b.order_id FROM (SELECT id FROM user_table WHERE age > 18) a JOIN (SELECT user_id, order_id FROM order_table WHERE order_status = 1) b ON a.id = b.user_id;
3. 连接顺序重写
多表连接时,优化器可能会因为统计信息不准确选择大表作为驱动表,导致扫描数据量过大。这时候可以调整表的连接顺序,用小表作为驱动表,减少被驱动表的扫描次数。
比如user_table有1000万数据,user_tag表有10万数据,原查询先扫描user_table再关联user_tag:
-- 原查询,大表驱动小表 SELECT a.id, b.tag_name FROM user_table a JOIN user_tag b ON a.id = b.user_id WHERE a.create_time > '2023-01-01';
重写后调整连接顺序,用小表user_tag作为驱动表:
-- 重写后,小表驱动大表 SELECT b.user_id, b.tag_name FROM user_tag b JOIN user_table a ON b.user_id = a.id WHERE a.create_time > '2023-01-01';
4. 避免函数转换导致索引失效
如果在查询条件的索引列上使用函数或者类型转换,会导致索引无法使用,触发全表扫描。这时候需要重写条件,把函数转换移到常量侧,或者调整索引设计。
比如user_table的phone列是varchar类型,有索引,原查询对phone列做类型转换:
-- 原查询,索引失效,全表扫描 SELECT id, phone FROM user_table WHERE CAST(phone AS UNSIGNED) = 13800138000;
重写后把常量转换为varchar类型,让索引可以正常使用:
-- 重写后,使用索引范围扫描 SELECT id, phone FROM user_table WHERE phone = '13800138000';
扫描路径验证方法
重写完成后,需要通过执行计划验证扫描路径是否生效。可以使用EXPLAIN命令查看SQL的执行计划,重点关注type字段和rows字段:
type为ref、range、index说明使用了索引扫描,ALL说明是全表扫描rows字段的值越小,说明扫描的数据行数越少,路径越优- 如果看到
Using index说明触发了索引覆盖扫描,是更优的路径
比如查看重写后的查询执行计划:
EXPLAIN SELECT id, name, age FROM user_table WHERE age > 18;
如果执行计划里type是range,rows远小于表的总行数,说明扫描路径重写生效。
注意事项
扫描路径重写需要结合实际的业务场景和数据分布,不能盲目套用技巧。比如有些情况下全表扫描的成本反而比索引扫描更低,比如查询需要返回表中80%以上的数据,这时候索引扫描加上回表的成本会高于全表扫描。另外,重写后需要定期更新表的统计信息,避免优化器因为统计信息过期再次选择低效的扫描路径。