同一句话,“教练甲带过的球员里,谁后来去了示例队乙”,可以写成一条SQL,也可以写成一条图查询。两种写法都能得到答案,区别是:SQL版本要连接四张表,还要小心时间条件写错;图版本几乎就是把问题画成一条路径。而换一句话,“球员A最近10场的场均得分”,SQL只需要一次过滤和一次平均,图查询反而显得笨重。体育数据系统里最实际的问题从来不是“Text-to-SQL和Text-to-Graph谁更先进”,而是“这个问题的形状更像表,还是更像路径”。

先看形状:表适合什么,图适合什么

关系数据库把世界组织成行和列:每场比赛是一行,每个球员每场的技术统计是一行。这种结构对“筛选、排序、分组、聚合”几乎是天然的。图数据库把世界组织成节点和边:球员、球队、教练、比赛是节点,“效力于”“执教”“参加”是边,每条边可以带起止时间。这种结构对“沿着关系走几步”几乎是天然的。

维度Text-to-SQL Text-to-Graph
数据形状 规则的行列表 实体加关系
擅长的问题最近N场均值、排名、分组对比、赛季汇总 教练与球员的关联路径、转会链条、共同经历
难点 字段口径、时间窗口、连接条件关系类型是否建模完整、路径过长导致的爆炸
典型错误 漏了过滤条件、平均口径错、连接后重复计数 把有效期外的关系也走进去、方向写反
结果形态 表格与数值路径、子图、名单

公开研究里,SPORTSQL系统演示把自然语言问题转成可执行SQL,并在带时间索引的数据库上生成表格与可视化;篮球比赛技术统计的本体知识图谱研究则强调用本体显式建模赛季、阶段与日期范围,再让大模型去查询。两类工作代表了两条路线,也说明了它们各自关心的重点不同。

同一个问题,两种写法

先看一个偏统计的问题:“示例队甲本赛季主场平均进球是多少?”SQL写起来很直接:

SELECT AVG(goals_for) AS avg_goals_for,
       COUNT(*)       AS games
FROM   team_games
WHERE  team_id = 'T_JIA'
  AND  season  = '2025-26'
  AND  is_home = 1;

这一次查询就是答案,图查询在这里没有任何优势。再看一个偏关系的问题:“教练甲执教期间效力过示例队甲、后来又加盟示例队乙的球员有哪些?”用图查询的风格描述,可以写成下面这样(简化写法,不针对某个具体图数据库):

MATCH (c:Coach)-[r1:COACHED]->(a:Team),
      (p:Player)-[r2:PLAYED_FOR]->(a),
      (p)-[r3:PLAYED_FOR]->(b:Team)
WHERE c.name = '教练甲'
  AND a.name = '示例队甲'
  AND b.name = '示例队乙'
  AND r2.start <= r1.end AND r2.end >= r1.start
  AND r3.start >= r2.end
RETURN p.name, r2.start, r2.end, r3.start;

这段写法的核心是三条边加两组时间条件:球员在示例队甲效力的区间要与教练执教区间有重叠,加盟示例队乙必须发生在离开示例队甲之后。用SQL也能写,需要教练任期表、球员合同表各连接一次,再对同一张合同表自连接,时间比较的条件与图版本一样多,只是读起来没有路径那么直观。这里要注意的是:写起来更短,并不等于更不容易出错,时间条件写反或漏写,两种查询都会悄悄地给出一批看起来合理的错误名单。关于图里时间区间的保存方式,可以参考时间关系知识图谱的相关说明。

混合问题:先拆开,再合起来

真正麻烦的是既有关系又有统计的问题。例如:“教练甲执教期间,示例队甲的前锋们平均每场进球是多少,后来转去示例队乙的那几位呢?”这句话至少包含两个子问题:

  1. 用图查询找出“教练甲执教期间效力于示例队甲的前锋”,以及其中“后来加盟示例队乙”的子集,得到一份球员编号名单和各自的时间区间;
  2. 把名单与时间区间交给SQL,聚合各人在对应区间内的进球与场次,再分别计算两个人群的平均值。

图负责回答“是谁、在什么时候”,SQL负责回答“那段时间里数据是多少”。两者之间通过实体编号传递,而不是通过名字字符串。半岛体育AI的查询规划环节会先判断问题里有几个“关系词”(执教期间、后来、效力过)和几个“统计词”(平均、场均、最高),再决定拆成几步、每步交给谁。这样的分工与工具调用的思路是一致的:由模型决定顺序,由不同的引擎各自把自己那一段做对。

先图后SQL还是先SQL后图,取决于哪一步更能缩小范围。上面的例子里,“教练甲执教期间”这个条件最有筛选力,所以先走图;如果问题是“本赛季得分最高的十名前锋里,谁曾在教练甲手下效力”,则应先用SQL选出十个人,再用图去核对他们与教练甲的关系。次序选错,不会影响正确性,但会让中间结果变得很大,拖慢整个流程。

怎么判断该走哪条路:几条实用的信号

  • 出现“最近N场”“本赛季”“平均”“排名”“最高”:多半是SQL。
  • 出现“后来”“之前效力过”“共同”“经由”“通过某人”:多半涉及关系路径,可能是图。
  • 同时出现两类信号:拆成混合流程,先图后SQL或先SQL后图,取决于哪一部分能先缩小范围。
  • 问题询问规则、传闻或新闻语境:这两者都不合适,应交给文本检索。

这些信号并不是硬规则,只是查询规划的起点,最终仍要看数据里实际有什么:如果教练任期没有建模成带时间区间的关系,那再擅长关系的图也回答不了“执教期间”。

两种查询各自最容易翻车的地方

Text-to-SQL:口径与连接

最常见的问题不是语法,而是语义:“场均得分”是按总得分除以场次,还是把缺席场次也算作0?把上场时间为0的记录混进来会拉低均值。另一个隐蔽错误是连接之后的重复计数,例如把球员表与多行事件表连接后再做求和,同一场的数据被重复累加。生成SQL的模型往往能写出语法正确的语句,但需要后续的校验去发现这类语义问题。

Text-to-Graph:关系范围与时间

图查询的隐患在于“能走的边太多”:路径长度不受限时,结果会迅速膨胀,甚至走进几乎无关的实体。另一类隐患是忽略边上的有效时间,把已经结束的关系也算进来,导致答案里出现“旧球队的现役球员”这类错误。系统在生成图查询时,应默认限制跳数,并要求涉及“某个时期”的关系必须带上时间条件。

共同的隐患:实体没对上

两种查询都依赖前一步的实体识别。如果“林某”被识别成了另一个同名球员,无论后面的SQL或图查询写得多漂亮,答案都是错的。所以半岛体育AI在生成任何查询前,都会先把人名、队名解析成内部编号。

模型写查询之前,需要先“看到”什么

无论是哪种查询,模型都不是凭空写出来的,它需要一份对数据结构的描述。对SQL来说,是表名、字段名、字段含义与取值范围,例如 is_home 的取值是0或1,minutes 的单位是分钟,season 的写法是“2025-26”;对图来说,是节点类型、关系类型、每种关系带哪些属性、时间属性用的是闭区间还是半开区间。半岛体育大模型在生成查询时会带上这类结构描述,并且只暴露与当前问题相关的一小部分,避免把整个库的字段一股脑塞进上下文,让模型在几百个字段名之间选错。

字段命名也影响正确率。如果同一个概念在不同表里叫 pts、points、score,模型很容易混用;如果关系名叫 REL_1、REL_2,模型根本无从判断哪个是“效力于”。规范的、语义清晰的命名,是让查询生成可靠的前提,这件事发生在建库阶段,而不是模型调优阶段。同样,示例问句与对应查询的配对也很有用:给出“最近N场均值”的标准写法,比空口告诉模型“注意排序和过滤”更有效。

校验:让生成的查询也接受检查

无论是SQL还是图查询,让大模型直接写、直接跑,都存在风险。可行的检查包括:只允许只读语句;用白名单限制可访问的表与关系类型;对生成的查询做静态检查,例如SQL里涉及“最近N场”时是否包含日期排序与上场记录过滤,图查询里涉及时间关系时是否包含区间条件;执行前后对比结果规模,返回0行或行数异常大时触发复核。再配合把查询本身展示给用户或写入追溯记录,答案才能被检验。公开的CricBench类评测把板球分析问题做成多语言Text-to-SQL基准,正说明这类查询的正确性需要被系统地评估,而不是靠感觉。

不选边站,才是体育数据系统的现实

把Text-to-SQL和Text-to-Graph描述成对手,是一种方便的讲法,却不符合数据本身的样子。体育数据里既有规整的、可以按行统计的比赛记录,也有交错的、随时间变化的人与队之间的关系。前者用表,后者用图,混合问题拆开走两步,中间用实体编号衔接。半岛体育的做法是不预先押注某一种形态,而是让查询规划先看清问题的形状,再选择合适的引擎,并用校验把结果拉回到“可复算、可追溯”的轨道上。