Logicform 基础结构

Logicform 采用 JSON 结构描述一条查询请求。接口定义可在 semanticdb-core/src/logicform/logicform.ts 中找到,核心内容如下:

export interface LogicformType {
  schema: string;
  query?: QueryType;
  preds?: PredItemType[];
  groupby?: GroupbyItemType[];
  sort?: { [key: string]: 1 | -1 };
  having?: QueryType;
  limit?: number;
  limitBy?: number;
  skip?: number;
  representation?: RepresentationType;
  entity_id?: string;
  from?: LogicformType;
  currency?: string;
}

其中 PredItemTypeGroupbyItemTypeQueryType 等结构都会在后续章节单独说明。

字段与 SQL 的对应

字段必填说明SQL 对应
schema当前 Logicform 的语义上下文。在简单场景下它也直接对应底层数据来源。FROM table
from以一个子 Logicform 作为当前查询的数据来源,用于表达 derived table / 子查询来源。FROM (subquery)
preds需要返回的指标或表达式,通常包含聚合函数。SELECT
query结果集的过滤条件,遵循 MongoDB 风格语法。若当前节点带 from,则过滤的是子查询结果。WHERE
groupby维度分组,可包含时间/地域层级设置。GROUP BY
sort结果排序,键为字段别名,值为 1/-1ORDER BY
having对聚合结果进行二次过滤。字段名基于结果列别名。HAVING
limit / skip结果集分页,默认 limit=20LIMIT / OFFSET
limitBy按组限制每组返回条数,常与 groupby 一起使用。LIMIT BY / per-group limit
representation结果展示方式,影响前端渲染策略,不改变查询语义。展示层
entity_id表示某个实体位点已经被识别为一个具体实体,而不是同名实体的聚合结果。值通常为该实体在实体表中的唯一 ID。实体唯一性上下文
currency查询使用的货币上下文。未显式指定时默认 CNY计算 / 展示上下文

from 子查询

from 用于表达“当前查询基于一个子 Logicform 的结果继续查询”。它和 query 中的子查询区别在于:

场景含义
from子 Logicform 是当前节点的整体数据来源
query 中的子 Logicform子 Logicform 只是某个筛选条件的右值

示例:

{
  "schema": "sales",
  "from": {
    "schema": "sales",
    "query": { "日期": "YTD" },
    "groupby": [{ "_id": "地区", "name": "地区" }],
    "preds": [
      { "name": "销售额", "operator": "$sum", "pred": "amount" }
    ],
    "having": {
      "销售额": { "$gt": 1000000 }
    }
  },
  "preds": [
    {
      "name": "高销售额地区数",
      "operator": "$count"
    }
  ]
}

这个结构表达的是:先在内层得到“年初至今销售额超过 100 万的地区”这个中间结果,再由外层对这些结果做计数。

如果用 SQL 来类比,可以理解为:

SELECT COUNT(0) AS 高销售额地区数
FROM (
  SELECT 地区, SUM(amount) AS 销售额
  FROM sales
  WHERE 日期 ...
  GROUP BY 地区
  HAVING SUM(amount) > 1000000
) t;

另一个常见理解方式是:from 适合表达“先筛一层中间表,再对中间表继续做聚合、排序或分页”,而不是只把普通 WHERE 条件搬到外层。

Having 子句

having 用于对聚合后的结果进行过滤,与 SQL 的 HAVING 子句对应。它与 query 的主要区别在于:

特性queryhaving
作用时机聚合前过滤聚合后过滤
字段引用原始表字段preds/groupby 的别名
对应 SQLWHEREHAVING
能否用聚合函数不能(需子查询)可以

基本示例

筛选销售额超过 100 万的地区:

{
  "schema": "sales",
  "groupby": [{ "_id": "地区", "name": "region" }],
  "preds": [
    { "name": "销售额", "operator": "$sum", "pred": "amount" }
  ],
  "having": {
    "销售额": { "$gt": 1000000 }
  }
}

对应的 SQL:

SELECT 地区, SUM(amount) AS 销售额
FROM sales
GROUP BY 地区
HAVING SUM(amount) > 1000000;

复杂条件

having 支持与 query 相同的操作符:

{
  "having": {
    "销售额": { "$gte": 500000, "$lte": 2000000 },
    "订单数": { "$gt": 100 }
  }
}

与 query 的组合

{
  "schema": "sales",
  "query": { "日期": { "year": 2024 } },
  "groupby": [{ "_id": "地区" }],
  "preds": [{ "name": "销售额", "operator": "$sum", "pred": "amount" }],
  "having": { "销售额": { "$gt": 1000000 } }
}

执行顺序:

  1. query 过滤 2024 年的数据
  2. 地区 分组并计算销售额
  3. having 过滤销售额 > 100 万的地区

跨表字段路径

在 Logicform 中,query 的 key、groupby._idpred.pred 等位置都可以使用带下划线的字段路径,例如 产品_品类门店_地理位置物料_品牌_品类

这里的 _ 不是普通字符串拼接,而是表示沿着 schema 的引用链逐级跨表取字段。可以把它理解为:

  • 门店_地理位置:先从当前 schema 关联到 门店,再从 门店 关联到 地理位置
  • 物料_品牌_品类:先到 物料,再到 品牌,最后取 品类

示例:

{
  "schema": "sales",
  "query": {
    "门店_地理位置": {
      "query": {
        "名称": "华东"
      }
    },
    "物料_品牌_品类": "酸奶"
  },
  "groupby": [
    { "_id": "门店_重点客户系统", "name": "重点客户系统" },
    { "_id": "物料_品牌_品类", "name": "品类" }
  ],
  "preds": [
    { "name": "销售额", "operator": "$sum", "pred": "amount" },
    { "name": "门店数", "operator": "$uniq", "pred": "门店_ID" }
  ]
}

这类路径是否能解析成功,取决于语义层中是否已经配置好对应 schema 之间的引用关系。若引用链存在,执行器会根据该链路自动生成 JOIN。

推荐理解规则:

  • query 中用 _:表示按跨表字段筛选
  • groupby._id 中用 _:表示按跨表维度分组
  • pred.pred 中用 _:表示引用跨表字段做指标计算

如果某条路径在语义层里不存在对应引用关系,即使字段名写对了,也无法正确执行。

实体唯一性

entity_id 用来表达“某个实体位点已经指向一个具体实体”。它的核心作用是保证实体唯一性,避免把同名实体错误地聚合在一起。

例如,用户提问“张三今年业绩”。“业绩”通常来自事件表,而 entity_id 则应出现在实体 schema 对应的子查询里,而不是直接挂在事件表根节点上。

{
  "schema": "employee_performance",
  "query": {
    "员工": {
      "schema": "employee",
      "query": {
        "姓名": "张三"
      },
      "entity_id": "E001"
    }
  },
  "preds": [
    { "name": "业绩", "operator": "$sum", "pred": "amount" }
  ]
}

这里的 entity_id: "E001" 表示 员工 这个实体位点已经被唯一定位到 ID 为 E001 的那个“张三”,因此不会把其他同名员工一起聚合进来。

如果当前阶段还不知道具体的实体 ID,也可以写成:

{
  "schema": "employee_performance",
  "query": {
    "员工": {
      "schema": "employee",
      "query": {
        "姓名": "张三"
      },
      "entity_id": "placeholder"
    }
  }
}

entity_id = "placeholder" 的作用不是提供真实 ID,而是向系统表达“这里是一个实体位点,不能把多个同名实体聚合在一起”。后续即使没有完成实体消歧,系统也应该保留“这是单个实体”的语义,而不是退化成同名实体汇总。

因此可以把它理解为一条规则:

  • entity_id 应该出现在实体 schema 的节点上
  • 事件表、明细表、事实表根节点通常不直接携带 entity_id

除了实体唯一性之外,在某些带层级编码的 schema 中,entity_id 还可能被执行器用于粒度判断。

货币上下文

currency 用于声明当前 Logicform 的货币上下文。它通常出现在涉及金额、汇率换算或跨币种展示的场景中。

{
  "schema": "sales",
  "currency": "USD",
  "preds": [
    { "name": "销售额", "operator": "$sum", "pred": "amount" }
  ]
}

如果未显式指定,默认值为 CNY,会参与金额换算。

书写约定

  1. Logicform 必须是一个合法的 JSON 对象。结构字段使用英文键名,例如 schemapredsquerygroupby
  2. 业务字段名通常出现在 query 的 key、groupby._idpred.pred 等位置,这些名称直接来自语义建模,可为中文。
  3. 嵌套结构主要出现在 query 中的子 Logicform、pred 中嵌套 PredItemType、以及 from 中的子 Logicform,而不是所有字段都任意嵌套。

了解了整体框架后,可以继续阅读: