一个企业查询问题,如何从自然语言走到 DSL 再走到 SQL

前两篇讲了两件事。

第一,企业查询不适合把最终 SQL 作为自然语言生成的直接结果。更容易控制的方式是:

text 复制代码
Text -> DSL -> SQL

第二,生成 DSL 时,上下文不能只有原始数据库 schema,还需要整理过的语义查询说明。

这一篇用一个具体问题,把这个过程串起来。

用户先提出一个业务问题

假设业务人员问:

text 复制代码
统计本月各客户的已完成订单金额,按金额从高到低排前 20 个。

这个问题看起来很普通。

如果是开发人员手写 SQL,大概也不难。

但如果要让系统根据这句话生成查询,就不能只看文本本身。

系统至少要知道:

  • 当前有没有订单查询模型。
  • "客户"用哪个字段表达。
  • "本月"按创建时间还是完成时间。
  • "已完成"对应哪个状态值。
  • "订单金额"是不是可以做 sum
  • 汇总查询是否允许按客户分组。
  • 排序能不能引用聚合别名。

这些信息不能靠临场判断,而要来自语义查询说明。

先准备一份查询说明

上下文可以是类似这样的内容:

text 复制代码
OrderQueryModel:订单查询模型

适合回答:
- 订单列表查询
- 按客户、状态、时间统计订单金额

字段:
- customer$caption:客户名称,可返回、可分组、可排序。
- orderStatus:订单状态,可筛选。已完成使用 COMPLETED。
- completedAt:订单完成时间,可筛选、可排序。本月统计默认使用该字段。
- orderAmount:订单金额,可返回、可聚合,常用 sum(orderAmount) as totalAmount。

规则:
- 明细查询使用 columns。
- 汇总查询中,非聚合字段必须放入 groupBy。
- 聚合表达式需要设置别名。
- orderBy 可以引用聚合别名。
- 默认不要返回超过 50 行。

这段说明不解释底层表怎么 JOIN,也不把所有数据库字段都暴露出去。

它只说明当前查询入口能做什么,以及 DSL 要按什么规则写。

先得到 DSL,不直接得到 SQL

基于这份说明,用户问题可以先转换成 DSL:

json 复制代码
{
  "columns": [
    "customer$caption",
    "sum(orderAmount) as totalAmount"
  ],
  "slice": [
    {
      "field": "orderStatus",
      "op": "=",
      "value": "COMPLETED"
    },
    {
      "field": "completedAt",
      "op": "[]",
      "value": ["2026-06-01", "2026-06-30"]
    }
  ],
  "groupBy": [
    "customer$caption"
  ],
  "orderBy": [
    "-totalAmount"
  ],
  "limit": 20
}

customer$caption 出现在 columns,也出现在 groupBy

这是因为它是汇总结果里的非聚合字段。

sum(orderAmount) as totalAmount 表达的是聚合指标。

orderBy 使用 -totalAmount,表示按聚合结果倒序。

时间范围已经从"本月"展开成了明确的起止日期。

这里得到的是结构化查询意图,不是最终 SQL。

查询引擎接手检查

DSL 生成以后,查询引擎要先检查。

例如:

  • customer$caption 是否存在。
  • customer$caption 是否允许返回和分组。
  • orderStatus 是否允许筛选。
  • COMPLETED 是否是可识别的状态值。
  • completedAt 是否支持区间查询。
  • orderAmount 是否允许聚合。
  • totalAmount 是否和已有字段重名。
  • groupBy 是否覆盖了所有非聚合字段。
  • orderBy 引用的别名是否来自当前查询。

这些检查听起来琐碎,但它们很重要。

因为这一步能把很多错误拦在 SQL 生成之前。

如果字段写成了 customerName,系统可以提示字段不存在。

如果忘了 groupBy,系统可以指出汇总查询结构不完整。

如果把 sum(orderAmount) 的别名写成已有字段名,系统也能提前拒绝。

错误提示越明确,后续人工排查和自动修正都更容易。

SQL 由系统生成

DSL 通过检查以后,查询引擎再根据 TM/QM、字段映射和数据库适配规则生成 SQL。

最终 SQL 可能类似这样:

sql 复制代码
select
  c.name as customer_caption,
  sum(o.order_amount) as total_amount
from biz_order o
join biz_customer c on c.id = o.customer_id
where o.status = ?
  and o.completed_at >= ?
  and o.completed_at <= ?
group by c.name
order by total_amount desc
limit 20

参数是:

text 复制代码
COMPLETED
2026-06-01
2026-06-30

这段 SQL 是系统生成的。

这部分不需要放到上游生成结果里。

biz_orderbiz_customer 怎么关联,当前数据库怎么分页,日期和字段别名怎么处理,都放在查询引擎里处理。

结果返回也要保留语义

数据库执行以后,结果可能是:

json 复制代码
[
  {
    "customer$caption": "华东渠道客户",
    "totalAmount": 186320.50
  },
  {
    "customer$caption": "华南直营客户",
    "totalAmount": 142008.00
  }
]

这时返回给用户的,不能只是数据库行。

系统最好还能保留一些查询语义:

  • 使用的是 OrderQueryModel
  • 时间字段是 completedAt
  • 订单状态筛选为 COMPLETED
  • 指标是 sum(orderAmount) as totalAmount
  • 排序是按 totalAmount 倒序。
  • 最多返回 20 行。

这样,用户看到结果时,才知道这次统计的口径是什么。

如果结果被质疑,开发人员也能回看:用户问题是什么,中间 DSL 是什么,系统最终执行了什么 SQL。

为什么要这样拆

从自然语言到 SQL,中间多了一层 DSL,看起来是多了一步,但这一步把分工拆清楚了。

用户问题保留原始意图。

语义查询说明描述当前能查什么。

DSL 承载结构化查询意图。

查询引擎负责校验、改写和生成 SQL。

数据库只负责执行最终查询。

这样,上游不需要理解整个数据库,后端也不用把任意 SQL 当成业务查询入口。

当问题变复杂时,系统还可以继续扩展:增加更多查询模型、增加指标规则、增加错误提示、增加审计记录、增加人工确认流程。

但基本流程不变:

text 复制代码
自然语言问题
  -> 语义查询说明
  -> 受控 DSL
  -> 查询引擎校验
  -> SQL
  -> 结果与口径说明

到这里,"从动态 SQL 到语义查询"这条线基本就走完了一段。

它不是从一开始就奔着自然语言查询去的。

最早只是为了解决 Java 项目里动态 SQL 越写越乱的问题。

后来有了模板、helper、权限注入、DSL、TM/QM、聚合、时间窗口、层级、pivot、错误提示和查询引擎。

再往后,自然语言只是多了一个新的入口。

关键仍然是这套查询能力本身是否清楚、受控、可检查、可复核。

下一阶段如果继续写,就不只讲查询引擎本身了。

更适合进入一个新的方向:

企业 AI 分析项目到底应该怎么落地。

相关推荐
浮游本尊1 天前
Java学习第41天 - 复杂查询、多表关联、索引优化与慢 SQL 调优
后端
llz_1121 天前
web-第五次课后作业
前端·后端·http
雨辰AI1 天前
生产级实测:SpringBoot3 + 达梦数据库接口从 200ms 优化至 20ms 完整调优指南
java·数据库·spring boot·后端·政务
Solis1 天前
Raft:分布式系统的定海神针
后端·架构
程序员老申1 天前
第三篇 5 天 12 个 commit:踩坑实录与代码演进
后端·程序员
程序员鱼皮1 天前
提示词工程已死,Loop Engineering 称王!保姆级教程 + 项目实战
前端·后端·ai编程
Mininglamp_27181 天前
Vibe Coding 之后是 Vibe Operating?
后端·开源·多智能体·ai agent·mano-p
星哥的编程之路1 天前
别再调 API 就说自己会 RAG 了,看看真正的企业级 AI 智能体长什么样
后端·面试
长大19881 天前
C++26 静态反射完整实战:告别宏代码生成,一键实现序列化
后端
yb7791 天前
Java 21 虚拟线程最佳实践:虚拟线程如何让高并发 Java 服务更轻更快
后端