大模型辅助的 SQL 重写优化:从执行计划分析到语义等价变换的工程方案

一、SQL 优化的经验瓶颈:为什么"改写 SQL"是 DBA 的手艺活

同一条查询逻辑,不同的 SQL 写法可能产生数十倍的性能差异。一个经典的例子:SELECT * FROM orders WHERE YEAR(create_time) = 2026 无法使用 create_time 上的索引,而 SELECT * FROM orders WHERE create_time >= '2026-01-01' AND create_time < '2027-01-01' 可以。这种"函数导致索引失效"的问题,DBA 凭经验可以识别,但更复杂的重写——如子查询转 JOIN、UNION 优化、窗口函数替代自连接——需要深厚的 SQL 功底和大量的试错。

更深层的问题是:SQL 重写需要保证语义等价——重写后的查询必须返回与原查询完全相同的结果。人工重写容易在边界条件下引入 Bug(如 NULL 值处理、重复行消除),而验证语义等价性本身就很困难。

二、SQL 重写优化架构:从执行计划分析到语义等价验证

大模型辅助的 SQL 重写核心思路是:分析执行计划中的性能瓶颈,基于规则和 LLM 生成语义等价的重写方案,再通过结果对比验证等价性。

flowchart TD
    A[原始 SQL] --> B[执行计划分析<br/>EXPLAIN]
    B --> C[瓶颈识别<br/>全表扫描/临时表/文件排序]
    C --> D[规则匹配<br/>已知重写模式]
    D --> E{规则命中?}
    E -->|命中| F[应用规则重写]
    E -->|未命中| G[LLM 语义重写]
    F --> H[语义等价验证<br/>结果集对比]
    G --> H
    H --> I{结果一致?}
    I -->|是| J[性能对比<br/>执行时间/资源消耗]
    I -->|否| K[丢弃重写方案]
    J --> L{性能提升?}
    L -->|是| M[推荐重写方案]
    L -->|否| K

关键设计决策在于语义等价验证的可靠性。简单的结果集对比在数据量大时不可行,需要采样验证或形式化验证。

三、工程实现:规则引擎、LLM 重写与等价验证

3.1 规则引擎:已知重写模式

from dataclasses import dataclass
from typing import List, Optional
import re

@dataclass
class RewriteRule:
    name: str
    pattern: str           # 正则匹配原始 SQL
    description: str
    rewrite_fn: callable   # 重写函数

class SQLRewriteRuleEngine:
    rules: List[RewriteRule] = []

    def register(self, rule: RewriteRule):
        self.rules.append(rule)

    def match(self, sql: str) -> List[RewriteRule]:
        matched = []
        for rule in self.rules:
            if re.search(rule.pattern, sql, re.IGNORECASE):
                matched.append(rule)
        return matched

# 规则1:函数导致索引失效 → 范围查询
def rewrite_function_on_indexed_column(sql: str) -> Optional[str]:
    """YEAR(col) = N → col >= N-01-01 AND col < (N+1)-01-01"""
    pattern = r'WHERE\s+YEAR\((\w+)\)\s*=\s*(\d{4})'
    match = re.search(pattern, sql, re.IGNORECASE)
    if not match:
        return None

    column = match.group(1)
    year = int(match.group(2))

    replacement = (
        f"WHERE {column} >= '{year}-01-01' "
        f"AND {column} < '{year + 1}-01-01'"
    )
    return re.sub(pattern, replacement, sql, flags=re.IGNORECASE)

# 规则2:子查询转 JOIN
def rewrite_subquery_to_join(sql: str) -> Optional[str]:
    """WHERE col IN (SELECT ...) → JOIN"""
    pattern = (r'WHERE\s+(\w+)\.(\w+)\s+IN\s*'
               r'\(\s*SELECT\s+(\w+)\s+FROM\s+(\w+)(?:\s+WHERE\s+(.+?))?\s*\)')
    match = re.search(pattern, sql, re.IGNORECASE)
    if not match:
        return None

    outer_table = match.group(1)
    outer_col = match.group(2)
    inner_col = match.group(3)
    inner_table = match.group(4)
    inner_where = match.group(5)

    # 构造 JOIN 语句
    join_clause = f"JOIN {inner_table} ON {outer_table}.{outer_col} = {inner_table}.{inner_col}"
    if inner_where:
        join_clause += f" AND {inner_where}"

    # 替换 WHERE IN 为 JOIN
    rewritten = re.sub(
        r'FROM\s+(\w+)\s+WHERE\s+' + re.escape(match.group(0).split('WHERE')[1]),
        f"FROM {outer_table} {join_clause} WHERE",
        sql, flags=re.IGNORECASE
    )
    return rewritten

# 规则3:OR 条件转 UNION ALL
def rewrite_or_to_union(sql: str) -> Optional[str]:
    """WHERE a = 1 OR b = 2 → UNION ALL of two queries"""
    pattern = r'WHERE\s+(.+?)\s+OR\s+(.+?)(?:\s+ORDER|\s+LIMIT|\s*$)'
    match = re.search(pattern, sql, re.IGNORECASE)
    if not match:
        return None

    cond_a = match.group(1).strip()
    cond_b = match.group(2).strip()

    # 提取 FROM 之前的部分
    select_part = sql[:sql.upper().index('FROM')].strip()
    from_part = sql[sql.upper().index('FROM'):sql.upper().index('WHERE')].strip()

    return f"{select_part} {from_part} WHERE {cond_a} UNION ALL {select_part} {from_part} WHERE {cond_b}"

3.2 LLM 语义重写

class LLMSQLRewriter:
    def __init__(self, llm_client):
        self.llm = llm_client

    def rewrite(self, sql: str, explain_output: str) -> Optional[str]:
        prompt = f"""你是一个 SQL 优化专家。请重写以下 SQL 以提升性能,保持语义完全等价。

原始 SQL:
{sql}

执行计划分析:
{explain_output}

重写要求:
1. 保持语义等价(返回完全相同的结果集)
2. 消除全表扫描、临时表和文件排序
3. 优先使用 JOIN 替代子查询
4. 避免在索引列上使用函数
5. 只输出重写后的 SQL,不要解释

重写后的 SQL:"""

        response = self.llm.call(prompt, max_tokens=2000)
        # 提取 SQL 代码块
        if '```sql' in response:
            return response.split('```sql')[1].split('```')[0].strip()
        if '```' in response:
            return response.split('```')[1].split('```')[0].strip()
        return response.strip()

3.3 语义等价验证

class SemanticEquivalenceValidator:
    def __init__(self, db_connection):
        self.conn = db_connection

    def validate(self, original_sql: str,
                 rewritten_sql: str,
                 sample_size: int = 1000) -> bool:
        """通过采样对比验证语义等价"""
        # 方案1:小数据集全量对比
        # 在测试环境中执行两条 SQL,对比结果集

        # 方案2:采样对比
        # 添加 LIMIT 后对比前 N 行
        limited_original = f"{original_sql.rstrip(';')} LIMIT {sample_size}"
        limited_rewritten = f"{rewritten_sql.rstrip(';')} LIMIT {sample_size}"

        try:
            result_a = self.conn.execute(limited_original).fetchall()
            result_b = self.conn.execute(limited_rewritten).fetchall()

            # 排序后对比(顺序可能不同)
            sorted_a = sorted([tuple(r) for r in result_a])
            sorted_b = sorted([tuple(r) for r in result_b])

            return sorted_a == sorted_b
        except Exception as e:
            # 重写后的 SQL 执行失败,不等价
            return False

    def validate_with_checksum(self, original_sql: str,
                                rewritten_sql: str) -> bool:
        """通过校验和验证全量等价(适用于小表)"""
        checksum_a = self._compute_checksum(original_sql)
        checksum_b = self._compute_checksum(rewritten_sql)
        return checksum_a == checksum_b

    def _compute_checksum(self, sql: str) -> str:
        import hashlib
        cursor = self.conn.execute(sql)
        hasher = hashlib.md5()
        for row in cursor:
            hasher.update(str(tuple(row)).encode())
        return hasher.hexdigest()

四、SQL 重写的等价性风险与适用边界

NULL 值的语义差异NOT IN (subquery) 在子查询结果包含 NULL 时行为与 NOT EXISTS 不同——NOT IN 返回空集,而 NOT EXISTS 返回正确结果。重写时必须考虑 NULL 语义,否则可能引入 Bug。

排序的隐式依赖:某些应用依赖查询结果的隐式排序(如 MySQL 在某些情况下按主键排序返回),但 SQL 标准不保证无 ORDER BY 时的排序。重写后排序可能变化,导致应用行为异常。

LLM 重写的不可靠性:LLM 可能生成语法正确但语义不等价的 SQL。例如,将 LEFT JOIN 重写为 INNER JOIN 会丢失不匹配的行。语义等价验证是必不可少的环节,但采样验证无法覆盖所有边界条件。

重写收益的上下文依赖:同一条重写规则在不同数据分布下效果不同。子查询转 JOIN 在小表驱动大表时性能提升明显,但在大表驱动小表时可能更差。重写推荐需要结合数据分布和执行计划做综合判断。

五、总结

大模型辅助 SQL 重写的本质是将"DBA 经验驱动的改写"转化为"规则匹配 + LLM 语义推导 + 等价验证"的系统化方案。本文方案的核心链路为:执行计划瓶颈识别 → 规则引擎匹配 → LLM 语义重写 → 采样等价验证 → 性能对比。落地时需重点关注三个原则:所有重写必须通过等价验证、优先使用规则引擎处理已知模式、LLM 重写仅作为规则引擎的补充。建议从高频慢查询开始优化,逐步积累重写规则库,并建立重写效果的量化追踪机制。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐