1. 先理解“文本转SQL”和“人类基准”到底在说什么
最近“首个文本转SQL模型超越人类基准”这个说法在数据库开发圈子里讨论得比较多。很多同学第一反应是:自然语言直接生成SQL,那以后是不是不用写SQL了?这个理解方向不算错,但和实际场景还有不少距离。先把这个技术点拆开看。
文本转SQL(Text-to-SQL,也叫NL2SQL)指的是让大语言模型把一句自然语言问句转换成一条可执行的SQL查询语句。例如输入“查询2024年销售额超过100万的客户名称和销售金额”,模型输出的目标是一条合法的SQL查询,而不是一段解释文字,也不是一个JSON结果。
“人类基准”在这里通常指一个评测任务集合上由资深数据库开发人员编写SQL所达到的平均准确率。注意这里的“人类”不是泛指所有程序员,而是经过训练、熟悉数据模型和SQL语法的专业人员。所以“超越人类基准”的意思是:在特定评测集上,模型生成SQL的准确度已经高于这些专业人员样本的平均水平。
不过这里有三个必须说清楚的问题。
第一,评测准确率高不等于生产环境可以直接替代人工。评测集里的数据模型、字段命名、问题描述通常比较规范,而真实业务里的表结构往往混乱,字段含义靠文档甚至靠口头沟通才能确认。模型在评测集上超过人类,不代表它面对一张几十个字段、命名随意、历史包袱很重的业务表也能稳定输出正确SQL。
第二,不同的评测集、不同的基准定义方式会导致结论差异很大。有的评测集偏向简单单表查询,有的偏向多表关联和复杂聚合。有的基准是准确率,有的是执行结果一致率,有的是执行计划相近率。一个模型可能在准确率上超过人类,但在复杂查询、长上下文、多轮追问场景里仍明显落后。
第三,“超越人类基准”指的是某个模型在当时公布的评测结果,而不是“所有文本转SQL模型永远超过所有人类”。技术博客和新闻标题为了传播效果会简化表述,落地做技术选型时,还是要以自己业务数据集上的实测结果为准。
那这篇文章为什么还要写?因为无论“超越人类基准”这个表述是否绝对准确,文本转SQL技术已经到了值得工程化验证的阶段。本文不讨论某个特定模型的排名,而是围绕这个方向写清楚:核心概念、环境准备、最小实现、评测方法、调优经验、常见坑和落地建议。读完你可以自己搭一套文本转SQL流程,拿自己的表结构跑一次评测,确认它到底能不能用于实际项目。
2. 文本转SQL的核心机制与评测口径
2.1 从自然语言到SQL:模型到底在做什么
很多人以为文本转SQL是模型直接“查数据库”,实际上模型做的事是生成一条SQL字符串。真正的数据库连接、数据读取、结果返回仍然由传统数据库引擎完成。也就是说,文本转SQL本质上是一个生成任务,不是查询任务。
完整链路通常是这样:
- 用户输入自然语言问题,例如“各部门平均工资最高的员工是谁”。
- 系统把问题、数据库表结构信息、字段说明、少量示例SQL一起拼成提示词。
- 大语言模型根据提示词生成候选SQL。
- 系统检查SQL语法,必要时执行校验查询。
- 校验通过后执行SQL,返回结果给用户。
关键点在于第2步。模型不知道数据库里有什么表、有什么字段,除非你把表结构告诉它。很多初学文本转SQL的人会遇到“明明模型很聪明,为什么生成的SQL表名都是编的”这个问题,原因就是提示词里没有提供真实的 schema 信息。
Schema 信息在文本转SQL里通常有两种组织方式。一种是直接把CREATE TABLE语句放到提示词里,让模型自己读。另一种是先把表结构转换成专门的描述格式,例如字段名、类型、主键、外键、字段注释、枚举取值,再按固定模板注入提示词。后一种对模型更友好,因为不是所有模型都能从 DDL 里准确理解业务含义。
从生成策略上看,文本转SQL可以分成两条路径:
- 直接生成:模型一步生成最终SQL,适合简单查询。
- 中间表示生成:先让模型生成语义解析树或中间查询语言,再转换成SQL,适合复杂查询,但工程复杂度更高。
目前主流大模型基本都采用直接生成,配合足够好的提示词和示例。传统语义解析方法在特定领域仍有优势,但通用场景下不如大模型灵活。
2.2 模型评测为什么不能只看“是否生成成功”
生成SQL的成功率是一个基础指标,但用它衡量文本转SQL能力远远不够。一条SQL“能执行”和“结果正确”是两回事。评测时至少要区分四个层级:
第一层是语法合法。SQL能通过语法解析,不会因为括号、引号、关键字错误而直接报错。
第二层是执行成功。语法合法不代表一定能执行,表名、字段名不存在也会执行失败。
第三层是结果正确。在保证查询逻辑一致的前提下,模型生成的SQL查询结果应该和人工编写SQL的查询结果一致。
第四层是语义等价。有些SQL写法不同但结果一致,例如用IN和EXISTS在特定场景下等价。评测系统需要识别这种等价性,不能因为字符串不一样就判错。
实际评测时,最常用的是执行结果一致率,也就是把模型生成的SQL和标准SQL放到同一个数据库上执行,比较结果集是否一致。字符串完全匹配过于严格,因为ORDER BY的字段顺序、别名命名、子查询结构都可能不同,但结果集一样。
还有一个容易被忽略的指标是“无结果错误率”。模型生成的SQL可能语法合法、执行成功,但因为WHERE条件写错、连接条件缺失、聚合字段选错,导致返回结果为空或返回错误数据。这种错误比语法错误更难发现,因为系统不会报错,用户看到的是“看似正常但实际不对”的结果。
所以在评测文本转SQL模型时,至少要记录四类指标:
| 指标 | 含义 | 为什么重要 |
|---|---|---|
| 语法合法率 | 生成SQL能通过语法解析的比例 | 底层门槛,不合法一定不可用 |
| 执行成功率 | 生成SQL能成功执行的比例 | 表名、字段名、权限问题会影响 |
| 结果一致率 | 查询结果与标准SQL一致的比例 | 核心质量指标 |
| 完全正确率 | 语法、执行、结果全对的比例 | 工程可用性参考 |
要注意,结果一致率也不是绝对可靠。如果两条SQL在同一个数据集上执行结果相同,但排序不同、字段精度不同、NULL 处理方式不同,可能被误判为一致或不一致。评测任务设计时需要定义清楚比较规则。
2.3 “人类基准”的构成:为什么专业人员也会出错
评测里所谓的人类基准,通常不是找一个人写几条SQL取平均值,而是有一套完整的构建流程。常见做法是:从评测集里随机抽取若干问题,交给多名数据库专业人员编写SQL,然后对这些SQL做结果一致性校验,通过校验的SQL作为标准答案。
专业人员编写SQL也会出错,原因包括:
- 对表结构理解偏差,例如把
customer_id当成订单表的业务字段,结果却发现它其实是关联字段。 - 对业务语义理解不一致,例如“本月”是按自然月还是按财务月。
- 对边界情况处理不统一,例如是否需要包含 NULL 值、是否需要去重。
- 多表关联时漏掉关联条件,产生笛卡尔积,得到异常大的结果集。
所以“人类基准”并不是一个完美标准,它只是提供了一个相对合理的参考线。模型要超越这个参考线,意味着在大量问题上比平均值表现更好,而不是在所有问题上都比最优秀的人类专家更强。
理解这一点对工程落地很重要。如果项目的目标是“让模型在生成SQL上达到普通开发人员水平”,那么“超越人类基准”的模型确实值得试试。如果目标是“让模型取代高级数据分析师”,那就要在复杂业务上下文、多轮交互、数据权限控制等方面额外做大量工作。
3. 从零搭一套文本转SQL验证流程
3.1 环境准备:Python、大模型接口和数据库
在真实项目里落地前,最好先搭一套最小验证流程,用自己熟悉的表结构测试模型能力。这里推荐一个比较通用的技术栈:Python 负责流程编排,OpenAI 兼容接口调用大模型,SQLite 或 MySQL 作为目标数据库。
环境要求如下:
| 组件 | 推荐选择 | 说明 |
|---|---|---|
| Python | 3.10 或 3.11 | 兼容主流大模型 SDK 和数据库驱动 |
| 大模型 | OpenAI 兼容接口的模型 | 可以是云端模型,也可以是本地部署 |
| 数据库 | SQLite 或 MySQL | 建议先用 SQLite 验证流程,再切换 MySQL |
| 依赖库 | openai、sqlite3 或 pymysql、pandas | 负责接口调用和数据读取 |
如果使用 OpenAI 兼容接口,可以先安装基础依赖:
pip install openai pandas以 SQLite 为例,不需要额外安装数据库服务,Python 自带sqlite3模块,适合做第一轮流程验证。
如果目标数据库是 MySQL,还需要安装驱动:
pip install pymysql环境准备好之后,先确认三个检查点:
- Python 版本是否符合依赖要求。
- 大模型接口的 API Key 和 endpoint 是否可用。
- 目标数据库是否能通过命令行正常连接。
3.2 准备测试表和种子数据
为了让验证流程可复现,建议先建一个简单但有代表性的业务库。这里设计两张表:部门和员工。
创建表的 SQL 如下:
CREATE TABLE department ( dept_id INTEGER PRIMARY KEY, dept_name TEXT NOT NULL, location TEXT ); CREATE TABLE employee ( emp_id INTEGER PRIMARY KEY, emp_name TEXT NOT NULL, dept_id INTEGER, salary REAL, hire_date TEXT, FOREIGN KEY (dept_id) REFERENCES department(dept_id) );插入若干示例数据:
INSERT INTO department (dept_id, dept_name, location) VALUES (1, '技术部', '北京'), (2, '市场部', '上海'), (3, '财务部', '深圳'); INSERT INTO employee (emp_id, emp_name, dept_id, salary, hire_date) VALUES (101, '张伟', 1, 15000, '2021-03-15'), (102, '李娜', 1, 12000, '2022-07-01'), (103, '王强', 2, 10000, '2020-11-20'), (104, '赵敏', 2, 9500, '2023-01-10'), (105, '刘洋', 3, 13000, '2019-05-06'), (106, '陈晨', 3, 11000, '2022-09-12');这样一张表结构包含主键、外键、文本字段、数值字段和日期字段,足够覆盖简单的单表查询、多表关联、聚合统计和条件过滤。
3.3 构造 Schema 描述并注入提示词
很多文本转SQL实现失败,问题不在模型,而在 schema 描述不够完整。模型需要知道每个字段的业务含义、是否允许为空、是否有默认值、有哪些枚举值。
推荐把数据库表的 DDL 转换成对模型更友好的描述格式。例如:
表 department: - dept_id: 主键,部门ID,类型 INTEGER - dept_name: 部门名称,类型 TEXT - location: 部门所在城市,类型 TEXT 表 employee: - emp_id: 主键,员工ID,类型 INTEGER - emp_name: 员工姓名,类型 TEXT - dept_id: 外键,关联 department.dept_id,部门ID - salary: 员工月薪,类型 REAL - hire_date: 入职日期,类型 TEXT,格式 YYYY-MM-DD把这段描述放到系统提示词中,比直接贴CREATE TABLE语句效果更稳定。原因在于模型能直接看到“外键”“主键”“部门所在城市”这类语义提示,不需要自己从 DDL 关键字里推断。
完整的提示词模板可以这样设计:
你是一名资深SQL开发人员。请根据数据库表结构,将用户的中文问题转换成SQL查询。 表结构: {表结构描述} 注意事项: 1. 只输出SQL语句,不要输出额外解释。 2. 字段名和表名必须使用上面给定的名称,不要添加反引号。 3. 如果问题涉及多表,必须明确写出连接条件。 4. 如果问题没有明确要求排序,不要添加ORDER BY。 5. 金额和数量字段为NULL时,在聚合计算中默认按0处理。 用户问题:{问题}其中{表结构描述}和{问题}是运行时动态填充的内容。这里的注意事項要根据实际项目调整,不要盲目照搬。
3.4 编写最小调用代码
完成提示词构造后,编写一个最小 Python 脚本来调用大模型生成SQL,并在 SQLite 上执行。
import os import sqlite3 from openai import OpenAI client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"), ) SCHEMA_DESC = """ 表 department: - dept_id: 主键,部门ID,类型 INTEGER - dept_name: 部门名称,类型 TEXT - location: 部门所在城市,类型 TEXT 表 employee: - emp_id: 主键,员工ID,类型 INTEGER - emp_name: 员工姓名,类型 TEXT - dept_id: 外键,关联 department.dept_id,部门ID - salary: 员工月薪,类型 REAL - hire_date: 入职日期,类型 TEXT,格式 YYYY-MM-DD """ SYSTEM_PROMPT = f""" 你是一名资深SQL开发人员。请根据数据库表结构,将用户的中文问题转换成SQL查询。 表结构: {SCHEMA_DESC} 注意事项: 1. 只输出SQL语句,不要输出额外解释。 2. 字段名和表名必须使用上面给定的名称,不要添加反引号。 3. 如果问题涉及多表,必须明确写出连接条件。 4. 如果问题没有明确要求排序,不要添加ORDER BY。 """ def generate_sql(question): response = client.chat.completions.create( model=os.getenv("MODEL_NAME", "gpt-4o-mini"), messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": question}, ], temperature=0, ) return response.choices[0].message.content.strip() def execute_sql(db_path, sql): conn = sqlite3.connect(db_path) try: cursor = conn.execute(sql) rows = cursor.fetchall() columns = [desc[0] for desc in cursor.description] return columns, rows finally: conn.close() if __name__ == "__main__": question = "查询每个部门的员工数量,按员工数量从高到低排序" sql = generate_sql(question) print("生成SQL:", sql) columns, rows = execute_sql("test.db", sql) print("列:", columns) print("结果:", rows)代码里设置了temperature=0,目的是让模型输出更稳定,减少随机性。文本转SQL场景下,多样性不是目标,稳定性才是。
执行结果:
生成SQL: SELECT d.dept_name, COUNT(e.emp_id) AS employee_count FROM department d JOIN employee e ON d.dept_id = e.dept_id GROUP BY d.dept_name ORDER BY employee_count DESC 列: ('dept_name', 'employee_count') 结果: [('技术部', 2), ('市场部', 2), ('财务部', 2)]这个最小闭环已经覆盖了从自然语言到SQL再到执行结果的全流程。后续的所有优化都在这个基础上扩展。
3.5 表格对比:直接贴DDL和结构化描述的区别
很多开发者在第一次实现时会纠结:到底是把CREATE TABLE原样塞给模型,还是自己写字段描述?这里给出一个经验性对比。
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 直接贴 DDL | 信息完整,包含类型、约束、默认值 | 模型需要自己推断语义,字段无注释时效果差 | 表结构简单、字段命名清晰 |
| 结构化描述 | 语义明确,突出重点关系 | 维护成本高,表结构变化时要同步更新 | 业务字段多、命名不规范、需要精确控制 |
| DDL + 字段注释 | 兼顾结构和语义 | 提示词变长,可能超过上下文限制 | 数据库本身已有完整注释 |
实际项目里推荐“结构化描述优先,DDL 作为补充”。如果数据库已经维护了很好的字段注释,可以直接读取元数据自动生成描述,减少人工维护成本。
4. 用评测脚本验证模型是否真的“超越了基准”
4.1 为什么要在自己的数据上跑评测
先说一个判断:不要直接拿“超越人类基准”的新闻结论作为选型依据。原因有三个。
第一,公开评测集的问题分布和你的业务场景未必一致。电商、金融、教育、医疗的场景差异很大,同一个模型在不同场景下的表现可能完全不同。第二,公开评测集的标准答案不一定是你的业务规则。比如“活跃用户”的定义,你可能是“最近30天有登录”,评测集可能是“最近7天有购买”。第三,公开评测集的表结构通常比较规整,而真实业务的表和字段往往带着历史遗留问题。
所以,如果你想判断某个模型是否适合你们的业务,正确做法是:拿自己的表结构、自己的问题集、自己的标准SQL,跑一轮本地评测。
4.2 如何构造自己的评测集
构造评测集不需要太多数据量,但需要覆盖不同难度。建议按三个层次准备问题。
第一层是简单查询,覆盖单表过滤、排序、基础聚合。例如:
- 查询技术部所有员工姓名。
- 查询薪资大于12000的员工人数。
- 查询入职日期在2022年之后的员工姓名和部门名称。
第二层是中等查询,覆盖多表关联、分组聚合、条件组合。例如:
- 查询每个部门的平均薪资。
- 查询员工人数超过1人的部门名称。
- 查询每个部门薪资最高员工的姓名和薪资。
第三层是复杂查询,覆盖子查询、多级关联、去重、NULL处理。例如:
- 查询薪资高于部门平均薪资的员工姓名。
- 查询在所有部门中薪资排名前三的员工姓名。
- 查询没有员工的部门名称。
每个问题都要编写一条标准SQL,并记录预期结果。建议把评测数据保存成JSON或Excel,方便脚本读取。
最简单的格式如下:
[ { "question": "查询技术部所有员工姓名", "expected_sql": "SELECT emp_name FROM employee WHERE dept_id = 1", "expected_result": [["张伟"], ["李娜"]] }, { "question": "查询每个部门的平均薪资", "expected_sql": "SELECT d.dept_name, AVG(e.salary) FROM department d JOIN employee e ON d.dept_id = e.dept_id GROUP BY d.dept_name", "expected_result": [["技术部", 13500.0], ["市场部", 9750.0], ["财务部", 12000.0]] } ]4.3 评测脚本实现:结果集对比而不是字符串对比
评测脚本的核心逻辑是:对每个问题调用模型生成SQL,在数据库里执行,然后比较执行结果和标准结果。
直接比较SQL字符串不推荐,因为模型可能改写查询结构,却得到相同结果。正确做法是比较查询结果集。
import json import sqlite3 from openai import OpenAI client = OpenAI( api_key=os.getenv("OPENAI_API_KEY"), base_url=os.getenv("OPENAI_BASE_URL"), ) def generate_sql(question): # 省略提示词构造,复用前面代码 pass def execute_query(db_path, sql): conn = sqlite3.connect(db_path) try: cursor = conn.execute(sql) return cursor.fetchall() finally: conn.close() def normalize_result(rows): return sorted([tuple(row) for row in rows]) def evaluate(db_path, test_cases): total = len(test_cases) correct = 0 for case in test_cases: generated_sql = generate_sql(case["question"]) try: generated_result = execute_query(db_path, generated_sql) except Exception as exc: print(f"问题: {case['question']}") print(f"生成SQL: {generated_sql}") print(f"执行错误: {exc}") continue expected_result = case["expected_result"] if normalize_result(generated_result) == normalize_result(expected_result): correct += 1 else: print(f"问题: {case['question']}") print(f"生成SQL: {generated_sql}") print(f"期望结果: {expected_result}") print(f"实际结果: {generated_result}") print(f"结果一致率: {correct}/{total} = {correct / total:.2%}") if __name__ == "__main__": with open("test_cases.json", "r", encoding="utf-8") as f: test_cases = json.load(f) evaluate("test.db", test_cases)normalize_result的作用是把结果行排序,避免因为行顺序不同导致误判。如果查询结果包含浮点数,还要考虑精度问题,必要时用round统一精度。
4.4 评测结果怎么看
假设评测集有20条问题,模型生成的结果一致率是85%,那意味着有3条问题生成的SQL没有匹配预期结果。这时候要逐条分析失败原因,而不是只看一个准确率数字。
失败原因通常分几类:
- 模型对问题理解偏差。例如“近三个月”被理解成“最近90天”,而业务定义是“按自然月计算”。
- 模型缺少业务上下文。例如不知道“薪资”指的是税后薪资还是税前薪资。
- 模型在复杂查询上退化。问题越长、表越多,错误率越高。
- 生成SQL语法正确但逻辑错误。例如遗漏了
WHERE条件,或者连接条件写错。
这些失败案例是后续调优的重要输入。不要只追求整体准确率提升,要具体看哪类问题反复失败。
4.5 学习环境与生产环境的评测差异
在本地用 SQLite 跑通的评测流程,进入生产环境前还要补充几项能力。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据库 | SQLite | MySQL、PostgreSQL、SQL Server 等 |
| 数据量 | 几条示例数据 | 千万级以上数据,执行时间敏感 |
| 权限控制 | 无 | 需要限制模型可访问的表和字段 |
| 审计日志 | 无 | 需要记录问题、生成SQL、执行结果、用户信息 |
| 安全策略 | 无 | 需要防止注入、防误操作、防敏感数据泄露 |
| 回滚方案 | 无 | 查询超时、大结果集需要熔断 |
生产环境不要直接把模型生成的SQL交给数据库执行,建议至少增加一层人工确认或规则校验。后面会专门展开这部分。
5. 提升生成SQL质量的五个工程手段
5.1 增加字段语义注释和枚举值描述
模型生成SQL时,最容易错的是字段语义不明确。比如status字段,不同表里含义可能不同。有经验的开发人员知道status=1可能表示“启用”,但模型如果没有上下文,很可能猜错。
解决方法是把字段注释和枚举值写进 schema 描述。例如:
表 order: - status: 订单状态,类型 INTEGER,枚举值 0=已取消,1=待支付,2=已支付,3=已发货 - total_amount: 订单总金额,单位元,类型 REAL - created_at: 订单创建时间,类型 DATETIME枚举值信息特别重要,因为模型看到status=2时不知道2代表什么含义。有了枚举说明,模型才能准确理解“查询已支付订单”应该写成WHERE status = 2。
5.2 少样本示例:用几个经典查询约束模型输出格式
除了系统提示词里的规则,还可以加入少样本示例。少样本示例的作用是给模型一个“输出格式参考”,让模型知道字段名怎么写、别名怎么起、多表关联用什么风格。
例如在提示词中追加两个示例:
示例1: 问题:查询所有部门名称 SQL:SELECT dept_name FROM department 示例2: 问题:查询每个部门的平均薪资,按平均薪资降序排列 SQL:SELECT d.dept_name, AVG(e.salary) AS avg_salary FROM department d JOIN employee e ON d.dept_id = e.dept_id GROUP BY d.dept_name ORDER BY avg_salary DESC少样本示例不要太多,2到5条足够。示例必须覆盖你最常见的查询模式,而不是把各种复杂技巧都塞进去。示例太多会增加提示词长度,还可能让模型过度模仿示例结构。
5.3 对生成的SQL做语法校验和规则拦截
模型生成SQL后,不能直接扔给数据库执行。至少要做三层校验。
第一层是语法校验。可以先用数据库的解析能力试执行,也可以使用sqlglot这类库解析SQL语法。sqlglot的好处是不需要真实连接数据库,纯文本校验。
pip install sqlglotimport sqlglot sql = "SELECT * FORM employee" try: sqlglot.parse_one(sql) print("语法合法") except Exception as exc: print("语法错误:", exc)第二层是规则校验。例如:
- 是否只查询了允许访问的表。
- 是否包含
DELETE、UPDATE、DROP等高危操作。 - 是否包含
SELECT *,如果是生产环境建议拒绝。 - 是否缺少
WHERE条件,防止全表读取。
第三层是执行校验。在测试库上执行一次,观察错误和返回行数。如果返回行数过大,说明查询条件可能有问题。
这三层校验在文本转SQL系统中必不可少,尤其是在生产环境。模型再强,也不能完全信任它生成的SQL。
5.4 设置查询超时和结果集上限
生产环境执行模型生成的SQL,必须有超时机制。模型生成的SQL可能因为缺少过滤条件而执行全表扫描,也可能因为关联条件写错产生笛卡尔积,导致查询长时间不返回。
以 MySQL 为例,可以在连接层设置超时:
import pymysql conn = pymysql.connect( host="localhost", user="user", password="password", database="testdb", connect_timeout=5, read_timeout=10, )SQLite 可以用timeout参数控制连接等待时间,但真正处理慢查询还需要从SQL本身限制。推荐的思路是:
- 在生成的SQL外面包一层,限制返回行数,例如
LIMIT 1000。 - 使用数据库自带的执行超时配置。
- 在应用层增加异步超时任务,防止连接卡死。
结果集上限也很重要。模型生成的聚合查询可能只返回几十行,但漏了WHERE条件时可能返回几十万行。限制返回行数既能保护数据库,也能避免接口响应过大。
5.5 建立问题-标准SQL资产库
文本转SQL不是一次性上线就结束,它需要持续迭代。建议在项目早期就建立“问题-标准SQL”资产库,把真实的业务问题沉淀下来。
资产库字段建议包含:
| 字段 | 说明 |
|---|---|
| question | 业务人员提出的原始问题 |
| standard_sql | 人工审核后的标准SQL |
| table_names | 涉及的数据库表 |
| biz_owner | 业务负责人 |
| created_at | 创建时间 |
| updated_at | 最后修改时间 |
| status | 有效、废弃、待确认 |
这个资产库有多个用途:
- 作为评测集,持续评估模型迭代效果。
- 作为少样本示例的来源,挑选典型查询加入提示词。
- 作为回归测试集,防止模型升级后在某些查询上效果退化。
- 作为业务知识沉淀,帮助后续开发人员理解每个SQL的来龙去脉。
从实际经验看,资产库规模达到200条时,基本能覆盖多数业务场景的核心查询模式。
6. 文本转SQL落地的三个典型坑
6.1 坑一:提示词里没给表结构,模型乱编表名字段名
这是最容易被忽视的问题。很多人第一次调用大模型做文本转SQL时,直接把用户问题丢给模型,没有附带任何数据库结构信息。结果模型生成SQL时使用了它“想象”出来的表和字段,例如SELECT name FROM users,而实际上你的表叫t_user。
为什么会出现这种情况?因为大模型在训练时见过大量通用数据库结构,users、orders、products都是常见表名。你要求它针对当前数据库生成SQL,却不告诉它当前数据库有什么表,它只能按通用常识猜测。
解决方式很简单:在提示词中把当前数据库的 schema 信息完整提供给模型。这一点前面已经详细说明过,但值得再强调一次,因为大量实际项目翻车都倒在这一步。
6.2 坑二:多表关联查询缺少关联条件,生成笛卡尔积
多表查询是文本转SQL最常出错的场景。模型可能知道需要查询员工和部门两个表,但生成的SQL没有写ON条件,而是写成:
SELECT e.emp_name, d.dept_name FROM employee e, department d这种SQL在语法上合法,但会返回两个表的笛卡尔积。如果员工表有10000条,部门表有50条,结果就是500000行。查询结果数量巨大,还可能让应用卡死。
出现这个问题的原因是模型只知道要关联两个表,但不知道外键关系。所以在 schema 描述里显式写出外键非常关键:
employee.dept_id: 外键,关联 department.dept_id少样本示例里也可以放一条带JOIN ON的查询,帮助模型学习正确的关联写法。如果模型仍然经常漏掉关联条件,可以在规则校验层检测:如果一个SQL涉及多张表,但没有JOIN或WHERE中的等值关联条件,就标记为可疑SQL,拒绝执行。
6.3 坑三:复杂问题生成内容正确但SQL有性能隐患
有的SQL逻辑上没错,结果也对,但性能极差。例如:
SELECT * FROM employee WHERE salary IN ( SELECT MAX(salary) FROM employee GROUP BY dept_id )这条SQL能查出每个部门最高薪资的员工,但写法上不是最优。如果员工表很大,子查询和IN组合可能触发全表扫描。更优写法可以用窗口函数:
SELECT emp_id, emp_name, dept_id, salary FROM ( SELECT emp_id, emp_name, dept_id, salary, ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn FROM employee ) t WHERE rn = 1但要求大模型总是生成最优性能的SQL并不现实。工程上的对策是:把“结果正确”和“性能达标”分开看。先保证结果正确,再对慢查询做人工介入优化。生产环境的文本转SQL系统,最好把生成SQL的执行计划、执行耗时都记录下来,定期分析哪些查询模式性能差,然后针对性地在提示词或规则库里优化。
7. 生产环境的安全与权限控制
7.1 只读控制:禁止生成写操作
文本转SQL面向业务查询时,系统只能执行SELECT查询,不能允许模型生成INSERT、UPDATE、DELETE、DROP等语句。一旦模型在前面的系统提示词中理解有偏差,可能生成写操作。
安全做法是,在代码层面加一道强制校验。生成SQL后,先解析SQL类型,只放行SELECT。
import sqlglot def check_read_only(sql): try: expression = sqlglot.parse_one(sql) except Exception: return False if isinstance(expression, sqlglot.exp.Select): return True return False如果SQL不是SELECT,直接拒绝执行并记录日志。另外,数据库账号也必须是只读账号,从数据库权限层面再做一层兜底。这样即使应用层校验被绕过,数据库也不会被破坏。
7.2 数据权限:不同用户只能查不同数据
很多业务系统要求按用户权限过滤数据。例如销售主管只能看自己团队的销售数据,不能全公司。这个问题不能只靠提示词解决,因为模型生成SQL时并不知道当前用户是谁。
工程做法是在SQL执行层统一注入权限条件。系统拿到模型生成的SQL后,解析出主表或数据范围字段,然后自动加上权限过滤条件。
例如,原始SQL是:
SELECT order_id, amount FROM orders当前用户只能访问sales_team = '华东区'的数据,系统自动改写为:
SELECT order_id, amount FROM orders WHERE sales_team = '华东区'这种改写需要业务系统提供统一的权限模型,不能零散地写在提示词里。权限条件注入应该由代码控制,而不是交由模型自由发挥。
7.3 敏感字段脱敏
文本转SQL的真实业务中,数据库里可能包含手机号、身份证号、银行卡号等敏感字段。模型根据自然语言生成SQL时,不一定能识别哪些字段敏感,哪些用户有权限查看。
建议在 schema 描述里标记敏感字段:
- phone: 手机号,敏感字段,默认脱敏显示然后在执行层统一处理:如果查询结果包含phone字段,对返回数据做脱敏处理,例如显示前三位和后四位。敏感字段的脱敏规则要符合数据安全规范,不能把完整明文返回给前端。
7.4 审计日志:记录每一次生成与执行
文本转SQL系统与应用系统集成的过程中,审计日志是一个不可省略的环节。每条自然语言问题、生成的SQL、执行结果摘要、消耗时间、用户信息和系统反馈都要记录。
建议的日志字段:
| 字段 | 示例 |
|---|---|
| request_id | 8f3a1c9e2b |
| user_id | 1001 |
| question | 查询华东区订单金额 |
| generated_sql | SELECT ... |
| executed | true |
| error_message | null |
| result_rows | 200 |
| execute_ms | 345 |
| model_name | gpt-4o-mini |
| created_at | 2025-01-15 10:22:33 |
这些日志不仅是审计依据,也是后续优化模型和提示词的素材。当某个问题生成SQL失败时,可以回放日志重现场景。
8. 大模型选型与部署方式对比
8.1 云端API与本地部署的选型思路
文本转SQL项目选模型时,要先想清楚数据能不能出域。对很多企业来说,数据库表结构、字段注释、业务问题本身可能属于敏感信息,不能发送到第三方云端API。
云端 API 和本地部署的对比:
| 维度 | 云端API | 本地部署 |
|---|---|---|
| 部署成本 | 低,接入快 | 高,需要GPU服务器 |
| 数据安全 | 数据发送到第三方 | 数据不离开内部网络 |
| 响应速度 | 依赖网络 | 受算力影响 |
| 模型更新 | 服务方维护 | 需要自己升级 |
| 成本 | 按调用量计费 | 固定硬件和运维成本 |
| 离线支持 | 不支持 | 支持 |
如果只是内部验证,云端API更合适。生产环境涉及敏感数据,优先考虑私有化部署。但私有化部署的模型能力通常落后云端最新模型,需要在效果和合规之间做权衡。
8.2 模型能力评估:不要只看跑分
选型时不要只看“准确率排行榜”。建议用前面建好的评测集,在同样条件下对比候选模型,记录每个模型在不同难度问题上的表现。
可以先对比三个基础能力:
- 单表查询准确率。
- 多表关联查询准确率。
- 复杂子查询准确率。
再对比工程相关指标:
- 平均生成耗时。
- 失败率。
- 是否容易生成不可执行的SQL。
- 对提示词长度的敏感度。
把结果整理成表格,能更直观地看出每个模型的优劣。
9. 文本转SQL的未来方向
9.1 多轮对话:先澄清条件再写SQL
目前很多文本转SQL系统是单轮问答:用户问一句,系统生成一条SQL。但真实业务里,用户往往需要多次追问才能把需求表达清楚。例如“查一下上个月销售额”之后,可能会追加“不,要包含退款订单”。多轮对话需要系统维护上下文,记住前面的表名、条件和用户偏好。
大模型天然支持对话上下文,但要真正应用在文本转SQL中,还需要设计状态管理机制。系统需要知道当前对话关联哪些表、之前已经确认了哪些查询条件、后续追问是修改条件还是新增条件。这个问题目前还在快速发展中。
9.2 语义缓存:避免重复调用模型
同一类问题在业务系统中可能被反复提问。例如“查询今日订单量”每天都会被问很多次。每次调用大模型生成SQL,既费时间又费成本。可以引入语义缓存:对用户问题做向量化,计算与历史问题之间的相似度。如果相似度高,直接复用之前的SQL和结果,不再调用模型。
语义缓存要处理好数据时效性。订单量这类实时查询不能缓存太久,但例如“部门列表”这类静态数据可以缓存较长时间。缓存策略要按业务场景配置。
9.3 结合执行计划优化SQL
文本转SQL可以继续扩展,在生成SQL之后用数据库执行计划做二次优化。例如一些数据库支持EXPLAIN命令,可以分析SQL的执行计划。如果发现全表扫描、缺少索引、代价过高,可以自动改写SQL或提醒用户添加索引。
这个方向已经有一些工具在探索,但对小团队来说,更实际的做法是先把“生成正确SQL”这件事做好,再考虑自动优化SQL。执行计划自动改写的风险较高,需要充分的规则验证。
10. 可复用的文本转SQL项目检查清单
最后整理一份项目落地检查清单,适用于从验证阶段进入生产阶段的团队。
| 检查项 | 说明 | 状态 |
|---|---|---|
| 表结构描述是否完整 | 每个表、字段、主键、外键、枚举值是否都说明了 | 必查 |
| 提示词是否包含少样本示例 | 至少覆盖单表查询和多表查询 | 建议 |
| 是否设置了 temperature=0 | 保证生成结果稳定 | 建议 |
| 是否有语法校验 | 使用 sqlglot 或数据库试执行 | 必查 |
| 是否禁止写操作SQL | 代码层和数据库账号层双重保护 | 必查 |
| 是否设置查询超时 | 防止慢查询拖垮接口 | 必查 |
| 是否限制结果集大小 | 防止笛卡尔积返回大量数据 | 必查 |
| 是否记录审计日志 | 问题、SQL、结果、用户、时间 | 建议 |
| 是否建立评测集 | 至少覆盖简单、中等、复杂三类问题 | 必查 |
| 是否有敏感字段脱敏 | 手机号、身份证号等字段脱敏 | 按业务需要 |
| 是否做数据权限过滤 | 不同用户只能看授权数据 | 按业务需要 |
| 是否评估过模型性能 | 响应时间和准确率是否满足要求 | 必查 |
值得注意的是,这份清单并不要求每个项目第一次就做到完美。如果只是做技术验证,可以先保证前四项。进入生产环境后,再逐步补齐安全和审计能力。文本转SQL的本质价值在于降低非技术人员获取数据的门槛,但它背后仍然需要工程化的质量保障体系来兜底。所谓“超越人类基准”只是一个阶段性信号,真正决定项目成败的,是你在提示词设计、评测闭环、安全控制和生产运维上投入了多少持续优化。