AI 写 SQL:自然语言转查询

写查询不必从空白开始。AI 能把”上个月各城市销售额”这类自然语言翻译成 SQL,但正确与否最终要由你和数据库共同确认。它本质是一种”受约束的自然语言生成”任务:输出既要符合 SQL 语法,又要引用真实存在的表与字段,还要对齐业务口径。把数据库当作事实来源、让模型只承担”意图→查询”的翻译层,是抑制幻觉的关键姿态——模型不应成为答案的来源,而只是把你的意图精确地映射成可执行语句。

是什么

Text-to-SQL(也称 NL2SQL)把一句自然语言问题映射为可在数据库执行的 SQL。最小的可用链路是:你提供 schema(建表语句、字段注释、少量样本行)→ 模型生成 SQL → 在数据库中执行 → 你核对行数与逻辑。这与”让模型凭记忆口算答案”有本质区别:后者一旦记错便无据可查,前者把事实责任交还给数据库,模型只负责把意图翻译成查询。

从技术演进看,这一任务经历过”基于规则/seq2seq 模型”到”基于大语言模型提示”两个阶段。早期的专用模型在小数据集上训练,泛化有限;如今用通用 LLM 做提示式生成,靠上下文里的 schema 与示例即可适配新库,迁移成本大幅下降。但通用模型并不知道你库里的真实结构,所以”给它看什么”决定了产出质量。

在工程实践中,直接”prompt 进、SQL 出”容易因 schema 过大、字段歧义而失准。更稳的做法是引入检索增强(RAG):把历史 SQL、表结构说明、数据字典向量化,生成时检索最相关的上下文再补全,从而让模型”看见”与当前问题最匹配的表与示例。vanna.ai 即采用这种思路——先通过 train 注入 DDL 与示例查询建立索引,再用 ask 提问,模型在检索到的上下文内作答,幻觉概率明显下降。进一步,部分系统还会做”执行式验证”:把生成的 SQL 先跑一遍,根据报错或空结果让模型自我修正,再交付。

import vanna
from vanna.openai import OpenAI_Chat
from vanna.chromadb import ChromaDB_VectorStore

class MyVanna(ChromaDB_VectorStore, OpenAI_Chat):
    def __init__(self, config=None):
        ChromaDB_VectorStore.__init__(self, config=config)
        OpenAI_Chat.__init__(self, config=config)

vn = MyVanna(config={"api_key": "你的密钥"})
vn.connect_to_postgres(host="localhost", dbname="shop", user="u", password="p")
vn.train(ddl="CREATE TABLE orders (id BIGINT, city TEXT, amount NUMERIC(12,2), created_at TIMESTAMP)")
sql = vn.ask("最近 30 天每个城市的订单数?")

LangChain 也提供 SQL toolkit,暴露 list_tablesschemaquery 等工具,交由 Agent 在”先看有哪些表、再读表结构、最后生成并执行”的循环里自主调用。评测这类系统时,通常区分”执行准确率”(生成的 SQL 能否跑通)与”结果匹配率”(返回集合是否与标准答案一致),后者才是业务真正关心的指标。

为什么有用

分析师、运营、产品常需临时取数,手写 SQL 受熟练度与上下文切换拖累:要在十几个表之间确认字段名、回忆 join 键、处理时间粒度。AI 把”我想要上个月各城市销售额”直接变成可跑的查询,省去翻 schema、拼 join 的等待。对探索性分析尤其有效——你能快速试错、迭代口径,而不必先写完一整段 SQL 才发现方向错了;对不熟悉某张表的新人,它也是顺手的”schema 导航员”。

但它提升的是”从意图到查询”的速度,不替你承担数据准确性责任。涉及对外报表、财务口径、合规统计时,AI 生成的 SQL 仍需人工评审与交叉验证。在 Spider、BIRD 等学术基准上,部分主流方法已接近可用水平,但与真实业务库中庞大的 schema、隐式口径相比仍有明显差距,不能假设”生成即可信”。更现实的做法是把它定位为”取数加速器”,而非”免审答案机”:让模型覆盖从零到一的繁琐,把人的精力留给口径确认与异常研判。

怎么用

提升准确率的核心是”给足上下文 + 让模型解释思路”。建议的提示模板包含四部分:建表语句与关键注释、字段业务含义(如时间字段是时间戳还是日期)、样本数据、以及”只查给定表、输出思路与执行计划”的约束。把数据库里的 COMMENT 注释、枚举值含义一并喂给模型,往往比单纯贴 DDL 更有效;字段名若用缩写(如 amtusr_id),更要在提示里写清中文含义。

-- 示例:最近 30 天每个城市的订单数
SELECT city, COUNT(*) AS orders
FROM orders
WHERE created_at >= NOW() - INTERVAL '30 day'
GROUP BY city
ORDER BY orders DESC;
给模型的约束示例:
- 仅使用上文给出的表与字段,不得臆造
- 时间字段 created_at 为 TIMESTAMP,按天聚合用 date_trunc('day', created_at)
- 先列出将访问的表与字段,再写出 SQL
- 说明每个 WHERE / JOIN 的过滤意图,便于人工纠偏
- 金额聚合用 SUM(amount),注意 NULL 与退款记录的口径

在工具侧,推荐把只读副本接入:用最小权限账号连接并禁止写操作;将”生成”与”执行”解耦——模型产出 SQL,人点击”运行”,或在 CI 流水线里先跑测试库、再决定是否落生产。对复杂多表查询,可先用 dbdiagram.io 之类的工具导出 DBML 作为 schema 上下文,既清晰又省 token。few-shot 也很关键:给 2–3 个同库的历史问答对,模型能更快对齐你们的项目约定。还可以加一层”自校验”:让模型在交稿前先用 EXPLAIN 或 LIMIT 100 预跑,确认非空且类型合理再返回。若要长期评估效果,建议沉淀一批”黄金问答”做回归,用执行准确率跟踪模型或提示变更是否退步。

注意点

  • 字段幻觉:模型可能引用不存在的列、拼错表名,或把 amount 当成”已退款后净额”。生成后务必核对 SELECT 列表中的字段在 schema 中真实存在,最好用信息_schema 自动比对。
  • 口径漂移:同一”销售额”可能含税/不含税、是否含退款、按下单还是支付时间,必须人来定义并在提示里写死规则,否则同一问题每次结果都可能不同。
  • join 方向错误:一对多关系里聚合位置放错,会成倍放大或漏算;漏写外键条件则会产出笛卡尔积。可用 EXPLAIN 看执行计划是否走了预期索引、扫描行数是否异常。
  • 时区与粒度:跨时区业务要显式指定时区(如 AT TIME ZONE 'Asia/Shanghai'),避免服务器默认时区导致”昨天”偏移一天。
  • 写操作红线:UPDATE / DELETE / DROP 必须先在备份或影子库执行并走审批;不要让模型直连生产库自动执行,也不要授予其写权限账号。
  • 成本与窗口:大 schema 全量塞进上下文既贵又易超窗口,应按需检索相关表,而非整库投喂;生成的 SQL 建议落日志以备审计。
人工把关清单:
- 抽样核对行数与已知报表是否一致
- 字段名逐个比对 schema(可脚本化)
- 写操作先备份、走评审
- 不让模型直接连生产库自动执行
- 时区/口径是否在提示中显式声明

小结

AI 写 SQL 适合加速取数与探索:把”意图→查询”的翻译交给模型,把”结果与口径”的确认留给人。给它足够的 schema 上下文,用 RAG 与 few-shot 提升命中,对字段真实性与数值口径做人工核对,写操作坚持备份与审批。把数据库当作事实来源,模型只是翻译层,而非答案来源;一旦它越界替你下结论,就需要拉回人工评审。

参考与延伸阅读

本文累计阅读