Text2SQL 实战:用 DeepSeek 大模型实现自然语言查询 SQLite 数据库
让 AI 帮你写 SQL,告别繁琐的表结构记忆和语法调试
在日常开发中,我们经常需要与数据库打交道。写 SQL 查询本身并不复杂,但当表变多、字段变复杂时,记字段名、写 JOIN、调条件总是让人分心。Text2SQL 技术应运而生——你只需要用自然语言描述“我要什么”,AI 直接帮你生成 SQL 语句。本文将带你从零开始,用 Python + SQLite + DeepSeek 大模型搭建一个完整的 Text2SQL 系统,并深度剖析每个底层细节。
一、整体架构预览
我们先看一眼最终实现的效果:
graph LR
A[自然语言问题] --> B[DeepSeek 大模型]
B --> C[生成 SQL 语句]
C --> D[SQLite 执行]
D --> E[返回查询结果]
用户问:“工程部门员工的姓名和工资是多少?”
系统输出:SELECT name, salary FROM EMPLOYEES WHERE department = '工程';
然后自动执行并打印结果。整个过程无需手写一行 SQL。
二、环境准备:OpenAI 兼容的 DeepSeek 客户端
首先,我们需要调用 DeepSeek 的 API。DeepSeek 提供了与 OpenAI 完全兼容的接口,所以直接用 openai 库即可。
from openai import OpenAI
client = OpenAI(
api_key="sk-dbd5d0d816254a66825aa2c0841a35b2",
base_url="https://api.deepseek.com/v1"
)
🔍 知识点拆解:
- OpenAI 兼容性:DeepSeek 的接口与 OpenAI 保持一致,这意味着你可以用任何支持 OpenAI SDK 的工具链来调用 DeepSeek 模型,迁移成本极低。
- base_url:指向 DeepSeek 的服务端点,而不是 OpenAI 的
https://api.openai.com/v1。这相当于告诉客户端“我换了个后端”。 - api_key:这里直接写在了代码里,实际项目中务必用环境变量或密钥管理服务,防止泄露。
三、SQLite:文件型关系数据库
我们选用 SQLite 作为演示数据库,因为它无需安装、无需配置、单文件存储,非常适合本地开发和教学。
3.1 连接与游标
import sqlite3
conn = sqlite3.connect("test.db") # 创建/连接数据库文件
cursor = conn.cursor() # 创建游标对象
🔍 游标(Cursor)是什么?
游标是数据库操作中的“指针”,它代表一个执行 SQL 语句的上下文环境。你可以通过游标来执行 SQL、获取结果。一个连接可以创建多个游标,每个游标独立管理自己的执行状态。底层实现中,游标会维护一个结果集和当前读取位置。
3.2 建表与批量插入
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
department TEXT,
salary INTEGER
)
""")
sample_data = [
(6, "张三", "销售", 5000),
(7, "李四", "市场", 6000),
(8, "王五", "研发", 7000),
(9, "赵六", "工程", 8000),
(10, "王二", "市场", 9000),
]
cursor.executemany(
"INSERT INTO employees VALUES (?,?,?,?)",
sample_data
)
conn.commit()
🔍 重点知识点:
IF NOT EXISTS:避免重复执行脚本时表已存在报错,保证幂等性。AUTOINCREMENT:在 SQLite 中,INTEGER PRIMARY KEY默认就会自增,但加上AUTOINCREMENT可以保证自增的值永不重用(更安全,但稍微慢一点)。executemany:批量插入的利器。它接受一个 SQL 模板(用?作为占位符)和一个元组列表,内部会一次性编译 SQL 并逐行绑定参数。相比循环执行execute,这能大幅减少 SQL 解析次数,提升性能。- 占位符
?:SQLite 使用?作为参数占位符(位置绑定),而不是%s或:name。这可以有效防止 SQL 注入,因为参数值是独立于 SQL 语句传输的,不会被当作代码执行。 - 事务与
commit():executemany默认不会自动提交,必须显式调用conn.commit()将更改持久化到磁盘。这是事务控制的基础——要么全部成功,要么全部回滚。你可以用conn.rollback()撤销未提交的修改。
四、获取数据库 Schema(表结构信息)
要让 AI 生成正确的 SQL,必须告诉它数据库中有哪些表、每个表的字段名和类型。这就是 Schema(模式)的作用。
schema = cursor.execute("PRAGMA table_info(employees)").fetchall()
print(schema)
# 输出: [(0, 'id', 'INTEGER', 0, None, 1), (1, 'name', 'TEXT', 0, None, 0), ...]
🔍 PRAGMA table_info 是什么?
PRAGMA 是 SQLite 特有的“编译指令”或“元数据查询命令”。table_info(table_name) 返回一个结果集,每一行代表表中的一个列,包含:
- 列序号(cid)
- 列名(name)
- 列类型(type)
- 是否非空(notnull)
- 默认值(dflt_value)
- 是否为主键(pk)
我们用 fetchall() 取出所有列信息,然后拼接成建表语句的字符串形式:
schema_str = "CREATE TABLE EMPLOYEES (\n" + "\n".join([f"{col[1]} {col[2]}" for col in schema]) + "\n)"
🔍 列表推导式与 f-string:
[f"{col[1]} {col[2]}" for col in schema] 遍历每个列,取出 name 和 type,拼接成类似 "id INTEGER" 的字符串,再用换行符连接。这样我们就得到了一个可读的 Schema 描述,供 AI 理解。
五、核心函数:ask_deepseek —— 自然语言转 SQL
def ask_deepseek(query, schema):
prompt = f"""
这是一个数据库的Schema:
{schema}
根据这个Schema, 请输出一个SQL查询来回答以下问题。
只输出SQL 查询语句本身,不要使用任何Markdown格式,
不要包含反引号、代码块标记或额外说明。
问题:{query}
"""
response = client.chat.completions.create(
model="deepseek-v4-flash",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
🔍 提示词工程(Prompt Engineering)的关键点:
- 明确约束:“只输出 SQL 查询语句本身”——防止 AI 输出解释、Markdown 代码块或多余文字,否则我们后续无法直接
execute。 - 注入 Schema:将表结构作为上下文提供给模型,这是 Text2SQL 的核心。没有 Schema,AI 只能猜测字段名,错误率极高。
- 使用最新模型:这里用了
deepseek-v4-flash(实际可用deepseek-chat等),DeepSeek 在代码生成和 SQL 任务上表现优异。 max_tokens:限制输出长度,防止过长回复。
🔍 底层调用过程:
client.chat.completions.create 发送 HTTP 请求到 DeepSeek 服务端,服务端运行大模型推理,返回一个 JSON 响应。我们从中提取 choices[0].message.content 即生成的 SQL。
六、实战检验:查询、插入、更新、删除
6.1 查询(SELECT)
question = "工程部门员工的姓名和工资是多少?"
sql_query = ask_deepseek(question, schema_str)
print(sql_query) # SELECT name, salary FROM EMPLOYEES WHERE department = '工程';
results = cursor.execute(sql_query).fetchall()
for row in results:
print(row) # ('赵六', 8000)
AI 自动识别了“工程部门”对应字段 department,并只选择了 name 和 salary 两列,完美!
6.2 插入(INSERT)
question = "在销售部门增加一个新员工,姓名为张三,工资为45000"
sql_query = ask_deepseek(question, schema_str)
print(sql_query) # INSERT INTO EMPLOYEES (name, department, salary) VALUES ('张三', '销售', 45000);
cursor.execute(sql_query)
conn.commit()
注意:这里 AI 没有指定 id,因为 id 是自增主键,数据库会自动分配。它甚至用了完整的列名列表,这是最佳实践。
6.3 更新(UPDATE)
question = "将王二的工资调整为55000"
sql_query = ask_deepseek(question, schema_str)
print(sql_query) # UPDATE EMPLOYEES SET salary = 55000 WHERE name = '王二';
cursor.execute(sql_query)
conn.commit()
AI 直接根据“王二”这个姓名作为条件,非常合理(假设姓名唯一)。
6.4 删除(DELETE)
question = "删除市场部门的王二"
sql_query = ask_deepseek(question, schema_str)
print(sql_query) # DELETE FROM EMPLOYEES WHERE department='市场' AND name='王二';
cursor.execute(sql_query)
conn.commit()
这里 AI 加上了 department 条件以避免误删同名员工,安全考虑很到位。
七、多表关联:理解表间关系
很多业务场景不止一张表。我们新增一个 departments 表,存储部门经理信息。
cursor.execute("""
CREATE TABLE IF NOT EXISTS departments(
id INTEGER PRIMARY KEY,
name TEXT,
manager TEXT
)
""")
sample_departments = [
(1, "销售", "王经理"),
(2, "工程", "李经理"),
(3, "市场", "张经理")
]
cursor.executemany("INSERT INTO departments VALUES (?,?,?)", sample_departments)
conn.commit()
此时数据库有两张表:employees 和 departments。我们需要把完整的 Schema 重新拼接给 AI。
tables = ["employees", "departments"]
schema_str = ""
for table in tables:
schema = cursor.execute(f"PRAGMA table_info({table})").fetchall()
schema_str += f"CREATE TABLE {table} (\n" + "\n".join([f"{col[1]} {col[2]}" for col in schema]) + "\n);\n\n"
现在,我们问一个涉及两张表的问题:
question = "根据两个表之间的关系,列出每个部门的员工人数和平均工资"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)
-- 输出:
-- SELECT d.name, COUNT(e.id) AS employee_count, AVG(e.salary) AS avg_salary
-- FROM departments d LEFT JOIN employees e ON d.name = e.department
-- GROUP BY d.id, d.name;
🔍 AI 如何发现“关系”?
它通过字段名相似性(departments.name 与 employees.department)推断出两张表可以通过部门名称关联。虽然我们没有显式定义外键,但 AI 利用语义理解完成了 JOIN 条件的推断。这就是大模型的强大之处。
🔍 LEFT JOIN 与 GROUP BY:
- 使用
LEFT JOIN保证没有员工的部门也会出现(计数为 0)。 GROUP BY d.id, d.name按部门分组,然后计算每组的员工数(COUNT(e.id))和平均工资(AVG(e.salary))。
八、总结与思考
通过本文,我们完整实现了一个 Text2SQL 系统,核心收益如下:
| 传统方式 | Text2SQL 方式 |
|---|---|
| 需要记住所有表名、字段名 | 只需用业务语言描述需求 |
| 需要掌握 SQL 语法细节 | AI 自动生成符合语法的 SQL |
| 多表关联容易出错 | AI 推断关联关系 |
| 调试 SQL 耗时 | 生成即正确(大部分情况下) |
🔑 关键知识点回顾:
- SQLite 游标(Cursor):执行 SQL 的上下文,管理结果集。
executemany:批量绑定性插入,提升性能并防止注入。- 事务提交
commit():确保数据持久化,可回滚。 PRAGMA table_info:获取表结构的元数据命令。- 提示词工程:清晰限定输出格式(纯 SQL),并注入完整 Schema。
- 多表 JOIN:大模型通过字段语义自动发现关联,无需外键定义。
🚀 延伸思考:
- 如果表结构复杂(几十张表),如何选择相关的表送入 Prompt?可以先用向量检索或规则筛选。
- 如果生成 SQL 有误,可以加入“校验-重试”机制(例如执行失败则让 AI 修正)。
- 在实际生产环境,建议加上 SQL 执行权限限制,防止误删数据。
Text2SQL 正在改变我们与数据交互的方式——它让数据查询不再是程序员的专利,产品、运营同学也能用自然语言自由探索数据。而这一切,得益于大模型对代码和结构化数据的深刻理解。
希望本文能帮你快速上手 Text2SQL,并在自己的项目中发挥创造力。动手试试吧,你的下一个应用可能就是“对话式 BI”工具!