几百张表的 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 token24M token$48
检索后只放入 5 张5 × 800=4K token0.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. 在线:检索、重排、裁列、生成、验证

推荐流程如下:

  1. 问题补全与意图识别:识别时间、指标、维度、业务域和 SQL 方言;
  2. 权限过滤:先限定租户、业务域、密级和可用表集合;
  3. 混合检索:关键词检索处理准确表名和字段名,向量检索处理“实收”“收款”等自然语言表达;
  4. 重排:结合相似度、核心表等级、血缘、使用热度和历史反馈选出 Top K;
  5. 列裁剪:只保留当前问题需要的字段、关联键、分区键和状态字段;
  6. 生成 SQL:同时提供必要的指标口径、JOIN 关系和已验证示例;
  7. 静态检查与试算:检查表字段是否存在、是否只读、是否有分区过滤,再执行 EXPLAIN 或小样例验证;
  8. 受控执行:只读账号、扫描量和超时限制,高风险查询转人工审批。

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 计算,用于排序,不是正确率或概率。

排名候选表相似度是否为本问题必要表
1dwd_trade_refund_di0.6272是,退款流水
2dim_payment_channel0.5500是,渠道维表
3dwd_trade_payment_di0.5235是,支付流水
4dwd_trade_detail_2040.5043否,模拟的一般明细表
5dwd_trade_detail_2580.5009否,模拟的一般明细表

对于这个单一问题,3 张必要表全部进入 Top-5,因此本次 Recall@5=3/3=100%。但候选中仍有 2 张无关表,说明向量检索并不是“精准答案生成器”,后续重排、用户确认和列裁剪仍然有价值。

本次最终组织出的生成提示词为 1316 个字符。必要表通过检索闸门后,审核 SQL 在 SQLite 样例数据上的结果为:

支付渠道实收金额退款金额净实收
云闪付040-40
微信支付20030170
支付宝30030270

脚本最后输出:

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. 推荐落地顺序

  1. 先选一个业务域和 20—50 张核心表,不要第一天就接入整个数仓;
  2. 收集真实问题,人工标注必要表、必要字段、业务口径和标准 SQL;
  3. 对比关键词检索、纯向量检索和混合检索的 Recall@K;
  4. 再增加表摘要、同义词、血缘、指标定义和已验证 SQL;
  5. 最后接入生成与执行,并默认只读、限量、可审计、可回滚;
  6. 建立索引更新机制:表新增、字段变化、权限收回时,检索副本必须同步更新。

很多团队最容易低估的是第 2 步和第 4 步。元数据治理和评测集建设,往往比选择哪一个向量数据库更决定最终效果。

六、总结

几百张表的 Schema 不能简单全塞,并不是因为模型“一个字也读不了”,而是因为全量输入同时放大了三个问题:

  • 效率:相关表被大量相似信息淹没,响应和排查更慢;
  • 成本:每次请求重复支付无关输入,错误重试还会追加费用;
  • 安全:表结构和历史 SQL 的可见范围被不合理扩大。

Uber、Pinterest、Snowflake 和 Databricks 的公开实践提供了同一个启发:业务域分流、候选表检索、列裁剪、语义资产、已验证 SQL 和结果校验,需要组合成完整系统。

向量数据库是其中很有用的一环,但它只负责帮助回答“可能该找哪些表”。真正可靠的数据开发助手,还必须继续回答“这些表是什么意思、用户有没有权限、SQL 是否算对、结果由谁验收”。


参考资料

  1. Uber Engineering:QueryGPT – Natural Language to SQL Using Generative AI,2024。业务域、选表、列裁剪与评估流程。
  2. Pinterest Engineering:How we built Text-to-SQL at Pinterest,2024;ZenML 案例摘要。表摘要、历史查询与两段式选表。
  3. Pinterest GitHub:Querybook,公开的数据查询与元数据项目。
  4. Snowflake Documentation:Semantic Views、Cortex Analyst Verified Query Repository。
  5. Databricks Documentation:Tune Genie Agent quality,知识库、字段隐藏、表关系与示例 SQL。
  6. Chroma Documentation:Manage Collections、Query and Get。
  7. FastEmbed Documentation:Supported Models,sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2 的维度、许可与模型信息。

注:大厂实践依据公开工程文章和官方文档整理,不代表其当前内部系统的完整实现。Pinterest 部分同时列出原始文章链接与二手技术摘要,本文未采用摘要中的效率指标作为结论。案例使用虚构表结构和虚构样例数据;成本数字仅用于说明计算方式,不是实际项目账单。

Logo

更多推荐