业务人员经常会遇到一种难以复现的现象:同一条“查询库存”的 SQL,有时很快,有时却明显变慢。直觉通常会指向数据库负载、缓存或索引失效,但真正的问题可能藏在 WHERE 条件中:对索引列调用函数、使用隐式类型转换、用 OR 拼接互斥条件,或者把 NULL 当成普通值处理。
这类问题适合引入模型 API 辅助分析,但模型不应该直接决定如何修改生产数据库。更稳妥的边界是:数据库只提供脱敏后的 SQL、执行计划和统计摘要;模型负责解释候选原因、整理验证顺序;最终优化仍由工程师通过执行计划和对照实验确认。
原理拆解
1. 函数可能让索引失去入口
假设表中有 created_at 的普通索引:
SELECT id, quantity
FROM inventory
WHERE DATE(created_at) = '2026-08-06'
AND sku = 'A-100';
数据库需要先计算每一行的 DATE(created_at),再判断结果是否相等。具体执行方式取决于数据库、统计信息和索引结构,但这种写法通常不能直接利用 created_at 的有序范围。
更适合优化为半开区间:
SELECT id, quantity
FROM inventory
WHERE created_at >= '2026-08-06 00:00:00'
AND created_at < '2026-08-07 00:00:00'
AND sku = 'A-100';
半开区间避免了“当天最后一秒是多少”的边界问题,也不会把下一天的记录算进去。时间值应该由应用按照明确时区生成,不能依赖数据库会话的隐式时区转换。
2. NULL 不是普通字符串
下面两种条件并不等价:
WHERE warehouse_id <> 3
WHERE warehouse_id IS NULL OR warehouse_id <> 3
在三值逻辑中,warehouse_id 为 NULL 时,warehouse_id <> 3 的结果不是 TRUE,而是 UNKNOWN,因此不会被保留。是否需要包含 NULL,必须由业务定义决定。模型可以提醒这种风险,却不能替业务补写语义。
3. 优化目标是可验证的
“看起来更快”不是结论。每次候选改写至少要保留:
- 原 SQL 和改写后的 SQL;
- 相同参数、相同数据范围和相同隔离级别;
EXPLAIN或EXPLAIN ANALYZE的输出;- 返回行数、扫描行数、是否发生排序或临时表;
- 数据库版本、统计信息更新时间和执行时间采样条件。
不同数据库的执行计划字段和命令存在差异,下面的示例只展示通用流程,具体参数应以目标数据库文档为准。
可执行实现
第一步:建立只读诊断账户
模型服务不应使用业务应用的数据库账号,更不能拥有 INSERT、UPDATE、DELETE、DDL 或管理权限。以支持角色授权的数据库为例,可以采用类似配置:
CREATE USER sql_observer WITH PASSWORD 'change-me';
GRANT CONNECT ON DATABASE appdb TO sql_observer;
GRANT USAGE ON SCHEMA public TO sql_observer;
GRANT SELECT ON TABLE inventory TO sql_observer;
密码应通过密钥管理系统或部署环境注入,示例中的值不能直接用于生产。若数据库不支持完全相同的授权语法,应根据其权限模型改写,并在独立环境验证。
第二步:先做本地规则检查
不要把每一条 SQL 都发送给模型。简单、确定的规则可以在本地完成,例如检测索引列上的函数、危险语句和可能的无界查询:
import os
import re
from dataclasses import dataclass
FORBIDDEN = re.compile(
r"\b(insert|update|delete|drop|alter|truncate|grant|revoke|create)\b",
re.I,
)
FUNCTION_ON_COLUMN = re.compile(
r"\b(date|lower|upper|coalesce|cast|convert)\s*\(",
re.I,
)
@dataclass
class Finding:
level: str
message: str
def inspect_sql(sql: str) -> list[Finding]:
findings = []
normalized = sql.strip()
if FORBIDDEN.search(normalized):
findings.append(Finding("block", "检测到非只读语句"))
if FUNCTION_ON_COLUMN.search(normalized):
findings.append(Finding("review", "条件中可能存在函数包裹列"))
if re.search(r"\bselect\s+\*\b", normalized, re.I):
findings.append(Finding("review", "建议明确列清单,减少无关数据读取"))
if not re.search(r"\bwhere\b", normalized, re.I):
findings.append(Finding("review", "缺少 WHERE,需确认是否允许全表读取"))
return findings
这段代码只是第一道闸门,不是 SQL 解析器。生产环境应使用目标数据库的解析器或成熟 SQL AST 工具,避免仅靠正则表达式判断嵌套查询、注释、字符串字面量和方言语法。
第三步:构造脱敏诊断上下文
发送前要删除账号、手机号、地址、订单号等业务敏感信息。参数值可以替换成类型和范围摘要,表结构只保留诊断所需字段:
{
"dialect": "mysql",
"sql": "SELECT id, quantity FROM inventory WHERE DATE(created_at)=? AND sku=?",
"parameters": ["date", "sku_pattern"],
"indexes": [
{
"name": "idx_inventory_created_at", "columns": ["created_at"]},
{
"name": "idx_inventory_sku", "columns": ["sku"]}
],
"plan": "EXPLAIN 输出应在此处放入脱敏后的文本",
"question": "列出可能的性能风险、验证步骤和不改变语义的候选改写"
}
plan 不应凭空补造。没有执行计划时,模型只能进行静态分析,结论必须标注为“待验证”。
第四步:通过环境变量接入模型 API
模型 API 的具体路径、请求字段和返回结构由服务商文档决定。下面用一个 OpenAI 风格的兼容接口展示调用边界,实际使用前应核对目标接口的认证方式、模型名称、超时和数据处理条款。HaerAPI 可作为待评估的模型接入选项,是否适合当前系统取决于其现行文档和合规审查结果。
import os
import requests
API_URL = os.environ["LLM_API_URL"]
API_KEY = os.environ["LLM_API_KEY"]
MODEL = os.environ.get("LLM_MODEL", "your-model")
def ask_model(context: dict) -> str:
prompt = (
"你是数据库性能分析助手。只根据输入证据回答;"
"区分已观察事实、推测原因和待验证步骤;不要执行 SQL,不要要求写权限。\n"
+ str(context)
)
response = requests.post(
API_URL,
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={
"model": MODEL,
"temperature": 0,
"messages": [{
"role": "user", "content": prompt}],
},
timeout=(3, 30),
)
response.raise_for_status()
return response.json()["choices"][0]["message"]["content"]
不要把密钥写进仓库、日志或前端代码。LLM_API_URL、LLM_API_KEY 和 LLM_MODEL 应由部署平台注入,并限制日志只记录请求 ID、模型标识和耗时,不记录完整提示词。
第五步:人工验证候选方案
建议按以下顺序执行:
- 在脱离生产流量的环境复制必要数据分布,先确认改写前后结果集一致。
- 对原 SQL 和候选 SQL 分别执行计划分析,关注访问类型、使用索引、估算行数与实际行数。
- 检查时间边界、
NULL、重复值和空结果等边界条件。 - 只在收益和语义都确认后,再提交索引或 SQL 变更,并保留回滚方案。
- 上线后观察慢查询、锁等待、错误率和资源使用情况,而不是只看单次执行时间。
常见问题
为什么加了索引仍然慢?
索引不是保证命中的开关。优化器可能判断全表扫描成本更低,也可能因为选择性不足、统计信息过期、隐式类型转换、排序或回表读取而选择其他路径。应以实际执行计划为依据,不能仅凭索引存在与否下结论。
OR 一定会导致索引失效吗?
不一定。数据库可能使用索引合并、改写条件或分别访问多个索引。是否需要拆成 UNION ALL,必须结合结果是否会重复、执行计划和数据分布确认。模型给出的改写只能作为候选方案。
能否让模型直接改 SQL 并自动上线?
不建议默认这样做。自动执行会把提示注入、错误语义和权限越界风险叠加到数据库变更链路。更合理的做法是让模型输出结构化诊断结果,由规则引擎阻断写操作,再由工程师审核 SQL、计划和回归结果。
脱敏后模型还看得懂吗?
通常可以保留表名、列名、类型、索引和参数类别,但是否足够取决于问题类型。若诊断依赖真实分布,应上传聚合统计而非原始值,例如分位数、基数和空值比例,并先确认这些摘要不会泄露业务信息。
总结
模型 API 适合承担 SQL 性能诊断中的解释、归纳和验证清单生成,不适合替代数据库优化器或变更审批。可上线的最小闭环应包含只读采集、本地规则拦截、脱敏上下文、超时与失败降级、人工验证和审计记录。
面对函数谓词、NULL 语义和索引选择问题,先把业务语义写清楚,再用执行计划验证改写结果。这样即使模型不可用,系统也能回退到规则诊断和人工排查,数据库性能治理才不会依赖某一次模型回答。