面试被问到 Text2SQL,我用 DeepSeek 自己实现了一个
面试官问我:"如果让你用 AI 实现自然语言查数据库,你会怎么做?" 我当时答得不太好,回来研究了一下,发现核心原理并不复杂。记录一下。
背景
前段时间面一家后端岗,面试官问了我一个开放题:"现在 AI 这么火,如果让你做一个自然语言查数据库的功能,你怎么设计?"
我支支吾吾说了一些,面试官追问:"那 Schema 怎么传给大模型?提示词怎么设计?多表 JOIN 怎么处理?"
答得不好,挂了。
回来之后我决定把这个东西搞明白。花了一天时间,用 DeepSeek API + SQLite + Python 实现了一个 Text2SQL 的小 demo。过程中把 SQL 的 JOIN 语法也重新过了一遍,顺便整理成这篇文章。
一、SQL JOIN 复习:面试高频考点
面试问 SQL,JOIN 是必考的。我之前一直搞混 INNER JOIN 和 LEFT JOIN 的区别,这次算是彻底弄清楚了。
两张测试表
bash
employees(员工表):
| id | name | department | salary |
|----|-------|------------|--------|
| 1 | 张三 | 销售 | 50000 |
| 2 | 李四 | 工程 | 75000 |
| 3 | 王五 | 销售 | 60000 |
departments(部门表):
| id | name | manager |
|----|------|---------|
| 1 | 销售 | 赵经理 |
| 2 | 工程 | 钱经理 |
| 3 | 市场 | 孙经理 |
注意:市场部没有员工。这个差异是理解三种 JOIN 的关键。
INNER JOIN(内连接)
sql
SELECT e.name, d.manager
FROM employees e
INNER JOIN departments d ON e.department = d.name;
lua
| name | manager |
|------|---------|
| 张三 | 赵经理 |
| 李四 | 钱经理 |
| 王五 | 赵经理 |
只返回两边都能匹配上的。 市场部没有员工,所以孙经理不会出现。
面试记忆法:INNER JOIN = 交集,只取两边都有的。
LEFT JOIN(左连接)
sql
SELECT e.name, d.manager
FROM employees e
LEFT JOIN departments d ON e.department = d.name;
lua
| name | manager |
|------|---------|
| 张三 | 赵经理 |
| 李四 | 钱经理 |
| 王五 | 赵经理 |
这个例子里结果跟 INNER JOIN 一样,因为所有员工都能匹配到部门。但如果有个员工的 department 字段是"财务部",而 departments 表里没有"财务部",LEFT JOIN 会保留这个员工,manager 显示 NULL。
面试记忆法:LEFT JOIN 以左表为主,左表一条不少。
RIGHT JOIN(右连接)
sql
SELECT e.name, d.manager
FROM employees e
RIGHT JOIN departments d ON e.department = d.name;
sql
| name | manager |
|------|---------|
| 张三 | 赵经理 |
| 李四 | 钱经理 |
| 王五 | 赵经理 |
| NULL | 孙经理 |
市场部没有员工,但 RIGHT JOIN 以右表(departments)为主,所以孙经理会保留,name 显示 NULL。
面试记忆法:RIGHT JOIN 以右表为主,右表一条不少。
顺便提一句:SQLite 不支持 RIGHT JOIN,MySQL 和 PostgreSQL 支持。实际开发中很少用 RIGHT JOIN,一般把表顺序换一下用 LEFT JOIN 就行。
面试速查表
| 连接类型 | 保留规则 | 面试怎么答 |
|---|---|---|
| INNER JOIN | 只保留两边都能匹配的 | "取交集" |
| LEFT JOIN | 左表全保留,右表补 NULL | "以左表为主" |
| RIGHT JOIN | 右表全保留,左表补 NULL | "以右表为主" |
GROUP BY 聚合
sql
SELECT department, COUNT(*) AS 人数, AVG(salary) AS 平均工资
FROM employees
GROUP BY department;
lua
| department | 人数 | 平均工资 |
|------------|------|---------|
| 工程 | 1 | 75000 |
| 销售 | 2 | 55000 |
聚合函数:COUNT()、SUM()、AVG()、MAX()、MIN()。这几个面试必问,记住就行。
二、Text2SQL 原理:面试怎么答
面试官问"Text2SQL 怎么做",其实就三步:
- 提取数据库 Schema(表结构)
- 构造提示词,把 Schema 和用户问题一起发给大模型
- 执行大模型返回的 SQL,返回结果
sql
用户输入:"工程部门员工的姓名和工资是多少?"
↓
提示词 = Schema + 用户问题
↓
大模型(DeepSeek / ChatGPT / Claude)
↓
输出 SQL:SELECT name, salary FROM employees WHERE department = '工程';
↓
执行 SQL → 返回结果
面试的时候把这个流程说清楚,再加一句"核心是提示词工程",基本就过了。
三、动手实现
面试过了还不够,自己得能写出来。下面是我实现的完整代码。
环境
bash
pip install openai
DeepSeek 兼容 OpenAI 接口,所以直接用 openai 库。
python
import os
import sqlite3
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1"
)
建库
python
conn = sqlite3.connect("text.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY,
name TEXT,
department TEXT,
salary INTEGER
)
""")
sample_data = [
(1, "张三", "销售", 50000),
(2, "李四", "工程", 75000),
(3, "王五", "销售", 60000),
(4, "赵六", "工程", 80000),
(5, "钱七", "市场", 55000)
]
cursor.executemany("INSERT INTO employees VALUES(?,?,?,?)", sample_data)
conn.commit()
提取 Schema
python
def get_schema(table_name):
schema = cursor.execute(f"PRAGMA table_info({table_name})").fetchall()
return f"CREATE TABLE {table_name} (\n" + \
"\n".join([f" {col[1]} {col[2]}" for col in schema]) + "\n);"
这个函数做的事情就是把表结构拼成 CREATE TABLE ... 的格式。面试时可以说"用 PRAGMA table_info 动态提取表结构"。
调用大模型
python
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
提示词设计要点:
- 给 Schema ------ 不给 Schema 的话 AI 根本不知道你数据库长什么样
- 约束输出格式 ------ "只输出 SQL,不要 markdown",不然 AI 返回
sql ...直接报错 - 问题要具体 ------ "查销售部门工资大于5万的员工" 比 "查点数据" 靠谱得多
安全执行
python
def safe_execute(sql):
dangerous = ["DROP", "TRUNCATE", "ALTER"]
if any(kw in sql.upper() for kw in dangerous):
raise ValueError(f"危险操作被拦截: {sql}")
try:
return cursor.execute(sql).fetchall()
except Exception as e:
return f"SQL 执行错误: {e}"
面试时如果被问到"AI 生成的 SQL 安全吗",就说"加了关键词拦截,生产环境建议做白名单,只允许 SELECT"。
跑一下
python
question = "工程部门员工的姓名和工资是多少?"
sql_query = ask_deepseek(question, get_schema("employees"))
print(f"AI 生成的 SQL: {sql_query}")
results = safe_execute(sql_query)
for row in results:
print(row)
# AI 生成的 SQL: SELECT name, salary FROM employees WHERE department = '工程';
# ('李四', 75000)
# ('赵六', 80000)
四、多表 JOIN:面试官最爱追问的
单表查询太简单了,面试官肯定会追问"多表怎么处理"。
python
# 建第二张表
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()
python
# 把两张表的 Schema 都告诉 AI
def get_full_schema(table_names):
schema_str = ""
for table in table_names:
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"
return schema_str
schema_str = get_full_schema(["employees", "departments"])
问一个需要 JOIN 的问题:
python
question = "列出每个部门的员工人数和平均工资,没有员工的部门也要显示"
sql_query = ask_deepseek(question, schema_str)
print(sql_query)
AI 生成的 SQL:
sql
SELECT d.name AS department,
COUNT(e.id) AS employee_count,
AVG(e.salary) AS average_salary
FROM departments d
LEFT JOIN employees e ON d.name = e.department
GROUP BY d.id, d.name;
结果:
sql
| department | employee_count | average_salary |
|------------|---------------|----------------|
| 销售 | 2 | 55000 |
| 工程 | 2 | 77500 |
| 市场 | 0 | NULL |
面试官问"AI 怎么知道用 LEFT JOIN"------因为提示词里说了"没有员工的部门也要显示",AI 理解了这个语义。
五、踩过的坑
坑 1:AI 返回 markdown 格式
go
```sql
SELECT name FROM employees;
```
直接 cursor.execute() 报错。解决:提示词里加"不要使用任何 markdown 格式"。
坑 2:中文匹配失败
问"销售部门的员工",AI 生成 WHERE department = '销售',结果返回空。后来发现是 SQLite 中文编码的问题,建库时统一 UTF-8 就好了。
坑 3:AI 搞错表关系
两张表没有外键约束,AI 错误地用了 ON e.id = d.id。解决:在 Schema 里加注释说明关联关系:
python
schema_str = """
CREATE TABLE employees (
id INTEGER,
name TEXT,
department TEXT, -- 关联 departments.name
salary INTEGER
);
CREATE TABLE departments (
id INTEGER,
name TEXT,
manager TEXT
);
"""
加注释真的有用。 测了几次,加了注释之后 AI 生成的 JOIN 条件基本都对了。
六、进阶:提示词工程技巧
Few-shot 示例
在提示词里给几个例子,AI 输出会稳定很多。面试时可以说"用 Few-shot 提升生成准确率":
python
prompt = f"""
你是一个 SQL 专家。请根据以下 Schema 和示例,生成正确的 SQL。
## Schema
{schema}
## 示例
问题:工资最高的员工是谁?
SQL:SELECT name FROM employees ORDER BY salary DESC LIMIT 1;
问题:每个部门有多少人?
SQL:SELECT department, COUNT(*) FROM employees GROUP BY department;
## 规则
1. 只输出 SQL,不要任何解释
2. 优先使用 LEFT JOIN 确保数据不丢失
## 问题
{query}
"""
多轮对话
还可以做上下文记忆,支持追问:
python
messages = [
{"role": "system", "content": f"你是 SQL 专家。数据库 Schema:\n{schema}"}
]
def chat(query):
messages.append({"role": "user", "content": query})
response = client.chat.completions.create(
model="deepseek-v4-flash",
messages=messages
)
answer = response.choices[0].message.content
messages.append({"role": "assistant", "content": answer})
return answer
chat("列出所有员工")
chat("只要工程部门的") # 自动加 WHERE
chat("按工资从高到低排序") # 自动加 ORDER BY
七、方案对比
面试官可能会问"为什么不直接用现成的框架":
| 方案 | 优点 | 缺点 |
|---|---|---|
| 自己写提示词(本文) | 灵活、可控、学习原理 | 需要手动维护 Schema |
| LangChain SQL Agent | 开箱即用 | 黑盒、依赖重 |
| vanna.ai | 专用框架、支持微调 | 学习成本高 |
| Chat2DB | 有 UI 界面 | 定制性差 |
面试时可以说:"学习阶段自己写理解原理,生产环境可以考虑 LangChain 或 vanna.ai。"
八、局限性(面试加分项)
面试时主动说出局限性,会加分:
- 复杂查询:多层嵌套子查询、窗口函数,AI 容易出错
- 歧义问题:"最近的订单"------最近是 7 天还是 30 天?AI 不知道
- 安全性:生成的 SQL 可能有注入风险
- 性能:AI 生成的 SQL 不一定是最优执行计划
最佳实践:
- Schema 加注释
- 提示词加 Few-shot 示例
- 执行前用
EXPLAIN检查 - 权限控制:只允许 SELECT
总结
| 环节 | 技术方案 |
|---|---|
| 数据库 | SQLite |
| 大模型 | DeepSeek API |
| 核心技术 | 提示词工程 + Schema 注入 |
| 支持操作 | SELECT / INSERT / UPDATE / DELETE / JOIN |
做完这个 demo 之后,我再回答面试官那个问题就顺畅多了。核心就是把 Schema 和用户问题一起发给大模型,剩下的就是提示词工程的细节了。
代码不多,核心逻辑几十行,感兴趣的同学可以自己跑一下。有问题评论区聊。