资讯详情

表结构检索为什么比文档难:schema 是图不是文本

📅 2026/10/23 11:03:04 | 华诺云谱 👁 阅读
表结构检索为什么比文档难:schema 是图不是文本
版权与内容来源声明本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容均在附表 A 中标注来源引用官方原文保持原样不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式也不对任何收益结果作承诺。转载请注明出处。一、用户一句话库里五十七列1.1 三个词落在三个地方运营同学在群里问了一句「上月华东的毛利是多少。」这句话里能当检索线索的词就三个上月、华东、毛利。我把它对到一个 9 张表、57 列的销售分析库上本机自建脚本与日志见附表 A得到的对应关系是这样用户说的词库里怎么存属于哪类信息文本检索能不能直接对上毛利fact_sales.gross_profit_amt注释「毛利金额单位分」列名 注释能靠「毛利」二字上月fact_sales.stat_month取值为2026-09这样的字符串列名 取值格式只能命中「月」还得先换算成具体月份华东dim_region.region_name这一列里的一个取值行数据不能。表名、列名、注释里都没有「华东」三行看完问题的形状就出来了三个词里只有一个能靠字面对上另外两个一个藏在行取值里一个藏在格式约定里。而「把 schema 拼成一段文本再检索」这种做法只看得到字面。1.2 出错的不是 SQL是选表选列Text2SQL把自然语言问题翻译成 SQL 的那类系统的默认叙事是「生成」把表结构给模型让它写 SQL。但真跑过一阵就会知道语法错反而是最容易发现的一种因为数据库会立刻拒绝执行。难查的是另一种错SQL 语法全对、能跑出结果但表选错了、列选错了数字是另一个东西。你盯着屏幕上的结果集它长得完全正常。所以第一步其实不是生成是选——从几十张表、几百列里挑出这个问题真正要用的那几张表、那几列。选对了生成几乎是顺带的选错了后面每一步都在给错误增加精度。二、文本检索在这里为什么失效把 schema表结构有哪些表、每张表有哪些列、列与列之间什么关系拼成一段文字做向量检索是第一反应。它在文档上有效因为文档的语义本来就写在文字里。schema 不是。2.1 失效点一列名在自然语言里没有共现共现指两个词在同一段话里一起出现过。用户说「毛利」列叫gross_profit_amt说「上月」列叫stat_month说「华东」压根没有这一列。这几组词在日常语料里从不一起出现所以它们的向量也不会有任何关系。用户说法库内写法需要靠什么才能对上毛利gross_profit_amt注释毛利金额单位分语义 别名表上月stat_month 2026-09时间换算规则 格式约定华东dim_region.region_name的取值华东取值索引把行数据也进检索品类dim_category.category_name在另一张表里外键路径这张表说明的是需要的信息分散在「列名、行取值、外键路径」三处而文本检索只看得见第一处的字面。2.2 失效点二必须一起出现的表靠外键连外键foreign key是一条声明这张表某列的取值必须来自另一张表的某一列。SQLite 官方文档的原话是外键约束用来强制表之间的「存在」关系enforce “exists” relationships between tables并把这边的表叫子表、被引用的叫父表。问题「各品类的退款金额排名」需要四张表。这四张里两张是维表存「是什么」商品、品类两张是事实表存「发生了什么」订单、退款表用户提到了吗为什么必须进来fact_refund提到「退款」退款金额在这里fact_sales没提品类挂在商品上商品挂在订单上dim_product没提桥接product_code→category_codedim_category提到「品类」品类名称在这里按文本相似度排序dim_product会排在最后因为它的表名、列名和问题没有一个共同词。这不是巧合桥接表的存在理由就是两边都没有共同词。如果它和两边都同名建模的人根本不会把它单独拆出来。2.3 失效点三判别性信息在约束里不在名字里真正决定「选哪一列」的信息大部分不是文字是约束。SQLite 的 CREATE TABLE 文档把可加的约束列得很清楚UNIQUE、NOT NULL、CHECK、FOREIGN KEY。PostgreSQL 文档还补了一句关键的外键必须引用一个主键、唯一约束或唯一索引覆盖的列。这句话的含义比它看起来重「这两张表能连」这件事是靠约束声明出来的不是靠名字像。约束是结构不是文本。还有个更容易被忽略的细节SQLite 文档写明 CHECK 约束一条限定列取值必须满足某个条件的规则只在写入时校验、读的时候不校验。这意味着你从数据里抽样看到的取值未必是全部合法取值——枚举某列的值被限定为有限的几个固定值比如订单状态只能是三种之一得从约束定义或字典表里拿不能靠SELECT DISTINCT猜。把这三节合起来schema 里最容易在「拼成文本」这一步丢掉的东西有五类主键、外键、枚举取值、单位、时间粒度。它们全是结构信息。三、schema 是张图两跳邻域怎么算3.1 把表读成点和边如果 schema 是图——表是点、外键是边——那「选表」就变成一个在图上游走的问题而不是在文本里比相似度。fact_sales24 列本库的枢纽 ├─ stat_month → dim_date 1 跳 ├─ product_code → dim_product → dim_category 1 跳 / 2 跳 ├─ region_code → dim_region → dim_region.parent_code自引用2 跳 ├─ channel_code → dim_channel 1 跳 ├─ emp_code → dim_employee → dim_region 1 跳 / 2 跳 └─ order_id ← fact_refund → dim_refund_reason1 跳 / 2 跳从fact_sales出发一跳能碰到 6 张表两跳能碰到 8 张——全库 9 张表里除自己以外全部可达本机实测。这提醒一件事两跳不能无脑用从事实表出发两跳几乎等于全库必须配合裁剪。关系怎么读出来不靠人肉看 DDL靠元数据。SQLite 里PRAGMA foreign_key_list(表名)返回这张表声明了哪些外键。这里有个坑值得记住SQLite 的外键约束默认是关闭的文档原文说 disabled by default且必须每个连接单独开启PRAGMA foreign_keys ON。我在本机验证了这个状态默认连接下PRAGMA foreign_keys的值是0而我在父表只有2026-09的情况下插了一条stat_month 202609的记录照样插进去了。但PRAGMA foreign_key_list依然读得出那条外键声明。**约束声明是元数据跟它是否被强制无关。**这一点对选表很关键你读的是声明不是校验结果。所以图能建起来哪怕这个库平时并不强制外键。3.2 两跳邻域一条能跑的 SQL下面这条查询从fact_refund出发一跳取它直接连的父表两跳再顺着父表往外走一层。它不写死任何表名全部从元数据里读WITHfkAS(SELECTm.nameASchild,fk.tableASparentFROMsqlite_masterASm,pragma_foreign_key_list(m.name)ASfkWHEREm.typetable)SELECTDISTINCTparent,1AShopFROMfkWHEREchildfact_refundUNIONSELECTDISTINCTf2.parent,2FROMfkASf1JOINfkASf2ONf1.parentf2.childWHEREf1.childfact_refundORDERBYhop,parent本机输出是 7 行一跳拿到fact_sales和dim_refund_reason两跳拿到dim_channel、dim_date、dim_employee、dim_product、dim_region。也就是说从一张退款表出发两跳就摸到了整库大半。这里要说明一句pragma_foreign_key_list是 SQLite 特有的写法。PostgreSQL 里对应的是information_schema.table_constraints与key_column_usage思路一样——先读出边再在边上走。3.3 实测图解决「漏」不解决「排序」我拿 6 个中文问题人工标注了每个问题最少需要哪几张表1.1 那句「上月华东毛利」是其中之一比了三种策略k取该问题标注的表数也就是排序后取前 k 个策略候选集里覆盖到标注表top-k 命中A 纯文本表名 列名 注释13/1512/15B 在 A 的种子上做外键两跳闭包15/1512/15C 同 B 的候选集但打分时把维表取值算进去15/1513/15三个读法第一B 把「标注表是否进入候选集」从 13/15 提到了 15/15。这才是两跳邻域真正的贡献——保证不漏。1.1 那个问题里纯文本的候选集只有fact_sales一张dim_region根本不在里面后面无论怎么排都排不出来。第二B 的 top-k 命中还是 12/15一点没涨。候选集变全了排名没变好。「不漏」和「排对」是两个问题用同一个文本得分兼做两件事必然有一头是瘸的。第三C 把维表取值也纳入打分涨到 13/15。涨得有限原因很实在k取的是「标注表数」这个事后才知道的数字。真实系统里没人告诉你答案需要几张表这恰恰是下面那条判据要解决的问题。四、裁剪清单该带哪些列4.1 一张能照着做的表选完表还要选列。下面这张表是我按「漏了会出什么错」倒推出来的可以直接当 checklist 用信息类别该带什么为什么非它不可漏了会怎样主键每张表的主键列名唯一标识一行join 与去重都靠它一对多 join 之后行数翻倍汇总偏大外键列名 指向哪张表的哪一列唯一的连接线索桥接表选不中SQL 缺一张表枚举取值CHECK 或字典表里的合法取值中文含义一并带上把「已支付」映射成PAID过滤条件写不出或写成一个不存在的值时间粒度字段格式 是日/月/季 是否已汇总「上月」要换成具体值要么报类型错要么静默命中 0 行单位金额是分还是元比率是小数还是百分数决定要不要除 100结果差 100 倍且看不出来注释一句话说明这列是什么唯一能弥合命名差异的文本模型只能靠猜列名关于k的判据我给一句能直接用的先问「这条 SQL 至少要几张表」再问「哪张表最像」。前者是数路径目标表 → 外键 → 维表路径上有几个点就是几张表。后者只能给你一个起点。把两跳邻域当成候选集的构造方式k由路径长度决定这比固定取 top-3 靠谱得多。4.2 裁剪的粒度57 列里带 3.8 列本机这个库里fact_sales一张表 24 列全库 57 列。6 个问题人工标注最少需要的列数分别是 4、5、4、5、2、3平均 3.8 列。把全库塞进提示词等于把 57 列摊在一个只有 3.8 列需求的问题上按问题裁剪量级差着一个数量级。列越多无关列对注意力的摊薄越明显——这不是玄学是每多一列就多一份「看起来也能用」的干扰。裁剪流程大致是三步值得写成代码而不是靠提示词自觉⚠️代码待验证defpick(question,graph,k):seedsretrieve(question,graph,top_n3)# 表名/列名/注释/取值一起检索candtwo_hop_closure(seeds,graph)# 外键闭包只负责不漏candrank(cand,question,use_valuesTrue)# 排序用取值与约束信息tablescand[:k]# k 由至少几张表估出来return[prune_columns(t,question)fortintables]五、生成前的四个自检前四章讲的是「怎么选」这一章讲「选完怎么验」。这四个自检不需要模型参与用 SQL 就能查。自检项具体查什么不通过怎么办列名对不对每个用到的列是否真在这张表里回上一步补表不要改列名硬凑枚举映射上没上问题里的中文说法能否对上库里的码值把映射表显式写进上下文单位与粒度金额单位、时间格式是否与字段一致写进上下文或改用视图统一聚合口径分母是订单数还是行数找业务确认别自己定5.1 枚举中文和码值之间没有字面桥梁SELECTDISTINCTstatusFROMfact_salesORDERBY1;本机返回三个值PAID、REFUND、UNPAID。而用户问的是「已支付和已退款订单分别有多少」。我算了一下「已支付/已退款」这几个字与三个码值的字符二元组交集是空集——字面上一点线索都没有。也就是说如果上下文里只有列名和一句「订单状态取值 PAID/UNPAID/REFUND」模型得靠英文常识去猜PAID对应「已支付」。它大概率猜对但这是运气不是机制。把PAID已支付、REFUND已退款、UNPAID未支付明写进去这一步才从猜变成查。5.2 粒度同一个九月被切成两组我在同一张表里放了两条记录一条stat_month 2026-09一条stat_month 202609毛利都是 2345。按月份汇总之后stat_monthSUM(gross_profit_amt)2026-0923452026092345同一个九月两行。SQL 没错语法没错GROUP BY也没错——错在字段里有两种格式而这个信息不在列名里只在数据里。金额同理SUM(gross_profit_amt)出来是 4690。按分读是 46.9 元按元读是 4690 元差 100 倍。这两类错误有个共同特征SQL 语法全对、结果全错而且不会报任何错。所以它们必须在生成前就被写进上下文而不是生成后再靠人眼发现。5.3 聚合口径客单价的分母聚合口径指汇总时分子和分母各算什么。这个定义不在表结构里只在业务规则里。「哪个渠道的客单价最高」这句话里客单价 净额合计 ÷ 订单数。但fact_sales一行是一个订单行一单多商品就是多行。用行数做分母客单价会被系统性拉低而且各渠道的商品组合不同被拉低的幅度也不同——排名可能整个反过来。口径不属于 schema它属于业务定义。这类东西没法从表结构里推出来只能问人。这也是第六章那条「什么时候不要用」的由来。这份资料里的 schema 与建表练习本文的迷你库和 6 个问题的标注我是自己搭的配套资料包里有一份从零建库到写查询的练习清单可以和本文的裁剪表对着做。放在资料包里扫码即可获取六、什么时候不要用 Text2SQL选表选列做得再好也只解决「能选中」的问题解决不了「这个问题本身没有唯一答案」。以下情形我建议直接停手转人工确认情形判断依据该怎么做join 超过三张数路径上的表含两跳桥接让人确认或先建宽视图需要窗口函数排名、同比、累计、同一主体取最后一条预置模板别指望一次生成口径有歧义毛利含不含运费税、退款算不算冲减先定口径再写 SQL涉及行级权限不同角色能看的数据范围不同在视图或查询层拦不交给模型是写操作INSERT / UPDATE / DELETE生成端只读写操作走审批6.1 判据要提前定不要事后补这张表里的每一条都是在问题进来之前就该定好的闸门。等 SQL 生成完、结果算出来了再判断「这个口径对不对」成本高得多你得先看出数字不对而 5.2 那两个坑恰好就是看不出来的那种。一个实用做法是给问题加一个前置分类命中上表任一条就走「先确认、再生成」的通道没命中才进自动生成。分类器不需要模型规则就够。6.2 停手不等于回到手工「不要用」不等于「让人从头写 SQL」。更实际的做法是把选择权收窄把歧义变成两三个选项让人点一下把常见口径写成视图或指标定义让生成环节只面对已经定义好的口径。生成的自由度降下来可控性才上去。七、把「选表选列」当成独立的检索问题7.1 召回和排序要分开看回到 3.3 那张实测表。三个策略的数字摆在一起结论其实很清楚召回该在候选集里的表在不在靠图——两跳闭包把 13/15 提到 15/15排序在候选集里能不能排进前 k靠取值与约束——把维表取值纳入打分工才从 12/15 走到 13/15。这两件事塞进一个向量检索里做一定有一头是瘸的。工程上更省事的做法是拆开图负责「不漏」取值与约束负责「排对」最后按「至少几张表」定 k。7.2 一个能落地的顺序先建关系图。SQLite 用pragma_foreign_key_listPostgreSQL 用information_schemaMySQL 用KEY_COLUMN_USAGE。这一步不依赖任何模型。把枚举和单位进索引。取值不是噪声它是把用户的话映射到过滤条件的唯一桥梁。用「至少几张表」定 k再按问题裁剪列。别把全库塞进提示词。生成后回读校验。列名、枚举、单位、粒度四项过一遍再交付。7.3 一句话收尾文档检索的难点在「找到讲这件事的那一段」表结构检索的难点在「找到必须一起出现的那几张表」。前者是文本问题后者是图问题。把 schema 当文本处理丢掉的恰好是它最值钱的那部分——约束、关系、取值。这也是为什么我建议把「选表选列」当成一个独立的检索问题来做而不是生成步骤的附属动作。这篇里的迷你库和图遍历脚本9 张表、57 列、10 条外键的建表语句以及两跳邻域那几条查询我都放进了资料包可以自己在本地跑一遍改改看。放在资料包里扫码即可获取附表 A本文引用事实与出处对照表事实出处本文位置外键约束用于强制表之间的「存在」关系子表/父表、子键/父键的定义SQLite 官方文档《SQLite Foreign Key Support》第 1 节sqlite.org/foreignkeys.html2.2外键约束默认关闭disabled by default需每个连接单独PRAGMA foreign_keys ON同上第 2 节「Enabling Foreign Key Support」3.1CREATE TABLE 支持 UNIQUE、NOT NULL、CHECK、FOREIGN KEY 约束SQLite 官方文档《CREATE TABLE》sqlite.org/lang_createtable.html2.3CHECK 约束只在写入时校验查询时不校验同上第 3.7 节「CHECK constraints」2.3外键必须引用主键、唯一约束或唯一索引覆盖的列PostgreSQL 官方文档 18《5.5. Constraints》5.5.5 Foreign Keyspostgresql.org/docs/current/ddl-constraints.html2.3主键定义了外键引用该表时的默认目标列同上5.5.4 Primary Keys2.39 张表 / 57 列 / 10 条外键边fact_sales24 列一跳 6 张表、两跳 8 张表本机实测脚本work/2026-10-22-10_表结构实测.py日志同名.log3.1默认连接下PRAGMA foreign_keys值为 0202609仍可插入父表只有2026-09的字段本机实测同上脚本[fk]段3.1两跳邻域查出 7 张表一跳 2 张、两跳 5 张本机实测同上脚本[demo]段3.2三策略对照候选集覆盖 13/15 → 15/15top-k 命中 12/15、12/15、13/15本机实测同上脚本[s1]、[s2]段6 个问题人工标注k 取标注表数3.3全库 57 列6 个问题标注列数 4/5/4/5/2/3平均 3.8本机实测同上脚本[s3]段4.2status取值为 PAID/REFUND/UNPAID与「已支付/已退款」的字符二元组交集为空集本机实测同上脚本[enum]段5.1同一月份2026-09与202609在GROUP BY后被分成两组各 2345SUM为 4690分本机实测同上脚本[demo]段5.2运行环境Python 3.13.14、sqlite3 3.53.1本机实测同上脚本运行环境全文SQL 标准中存在对约束的规范定义本文未引用具体年份版本未核到可靠一手文本待验证2.3附表 B术语速查表术语一句话解释在本文哪里用到schema一个库的表结构有哪些表、每张表有哪些列、列之间什么关系全文主键能唯一标识一行的那一列或几列组合4.1外键一条声明说这列的值必须来自另一张表的某列2.2桥接表只用来把两张表连起来的中间表自身几乎没有业务字段2.2维表 / 事实表维表存「是什么」区域、商品、渠道事实表存「发生了什么」订单、退款2.2枚举取值某列被限定为有限的几个值如PAID/REFUND2.3两跳邻域从一张表出发沿外键走两步能碰到的所有表3.1召回该在候选集里的东西有没有进候选集3.3top-k 命中候选集排序后取前 k 个里面有几个是对的3.3聚合口径汇总时分子分母各算什么如客单价的分母是订单数还是行数5.3时间粒度时间字段精确到日、月还是季是否已经汇总过4.1写在最后这篇用到的资料写这篇文章时把相关的官方文档和源码又翻了一遍顺手也整理了几份配套的东西大模型学习路线图从零基础到能自己动手做 Agent按阶段说明每一步该学什么、哪些可以先跳过《LangChain LangGraph MCP 智能体开发实战》视频课7 个模块从私有化部署、EmbeddingRAG 到 MCPAgent 全流程AI 大模型知识库在线可查Agent Skills 从入门到落地、Claude Skills 完全指南等专题按目录浏览即可640 套 AI 大模型行业报告 经典 PDF 书籍看行业落地案例和别人怎么做的时候用得上大模型零基础到精通教学视频跟着敲一遍比只读文档快得多资料是我自己整理的放在下面这个码上扫码即可获取添加时备注「AI」优先通过。资料按「先路线、再动手、最后查漏」的顺序整理好了建议先看学习路线那一份照着它挑一条适合自己当前基础的路径再往下看。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑