几百张表的 Schema 为什么不能全塞给大模型?大厂实践与向量数据库实战
几百张表的 Schema 为什么不能全塞给大模型?大厂实践与向量数据库实战

摘要:做 Text-to-SQL 或数据开发助手时,把整个数仓的表结构全部放进提示词,看上去最简单,实际却会带来上下文干扰、token 成本、响应延迟和权限暴露。本文从效率、成本、安全三个角度分析问题,梳理 Uber、Pinterest、Snowflake、Databricks 的公开实践,并用 300 张模拟表、Chroma 向量数据库和 SQLite 给出一个可运行的 Schema 检索案例。
关键词:数据开发、Text-to-SQL、Schema RAG、向量数据库、Chroma、元数据、语义层、Claude
假设数仓里有 300 张表,用户问:
“统计 2026-10-01 各支付渠道的实收金额和退款金额。”
最省事的办法,似乎是把 300 份 CREATE TABLE 全部交给大模型,再让它自由发挥。但这个问题真正的难点,往往不是模型会不会写 SUM()、GROUP BY,而是它能否先找到支付流水、退款流水和渠道维表,并正确理解“实收”和“退款”的业务口径。
一句话结论:当表越来越多,Text-to-SQL 首先会变成元数据检索问题,然后才是 SQL 生成问题。向量数据库可以帮助找表,但不能代替业务语义、权限控制和结果验证。
一、为什么不能把全部 Schema 不加筛选地塞进去?
1. 效率:上下文更长,不代表有效信息更多
数仓里通常存在大量名字相似、粒度不同、重复建设的表。例如订单明细、订单日报、支付流水、财务结算,都可能出现“金额”和“渠道”字段。全部放入后,模型需要在一次请求里自行排除大量干扰。
更麻烦的是,表多和表宽会同时发生。Uber 在 QueryGPT 工程文章中提到,一些核心表超过 200 个字段,一张表的 Schema 就可能占用 40K—60K token;当时即使使用 128K 上下文模型,放入几张宽表也会遇到问题。今天模型的上下文容量更大,但“无关信息如何排除”并没有因此消失。
最后还要区分三件事:
- 找到候选表:检索是否召回支付、退款和渠道表;
- 理解业务含义:实收是否只算支付成功,退款按退款时间还是原支付时间统计;
- 生成正确 SQL:关联键、分区过滤、状态值和聚合粒度是否正确。
把更多 DDL 塞进去,只增加了模型可见的信息,并没有自动补齐后面两项能力。
2. 成本:每一次提问,都在重复支付全量 Schema 的输入费用
做一个纯粹用于说明计算方式的假设:每张表整理后的 Schema 平均为 800 token,每天发起 100 次查询,模型输入单价假设为每百万 token 2 美元。
| 方式 | 单次 Schema 输入 | 100 次请求的 Schema 输入 | 假设输入费用 |
|---|---|---|---|
| 300 张表全部放入 | 300 × 800=240K token | 24M token | $48 |
| 检索后只放入 5 张 | 5 × 800=4K token | 0.4M token | $0.80 |
只看这部分输入,后者是前者的 1/60。但这不是实际项目账单:它没有计算系统提示词、用户问题、输出 token、向量模型、索引维护、缓存和人工复核,也不代表所有任务都应只给 5 张表。
因此,真正要优化的是单位正确结果的总成本,而不是机械追求最少 token。如果候选表给少了,反复追问和错误查询反而可能更贵。
3. 安全:Schema 不是可以随便全量外发的“无敏感信息文件”
表名、字段名和注释可能暴露客户标签、风控规则、内部项目、财务指标和组织结构。历史 SQL 还可能带有真实条件值、样例 ID 或业务筛选逻辑。
如果先让模型看到全部表,再告诉它“不要访问没有权限的表”,这已经晚了。更合理的顺序是:
先根据用户、租户、业务域和密级过滤可检索范围,再做相似度搜索,最后只把有权使用的表交给生成模型。
向量也不是脱敏手段。向量索引、原始检索文档及其中的权限元数据,都需要访问控制、备份保护和删除更新机制。

二、行业大厂怎么做?不是只靠一个向量数据库
1. Uber QueryGPT:先分流业务域,再选表、裁剪列
Uber 公开的 QueryGPT 演进很能说明问题。
早期版本会把用户问题向量化,通过相似度搜索取得 3 张相关表和 7 个 SQL 示例。表和示例较少时可行,接入更多表后,生成准确率开始下降。团队还发现,直接拿自然语言问题去搜索原始 DDL 和 SQL,并不一定得到好结果。
后续设计增加了几层处理:
- Workspace:按广告、出行、核心服务等业务域组织表和 SQL 示例;
- Intent Agent:先把问题映射到一个或多个业务域,缩小搜索半径;
- Table Agent:选择候选表,交给用户确认或修改;
- Column Prune Agent:删除与当前问题无关的列,减少 token、费用和延迟;
- 评估集:人工确认问题对应的意图、必要表和标准 SQL,分别评估意图、表重叠、能否执行、是否有输出和 SQL 相似度。
值得关注的是,Uber 并没有把“SQL 能执行”当成“SQL 正确”。文章专门举例:错误的状态过滤条件可能让查询正常执行却返回 0 行。即使返回了数据,也仍需核对业务结果。
2. Pinterest Text-to-SQL:表摘要+历史查询,两段式选表
Pinterest 在 Querybook 中建设 Text-to-SQL。其第一版由用户先选表,系统再读取表结构生成 SQL;后来团队发现,不知道哪张表能用本身就是用户的主要困难。
公开案例中的改进思路包括:
- 离线为表摘要和历史查询摘要建立向量索引;
- 表摘要结合表结构与近期示例查询,描述表的内容和潜在使用场景;
- 优先纳入质量较高的核心表,而不是无差别索引全部数据资产;
- 使用 OpenSearch 做相似度搜索,先取 Top N,再让模型结合问题选出 Top K;
- 把候选表交给用户确认,然后进入 SQL 生成;
- 对低基数字段补充真实存储值,避免把
WEB错写成web; - 通过精简 Schema 和按标签排除字段,控制宽表上下文。
这里的关键不是“用了 OpenSearch”,而是把表文档、历史查询、数据治理和人工确认组合起来。如果表注释长期缺失,单纯更换向量数据库很难解决根本问题。
3. Snowflake Cortex Analyst:用语义模型和已验证 SQL 约束生成
Snowflake 的语义视图用于定义逻辑表、事实、指标、维度及其关系,把业务语言与物理字段连接起来。例如,“净收入”不只是一个中文字段名,还可以被定义为统一的计算表达式和聚合方式。
Cortex Analyst 的 Verified Query Repository 则保存“自然语言问题+经过验证的 SQL”。遇到相似问题时,可以参考这些查询。它解决的不是找表本身,而是让组织已有的正确口径变成可复用资产。
4. Databricks Genie:知识库、字段隐藏和示例 SQL
Databricks Genie Agent 的知识库可以维护表描述、业务同义词、JOIN 关系、指标和筛选条件的 SQL 表达式。无关、重复或容易误导的列可以在 Agent 上下文中隐藏,而不改变底层表。
示例 SQL、Unity Catalog 函数和文本指令则用于补充组织特有的逻辑。这与 Snowflake 的已验证查询有一个共同方向:不要把所有业务知识都寄希望于模型现场猜测。

5. 四种实践放在一起看
| 实践 | 主要解决的问题 | 可以直接借鉴的做法 |
|---|---|---|
| Uber QueryGPT | 业务域过大、宽表、选表不稳定 | 业务域分流、用户确认表、列裁剪、分阶段评估 |
| Pinterest Text-to-SQL | 用户不知道去哪找表 | 表摘要和历史查询索引、Top N 到 Top K、核心表优先 |
| Snowflake Cortex Analyst | 业务概念与物理字段不一致 | 语义模型、指标定义、已验证 SQL 仓库 |
| Databricks Genie | 企业术语、JOIN 和指标逻辑难维护 | 知识库、同义词、字段隐藏、示例 SQL |
这些系统并不完全相同,公开文章也不一定代表它们当前内部实现的全部细节,但共同方向非常清楚:先缩小范围,再补充高质量语义,最后验证结果。
三、一套可落地的 Schema RAG 架构

可以把它拆成离线和在线两条链路。
1. 离线:把原始 Schema 加工成“可检索文档”
元数据平台仍是事实来源,向量数据库只保存可重建的检索副本。每张表的检索文档可以包含:
表名:dwd_trade_payment_di
业务域:交易
表描述:支付成功流水,paid_amount 用于计算实收金额
业务同义词:支付、收款、实收、支付渠道
字段:channel_id、pay_status、paid_amount、pay_time、dt
关联关系:channel_id = dim_payment_channel.channel_id
数据粒度:一行一条支付流水
分区与使用限制:dt 为日期分区,实收只统计 SUCCESS
权限标签:交易域、租户、密级、负责人
不要只索引 CREATE TABLE。表名和字段类型告诉模型“有什么”,业务描述、同义词、关联关系和使用限制才更接近“应该怎么用”。
历史 SQL 也很有价值,但需要治理:去除敏感值、确认正确性、记录适用方言和日期,避免把废弃查询当成标准答案。
2. 在线:检索、重排、裁列、生成、验证
推荐流程如下:
- 问题补全与意图识别:识别时间、指标、维度、业务域和 SQL 方言;
- 权限过滤:先限定租户、业务域、密级和可用表集合;
- 混合检索:关键词检索处理准确表名和字段名,向量检索处理“实收”“收款”等自然语言表达;
- 重排:结合相似度、核心表等级、血缘、使用热度和历史反馈选出 Top K;
- 列裁剪:只保留当前问题需要的字段、关联键、分区键和状态字段;
- 生成 SQL:同时提供必要的指标口径、JOIN 关系和已验证示例;
- 静态检查与试算:检查表字段是否存在、是否只读、是否有分区过滤,再执行
EXPLAIN或小样例验证; - 受控执行:只读账号、扫描量和超时限制,高风险查询转人工审批。
Top K 不应写成永远固定的 5。简单单表问题可能只需要 1 张,跨支付和退款的问题可能需要 3—5 张,复杂经营分析可能需要更多。应通过召回率和最终正确率确定阈值,不足时允许扩大候选范围或转人工。
四、具体案例:300 张模拟表,怎样只把相关表交给 SQL 生成步骤?
1. 案例目标与技术栈
我构造了一个包含 300 张模拟表的元数据目录,覆盖交易、用户、财务、物流、营销和风控六个业务域。用户问题仍然是:
“统计 2026-10-01 各支付渠道的实收金额和退款金额。”
正确回答至少需要:
| 表 | 用途 |
|---|---|
dwd_trade_payment_di | 支付成功流水,汇总实收金额 |
dwd_trade_refund_di | 退款成功流水,汇总退款金额 |
dim_payment_channel | 把渠道编号转成渠道名称 |
案例使用:
- Chroma:保存表文档向量及业务域元数据;
- FastEmbed:在本地运行
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2多语言向量模型; - SQLite:创建三张样例表,验证 SQL 和查询结果;
- 审核后的 SQL 模板:代替外部大模型调用,让案例可以离线复现。
这不是一个“自动生成任意 SQL 的生产系统”,而是一个聚焦 Schema 检索的最小实验。生产接入时,可以把模板位置替换成 Claude 或其他模型,但后面的校验、权限和人工确认不能因此省略。
2. 运行环境与完整文件
配套文件位于本文旁的 schema-rag-demo 目录:
schema_rag_demo.py:300 张模拟表、向量索引、检索、SQL 和结果断言;requirements.txt:Python 依赖;.models/:首次运行后保存本地向量模型缓存。
Windows Git Bash 下运行:
cd schema-rag-demo
python -m venv .venv
.venv/Scripts/python -m pip install -r requirements.txt
.venv/Scripts/python schema_rag_demo.py
首次运行需要下载向量模型,后续会使用本地缓存。如果 Hugging Face 直连受限,可以在首次运行前设置 HF_ENDPOINT=https://hf-mirror.com;模型下载完成后,可设置 HF_HUB_OFFLINE=1 验证离线运行。Chroma 使用内存模式,每次启动重新建立演示索引;生产环境可以改用持久化客户端或服务端。
3. 第一步:建立 300 张表的向量索引
核心思想是“一表一份检索文档”,并额外保存业务域等用于过滤的元数据。下面省略具体表定义,展示索引主流程:
import chromadb
from chromadb.config import Settings
from fastembed import TextEmbedding
embedder = TextEmbedding(
model_name="sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2",
cache_dir=".models",
)
client = chromadb.EphemeralClient(
settings=Settings(anonymized_telemetry=False)
)
collection = client.create_collection(
name="warehouse_schema",
embedding_function=None,
metadata={"hnsw:space": "cosine"},
)
documents = [table_document(table) for table in tables]
vectors = list(embedder.embed(documents))
collection.add(
ids=[table["name"] for table in tables],
documents=documents,
embeddings=[vector.tolist() for vector in vectors],
metadatas=[
{
"table_name": table["name"],
"domain": table["domain"],
"tier": table["tier"],
"description": table["description"],
}
for table in tables
],
)
这里显式生成向量,是为了让“文本如何进入向量数据库”的过程更清楚。实际项目也可以在创建 Collection 时配置一致的 embedding function。
4. 第二步:先按业务域过滤,再检索 Top 5
演示中的 route_domain() 根据“支付、退款、实收”等关键词,将问题分到 trade 域。生产系统可以换成规则、分类模型或 LLM Intent Agent,但无法唯一确定业务域时,应该要求用户补充,而不是直接猜。
query_vector = list(embedder.embed([question]))[0].tolist()
result = collection.query(
query_embeddings=[query_vector],
n_results=5,
where={"domain": "trade"},
include=["documents", "metadatas", "distances"],
)
where={"domain": "trade"} 很重要:它不是让模型在 300 张表里自由挑选,而是先把搜索范围限制在交易域。生产环境还应把租户、权限和密级加入过滤条件。
5. 第三步:把检索结果组织成生成提示词
def build_generation_prompt(question, retrieved):
context = "\n\n".join(item["document"] for item in retrieved)
return (
"你是数据开发助手。只能使用下面给出的表、字段和关联关系,"
"实收金额只统计支付成功流水,退款金额只统计退款成功流水,"
"必须添加日期分区过滤,不得编造其他表或字段。\n\n"
f"候选 Schema:\n{context}\n\n用户问题:{question}"
)
注意,提示词中的“不得编造”只是约束,不是安全保证。是否使用了不存在的表和字段,仍要在生成后解析 SQL 并检查。
6. 第四步:必要表没有召回,就停止后续执行
required = {
"dwd_trade_payment_di",
"dwd_trade_refund_di",
"dim_payment_channel",
}
missing = required - set(selected_table_names)
if missing:
raise RuntimeError(f"检索结果缺少必要表,停止生成和执行: {missing}")
这是一道很简单的检索闸门。它不能证明候选表足够回答所有问题,却能防止本案例在缺少退款表时仍然“硬着头皮生成”。
真实系统可以用人工标注的必要表集合计算 Recall@K:
Recall@K=前 K 个候选中成功召回的必要表数量 ÷ 该问题全部必要表数量
如果 Recall@5 持续不达标,应优先检查表文档、业务域路由、向量模型和重排,而不是继续增加 SQL 生成提示词。
7. 第五步:用审核后的 SQL 验证流程
本案例的 SQL 模板如下。支付和退款分别聚合,再关联渠道维表,可以避免同一订单多笔支付或多笔退款直接 JOIN 后造成金额重复放大。
WITH payment AS (
SELECT channel_id, SUM(paid_amount) AS paid_amount
FROM dwd_trade_payment_di
WHERE dt = '2026-10-01' AND pay_status = 'SUCCESS'
GROUP BY channel_id
),
refund AS (
SELECT channel_id, SUM(refund_amount) AS refund_amount
FROM dwd_trade_refund_di
WHERE dt = '2026-10-01' AND refund_status = 'SUCCESS'
GROUP BY channel_id
)
SELECT
c.channel_name,
COALESCE(p.paid_amount, 0) AS paid_amount,
COALESCE(r.refund_amount, 0) AS refund_amount,
COALESCE(p.paid_amount, 0) - COALESCE(r.refund_amount, 0) AS net_received
FROM dim_payment_channel c
LEFT JOIN payment p ON c.channel_id = p.channel_id
LEFT JOIN refund r ON c.channel_id = r.channel_id
ORDER BY c.channel_name;
执行前,演示代码做了三项检查:只允许 SELECT 或 WITH、不允许多条语句、先运行 EXPLAIN QUERY PLAN,然后打开 SQLite 的 query_only 模式。
single_statement = sql.strip().rstrip(";")
if not re.match(r"^(SELECT|WITH)\b", single_statement, re.IGNORECASE):
raise ValueError("只允许 SELECT 或 WITH 查询")
if ";" in single_statement:
raise ValueError("一次只允许执行一条 SQL")
connection.execute("EXPLAIN QUERY PLAN " + single_statement)
connection.execute("PRAGMA query_only = ON")
rows = connection.execute(single_statement).fetchall()
正则检查只适合这个教学案例,不能可靠处理注释、字符串和复杂 SQL。生产环境应使用与目标方言匹配的 SQL 解析器构建 AST 白名单,同时由数据库执行只读权限、行级权限、扫描配额和超时控制。
8. 实际运行结果
在本机使用上述模型和 300 张模拟表实际运行,得到的 Top-5 如下。相似度按 1 - cosine distance 计算,用于排序,不是正确率或概率。
| 排名 | 候选表 | 相似度 | 是否为本问题必要表 |
|---|---|---|---|
| 1 | dwd_trade_refund_di | 0.6272 | 是,退款流水 |
| 2 | dim_payment_channel | 0.5500 | 是,渠道维表 |
| 3 | dwd_trade_payment_di | 0.5235 | 是,支付流水 |
| 4 | dwd_trade_detail_204 | 0.5043 | 否,模拟的一般明细表 |
| 5 | dwd_trade_detail_258 | 0.5009 | 否,模拟的一般明细表 |
对于这个单一问题,3 张必要表全部进入 Top-5,因此本次 Recall@5=3/3=100%。但候选中仍有 2 张无关表,说明向量检索并不是“精准答案生成器”,后续重排、用户确认和列裁剪仍然有价值。
本次最终组织出的生成提示词为 1316 个字符。必要表通过检索闸门后,审核 SQL 在 SQLite 样例数据上的结果为:
| 支付渠道 | 实收金额 | 退款金额 | 净实收 |
|---|---|---|---|
| 云闪付 | 0 | 40 | -40 |
| 微信支付 | 200 | 30 | 170 |
| 支付宝 | 300 | 30 | 270 |
脚本最后输出:
PASS: 必要表均已召回,SQL 验证与样例结果断言通过
这些结果证明的是:**本次模拟目录中的必要表被成功召回,审核 SQL 能按预期处理支付成功、退款成功、失败状态和跨日期数据。**它不证明模型生成 SQL 的准确率,因为本案例刻意使用了审核后的 SQL 模板。
这个案例演示的是检索链路和验证方法,不代表同一向量模型在任意企业数仓都能得到相同排序。模拟表只有有限的业务描述,真实数仓还存在同义表、重复字段、缺少注释、复杂血缘和多方言问题。
五、从 Demo 到生产,还要补上哪些环节?
1. 不要只评估“SQL 能不能跑”
建议从真实问题中整理一组带标准答案的评测集,分别记录:
| 指标 | 要回答的问题 |
|---|---|
| 业务域路由准确率 | 是否先进入正确的业务域? |
| Table Recall@K | 必要表是否出现在前 K 个候选中? |
| 无关表比例 | 候选中有多少表其实不需要? |
| 列裁剪保留率 | 必要字段、关联键和分区键是否被保留? |
| SQL 执行成功率 | 语法、表和字段是否有效? |
| 非空查询比例 | 是否因状态值或过滤条件错误返回 0 行? |
| 业务结果正确率 | 数值是否与人工标准 SQL 或对账结果一致? |
| P50/P95 延迟 | 检索、生成和验证分别花了多久? |
| 单次正确结果成本 | 包括重试、输出、人工复核后多少钱? |
| 越权检索与执行次数 | 是否出现不可见表或越权 SQL?目标必须为 0 |
**查询成功和返回非空,都不等于业务正确。**最终结果仍需要标准 SQL、样例数据断言或对账流程验证。
2. 向量数据库怎么选?
| 方案 | 更适合的情况 | 需要注意 |
|---|---|---|
| Chroma | 本地实验、小规模原型、快速验证检索文档 | 演示方便,生产需另评估持久化、服务部署和权限体系 |
| PostgreSQL+pgvector | 已使用 PostgreSQL,希望元数据与向量一起管理 | 需要设计索引、备份、更新和查询性能 |
| OpenSearch/Elasticsearch | 同时需要关键词、过滤、聚合和向量检索 | 运维与资源成本不能只按向量库估算 |
| Milvus 等专用向量数据库 | 向量规模大、检索服务独立、需要专门扩展能力 | 仍需与元数据平台、权限系统和语义层集成 |
选型不应该只看最近邻搜索速度,还要看元数据过滤、更新删除、备份恢复、权限、运维和现有技术栈。
3. 推荐落地顺序
- 先选一个业务域和 20—50 张核心表,不要第一天就接入整个数仓;
- 收集真实问题,人工标注必要表、必要字段、业务口径和标准 SQL;
- 对比关键词检索、纯向量检索和混合检索的 Recall@K;
- 再增加表摘要、同义词、血缘、指标定义和已验证 SQL;
- 最后接入生成与执行,并默认只读、限量、可审计、可回滚;
- 建立索引更新机制:表新增、字段变化、权限收回时,检索副本必须同步更新。
很多团队最容易低估的是第 2 步和第 4 步。元数据治理和评测集建设,往往比选择哪一个向量数据库更决定最终效果。
六、总结
几百张表的 Schema 不能简单全塞,并不是因为模型“一个字也读不了”,而是因为全量输入同时放大了三个问题:
- 效率:相关表被大量相似信息淹没,响应和排查更慢;
- 成本:每次请求重复支付无关输入,错误重试还会追加费用;
- 安全:表结构和历史 SQL 的可见范围被不合理扩大。
Uber、Pinterest、Snowflake 和 Databricks 的公开实践提供了同一个启发:业务域分流、候选表检索、列裁剪、语义资产、已验证 SQL 和结果校验,需要组合成完整系统。
向量数据库是其中很有用的一环,但它只负责帮助回答“可能该找哪些表”。真正可靠的数据开发助手,还必须继续回答“这些表是什么意思、用户有没有权限、SQL 是否算对、结果由谁验收”。
参考资料
- Uber Engineering:QueryGPT – Natural Language to SQL Using Generative AI,2024。业务域、选表、列裁剪与评估流程。
- Pinterest Engineering:How we built Text-to-SQL at Pinterest,2024;ZenML 案例摘要。表摘要、历史查询与两段式选表。
- Pinterest GitHub:Querybook,公开的数据查询与元数据项目。
- Snowflake Documentation:Semantic Views、Cortex Analyst Verified Query Repository。
- Databricks Documentation:Tune Genie Agent quality,知识库、字段隐藏、表关系与示例 SQL。
- Chroma Documentation:Manage Collections、Query and Get。
- FastEmbed Documentation:Supported Models,
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2的维度、许可与模型信息。
注:大厂实践依据公开工程文章和官方文档整理,不代表其当前内部系统的完整实现。Pinterest 部分同时列出原始文章链接与二手技术摘要,本文未采用摘要中的效率指标作为结论。案例使用虚构表结构和虚构样例数据;成本数字仅用于说明计算方式,不是实际项目账单。
更多推荐



所有评论(0)