子查询优化

SQL语句中包含冗余或可以提前计算的子查询时,会增加查询分析和执行的开销,影响数据库性能。此问题在ORM框架(Object-Relational Mapping,即对象关系映射)自动生成复杂嵌套查询的场景中尤为突出。PolarDB MySQL的子查询优化功能,通过在优化器阶段重写SQL,能够自动移除不必要的子查询、提前计算常量子查询,从而简化执行计划,显著提升查询性能。

适用范围

  • 产品系列:集群版标准版

  • 内核版本:MySQL 8.0.2,且修订版本需为8.0.2.2.19及以上版本。

开启子查询优化

通过设置loose_simplify_subq_mode参数来控制此优化功能的行为。

PolarDB集群参数在控制台与会话中修改方式存在差异,详细区别如下:

  • PolarDB控制台上修改

    • 兼容性说明:部分集群参数在PolarDB控制台上均已添加MySQL配置文件的兼容性前缀loose_

    • 操作方法:找到并修改这些带loose_前缀的参数。

  • 在数据库会话中修改(使用命令行或客户端):

    • 操作方法:当您连接到数据库,使用SET命令修改参数时,请去掉loose_前缀,直接使用参数的原始名称进行修改。

参数名称

级别

描述

loose_simplify_subq_mode

Global/Session

控制该功能的主开关。取值范围如下:

  • REPLICA_ON(默认值):仅在只读(RO)节点上开启此功能。

  • ON:开启。

  • OFF:关闭。

优化场景与示例

场景一:消除冗余的SELECT嵌套

场当子查询仅仅是为了包裹一个聚合函数或表达式,而没有其他复杂逻辑时(例如SELECT (SELECT SUM(a) FROM t2) FROM dual),优化器会剥离掉外层的SELECT,直接执行核心的表达式。

-- 出现在投影列
SELECT (SELECT SUM(a) FROM t2) FROM dual;
-- 优化后
SELECT SUM(`test`.`t2`.`a`) AS `sum(a)` FROM `test`.`t2`

-- 出现在HAVING子句中
SELECT SUM(a) FROM t2 HAVING (SELECT(SELECT(SELECT count(b))));
-- 优化后
SELECT SUM(`testdb`.`t2`.`a`) AS `SUM(a)` from `testdb`.`t2` HAVING (0 <> count(`testdb`.`t2`.`b`))
  1. 准备测试数据

    DROP TABLE IF EXISTS t2;
    CREATE TABLE t2 (
        id INT PRIMARY KEY AUTO_INCREMENT,
        a INT,
        b INT
    );
    INSERT INTO t2 (a, b) VALUES (10, 100), (20, NULL), (50, 200), (120, NULL);
  2. 关闭优化进行查询:首先,在会话中关闭子查询优化功能,以观察优化前的执行计划。

    SET simplify_subq_mode = 'OFF';
    EXPLAIN SELECT SUM(a) FROM t2 HAVING (SELECT(SELECT(SELECT count(b))));
    SHOW warnings;
    
    -- 返回结果
    /* select#1 */ select sum(`testdb`.`t2`.`a`) AS `SUM(a)` from `testdb`.`t2` having (0 <> (/* select#2 */ select (/* select#3 */ select (/* select#4 */ select count(`testdb`.`t2`.`b`)))))
  3. 开启优化进行查询:现在,在会话中子查询优化功能,并执行相同的查询。

    SET simplify_subq_mode = 'ON';
    EXPLAIN SELECT SUM(a) FROM t2 HAVING (SELECT(SELECT(SELECT count(b))));
    SHOW warnings;
    
    -- 返回结果
    /* select#1 */ select sum(`testdb`.`t2`.`a`) AS `SUM(a)` from `testdb`.`t2` having (0 <> count(`testdb`.`t2`.`b`))

场景二:提前判断[NOT] EXISTS子查询

当[NOT] EXISTS子查询的条件可以被静态推断为始终为真(结果非空)或始终为假(结果为空)时,优化器会直接将[NOT] EXISTS子句替换为TRUEFALSE,从而避免执行子查询。

-- 非空集合
SELECT * FROM t1 WHERE EXISTS(SELECT MAX(a) FROM t2);
-- 优化后
SELECT * FROM t1

-- 空集合 WHERE/HAVING子句=false, 或者LIMIT 0
SELECT * FROM t1 WHERE EXISTS(SELECT max(a) FROM t2 HAVING 1=2 );
-- 优化后
SELECT * FROM t1 WHERE false
  1. 准备测试数据

    DROP TABLE IF EXISTS t1;
    DROP TABLE IF EXISTS t2;
    CREATE TABLE t1 (id INT);
    CREATE TABLE t2 (val INT);
    INSERT INTO t1 VALUES (1), (2);
  2. 关闭优化进行查询:首先,在会话中关闭子查询优化功能,以观察优化前的执行计划。

    SET simplify_subq_mode = 'OFF';
    EXPLAIN SELECT * FROM t1 WHERE EXISTS(SELECT MAX(a) FROM t2);
    SHOW warnings;
    
    -- 返回结果
    /* select#1 */ select `testdb`.`t1`.`id` AS `id` from `testdb`.`t1` where exists(/* select#2 */ select max(`testdb`.`t1`.`id`) from `testdb`.`t2`)
  3. 开启优化进行查询:现在,在会话中子查询优化功能,并执行相同的查询。

    SET simplify_subq_mode = 'ON';
    EXPLAIN SELECT * FROM t1 WHERE EXISTS(SELECT MAX(a) FROM t2);
    SHOW warnings;
    
    -- 返回结果
    /* select#1 */ select `testdb`.`t1`.`id` AS `id` from `testdb`.`t1`

场景三:为ANY/ALL子查询的常量投影添加LIMIT 1

ANYALL子句中的子查询只查询常量(不涉及任何列)时,获取多行相同的值是没有意义的。优化器会自动为这类子查询添加LIMIT 1,避免对子查询的表进行不必要的全表扫描。

-- 优化前
SELECT * FROM t1 WHERE a > ANY (SELECT 1 FROM t2);
-- 优化后
SELECT * FROM t1 WHERE a > ANY (SELECT 1 FROM t2 LIMIT 1);

生产环境使用建议

  • 充分测试:该优化在绝大多数情况下保证逻辑等价。但在少数依赖于特定执行顺序或子查询执行次数的场景下,可能存在风险。建议在核心业务上线前,在预发或测试环境开启此功能进行充分回归测试。

  • 定期更新统计信息:部分优化(如推断空集/非空集)依赖于表的统计信息。陈旧的统计信息可能导致优化器做出次优选择。建议定期对表执行ANALYZE TABLE