数据库查询优化中的查询重写规则与启发式优化
描述
查询重写是数据库查询优化的重要阶段,它基于预定义的规则和启发式方法,将用户提交的SQL查询转换为语义等价但执行效率更高的形式。与基于代价的优化不同,查询重写通常在逻辑层面进行,不依赖于具体的物理存储结构和数据分布统计信息。其核心目标是在查询进入代价优化器之前,通过应用一系列转换规则,简化查询结构、消除冗余操作、启用更多潜在的高效访问路径。
解题过程
第一步:理解查询重写的目标与基本原则
查询重写的主要目标可以概括为:
- 简化查询结构:减少查询块(Query Block)的嵌套层次,降低优化复杂度。
- 减少中间结果集大小:尽早过滤掉不需要的数据,减少后续操作(如连接、排序)需要处理的数据量。
- 启用更多访问路径:将查询改写为能够更好地利用现有索引或高效连接算法的形式。
- 消除冗余计算:避免重复计算相同的表达式或子查询。
基本原则是保持语义等价。任何重写操作都不能改变查询的最终结果。
第二步:掌握核心的查询重写规则
以下是一些最重要且常见的查询重写规则:
-
谓词下推
- 描述:将过滤条件(WHERE子句中的条件)尽可能地向数据源方向移动,使其在查询执行的早期就被应用。
- 过程:
- 场景:一个包含连接和过滤的查询。例如:
SELECT * FROM orders o JOIN customers c ON o.cust_id = c.id WHERE c.country = 'China' AND o.amount > 1000。 - 重写逻辑:优化器会识别出
c.country = 'China'是作用于customers表的谓词,o.amount > 1000是作用于orders表的谓词。理想的重写是,在扫描customers表时就直接应用country = 'China'的过滤,在扫描orders表时就直接应用amount > 1000的过滤。这样,参与连接操作的数据量会大大减少。
- 场景:一个包含连接和过滤的查询。例如:
- 为什么有效:显著减少了参与中间步骤(特别是连接)的数据行数。
-
视图合并
- 描述:将视图(或派生表/子查询)的定义合并到外层查询中,形成一个单一的、更复杂的查询块,从而为优化器提供全局优化的视野。
- 过程:
- 原始查询:
SELECT v.order_id, v.total_amount FROM (SELECT order_id, SUM(amount) as total_amount FROM order_items GROUP BY order_id) v WHERE v.total_amount > 5000。 - 重写后:
SELECT order_id, SUM(amount) as total_amount FROM order_items GROUP BY order_id HAVING SUM(amount) > 5000。 - 逻辑:将派生表
v的定义(一个分组聚合)合并到主查询,并将对v.total_amount的过滤转换为HAVING子句。
- 原始查询:
- 为什么有效:避免了将整个视图物化后再进行过滤。优化器现在可以统一考虑聚合和过滤,可能选择先过滤
order_items表中的行再进行聚合(如果存在合适的索引或谓词),效率更高。
-
子查询展开/消除
- 描述:将相关的或不相关的子查询转换为等价的连接操作。
- 过程:
- 原始查询(不相关子查询):
SELECT name FROM employees WHERE dept_id IN (SELECT id FROM departments WHERE location = 'Shanghai')。 - 重写后:
SELECT e.name FROM employees e JOIN departments d ON e.dept_id = d.id WHERE d.location = 'Shanghai'。 - 逻辑:将
IN子句和子查询转换为一个标准的INNER JOIN。
- 原始查询(不相关子查询):
- 为什么有效:连接操作(如Hash Join, Merge Join)通常比嵌套循环执行子查询(对外表每一行都执行一次内查询)的效率高得多。这为优化器提供了更多连接算法和顺序的选择。
-
外连接消除
- 描述:在特定条件下,将外连接(OUTER JOIN)简化为内连接(INNER JOIN)。
- 过程:
- 条件:当查询的谓词条件或业务逻辑保证了外连接侧的表不会产生NULL扩展行时。
- 示例:
SELECT o.id, c.name FROM orders o LEFT JOIN customers c ON o.cust_id = c.id WHERE c.phone IS NOT NULL。 - 逻辑:
WHERE子句中的c.phone IS NOT NULL隐含了c表的行必须存在,否则c.phone就是NULL。这使得左连接失去了“保留左表所有行”的意义,因此可以安全地重写为INNER JOIN。
- 为什么有效:内连接的优化空间和可用算法通常比外连接更广,执行效率可能更高。
-
常量表达式求值与常量传播
- 描述:在编译时计算常量表达式,并将结果传播到查询的其他部分。
- 过程:
- 常量求值:将
WHERE salary > 3000 * 12直接计算为WHERE salary > 36000。 - 常量传播:对于
SELECT * FROM t WHERE c1 = 5 AND c2 = c1,优化器会将c1 = 5传播到c2 = c1,重写为WHERE c1 = 5 AND c2 = 5。这可能会启用新的索引使用机会(例如,现在可以在(c1, c2)或(c2, c1)上使用复合索引)。
- 常量求值:将
- 为什么有效:减少运行时计算开销,并可能通过简化谓词来启用更优的访问路径。
第三步:理解启发式优化在查询重写中的作用
查询重写大量依赖启发式规则(“经验法则”),这些规则通常被证明在大多数情况下是有益的。例如:
- 尽早执行选择操作:这是谓词下推背后的核心启发式规则,因为选择操作通常能大幅减少数据量。
- 尽早执行投影操作:只选择需要的列,减少数据在操作符间流动的大小。
- 将子查询转换为连接:这是一个强有力的启发式规则,因为连接优化是数据库的强项。
这些启发式规则指导优化器优先尝试某些类型的重写,即使在没有精确统计信息的情况下,也能很大概率上生成一个更好的执行计划起点。
第四步:综合示例分析
假设有一个查询:
SELECT e.name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id WHERE e.salary > (SELECT AVG(salary) FROM employees WHERE dept_id = 10) AND EXISTS (SELECT 1 FROM projects p WHERE p.lead_id = e.emp_id AND p.status = 'Active');
一个成熟的查询重写器可能会执行以下步骤:
- 常量求值与传播:计算子查询
(SELECT AVG(salary) ...)(如果dept_id=10的统计信息稳定,优化器可能会将其估算为一个常量)。 - 子查询展开:
- 将
EXISTS子查询重写为连接:e JOIN projects p ON e.emp_id = p.lead_id AND p.status = 'Active'。 - 将标量子查询
(SELECT AVG(salary)...)的结果(假设为X)直接用于比较e.salary > X。
- 将
- 外连接消除检查:由于
EXISTS子查询已转换为与employees的连接,并且这个连接是内连接,它要求e表中的行必须在projects中有匹配。这可能会使得左连接LEFT JOIN departments在某些情况下(如果业务逻辑保证员工必有部门)可以简化为内连接,但需要谨慎分析。假设不能简化。 - 谓词下推:将
p.status = 'Active'下推到projects表的扫描阶段。将e.salary > X下推到employees表的扫描阶段(如果X已确定)。 - 最终重写形式可能类似于:
SELECT e.name, d.dept_name FROM employees e LEFT JOIN departments d ON e.dept_id = d.dept_id JOIN projects p ON e.emp_id = p.lead_id WHERE e.salary > 55000 -- 假设AVG结果是55000 AND p.status = 'Active';
经过重写后,一个包含嵌套子查询的复杂查询变成了一个清晰的多表连接查询,为后续基于代价的优化器(CBO)选择最佳的连接顺序和算法提供了更好的基础。