全部文档
文档中心DeepModel功能DeepQL常用语法与使用技巧六、分组与汇总

六、分组与汇总

需要按某个(或某几个)维度分组,再对每组做聚合(计数、求和等)时,使用 group 语法。

传统 SQL 的 GROUP BY 直接把分组字段和聚合结果压缩到同一行,源数据行「消失」了——你无法在同一条语句里既拿到分组汇总、又看到组内的明细。

DeepQL 的 group 完全不同:它不会丢弃源数据。group 的结果是一组自由对象(Free Object),每个自由对象包含三个字段:

字段

类型

含义

key

自由对象(仅含分组字段)

该组的分组维度值,如 {status: "draft"}

grouping

字符串集合

分组字段的名称集合,如 {"status"}

elements

源对象集合

该组内的所有源对象,可聚合或展开

注意:key 是一个只包含分组字段值的自由对象(不是数据库中的对象,没有 id),它的字段与 by 中指定的分组项一一对应。

下面用文本图示说明这一关系。假设有 5 个订单头,按 status 分组后得到 3 个分组结果:

┌────────────────────────────────────────────────────────────────────┐
│                      group Order by .status                        │
│                                                                    │
│  产生 3 个自由对象(Free Object),每个代表一组                      │
│                                                                    │
│  ┌──────────────────────────────────────────────┐                  │
│  │ Free Object #1                               │                  │
│  │  .key       = { status: "draft" }            │  ◄─ 仅分组字段   │
│  │  .grouping  = { "status" }                   │                  │
│  │  .elements ───────────┐                      │                  │
│  └───────────────────────│──────────────────────┘                  │
│                          ▼                                         │
│                 ┌──────────────────┐                               │
│                 │ Order: ORD001    │  源对象,完整保留               │
│                 │ Order: ORD004    │                               │
│                 └──────────────────┘                               │
│                                                                    │
│  ┌──────────────────────────────────────────────┐                  │
│  │ Free Object #2                               │                  │
│  │  .key       = { status: "confirmed" }        │                  │
│  │  .grouping  = { "status" }                   │                  │
│  │  .elements ───────────┐                      │                  │
│  └───────────────────────│──────────────────────┘                  │
│                          ▼                                         │
│                 ┌──────────────────┐                               │
│                 │ Order: ORD002    │                               │
│                 │ Order: ORD003    │                               │
│                 └──────────────────┘                               │
│                                                                    │
│  ┌──────────────────────────────────────────────┐                  │
│  │ Free Object #3                               │                  │
│  │  .key       = { status: "done" }             │                  │
│  │  .grouping  = { "status" }                   │                  │
│  │  .elements ───────────┐                      │                  │
│  └───────────────────────│──────────────────────┘                  │
│                          ▼                                         │
│                 ┌──────────────────┐                               │
│                 │ Order: ORD005    │                               │
│                 └──────────────────┘                               │
└────────────────────────────────────────────────────────────────────┘

理解了这一点,后续的操作就很自然了:

  • 取分组维度.key.status——从 key 自由对象中取分组字段值

  • 对组内数据聚合count(.elements)sum(.elements.某属性)

  • 展开组内明细.elements: { order_no, order_date, ... }

  • 沿 elements 继续走链接.elements.<order[is OrderLine] 得到组内所有订单的订单行

直接对属性分组,属性名在 by 中必须用前导点简写(.属性名):

Copy
group Order by .status

结果中每个自由对象的 .key 会包含 .status 字段,.grouping{"status"}

当需要对属性做转换或计算后再分组时,用 using 定义别名,by 中引用别名:

Copy
group Order
using year := datetime_get(.order_date, 'year')
by year

这里先从日期中提取年份,再按年份分组。using 中的别名 year 会出现在 .key.year.grouping 中。

by 后可以用逗号分隔多个分组项(属性或 using 别名),结果按所有字段的组合分组:

Copy
group Order
using customer_name := <str>.customer.name['zh-cn']
by .status, customer_name

此时 .key 中同时包含 statuscustomer_name 两个字段。

group 后的对象可以附加形状 { }。单纯列出已有属性意义不大(后续 select 时再指定即可),它真正的用途是在形状中定义计算字段,这些计算字段会保留在 elements 的对象上,后续可以直接在 by 中引用,也可以在 select 时使用。

using 的区别:

using 别名

shape 计算字段

定义位置

using 子句

group 后的 { } 形状

能否在 by 中引用

✅ 可以

✅ 可以(通过 .字段名

是否出现在 elements 中

❌ 不会

——作为 elements 中每个对象的字段

适用场景

只用于分组维度,不需要带到明细

既用于分组,又想在 elements 明细中保留

示例:定义一个计算字段 line_amount(行金额),既按它做分组条件,又在 elements 中保留:

Copy
-- using:line_amount 只在 by 和 key 中可用,elements 中看不到
group OrderLine
using amount_level := (
    'high' if .qty * .price > 200 else 'low'
)
by amount_level

-- shape:line_amount 计算字段会保留在 elements 的每个对象上
group OrderLine {
    *,
    line_amount := .qty * .price
}
using amount_level := (
    'high' if .line_amount > 200 else 'low'
)
by amount_level

后者的 elements 中每个 OrderLine 对象都会带上 line_amount 字段,select 展开时可以直接用:

Copy
with groups := (
    group OrderLine {
        *,
        line_amount := .qty * .price
    }
    using amount_level := (
        'high' if .line_amount > 200 else 'low'
    )
    by amount_level
)

select groups {
    level := .key.amount_level,
    lines := .elements { product_name, qty, price, line_amount }
}

简而言之using 定义的别名是「用完即丢」的分组维度;shape 中的计算字段则会「跟着数据走」,留在 elements 里供后续使用。

Copy
# 可选 with 前置
with 变量 := ...

# group 语句
group 对象集合 { 可选形状 }
using 别名1 := 表达式1, 别名2 := 表达式2    # using 可选
by 分组项1, 分组项2, ...                     # by 必选

分组结果的结构:

Free Object
  ├── .key         ─── 自由对象,仅含分组字段值(无 id)
  │                     如 { status: "draft" }
  │                     如 { status: "draft", customer_name: "客户甲" }(多字段时)
  ├── .grouping    ─── 分组字段名集合
  │                     如 { "status" }
  │                     如 { "status", "customer_name" }
  └── .elements    ─── 该组内的源对象集合
                        可聚合:count(.elements)
                        可展开:.elements: { order_no, ... }
                        可走链接:.elements.<order[is OrderLine]

按订单头的 status 分组,对每组统计:订单数、该组内所有订单行的数量之和、订单行金额之和。

Copy
with OrderGroup := (
    group Order
    by .status
)

select OrderGroup {
    status := .key.status,
    order_count := count(.elements),
    total_lines := count(.elements.<order[is OrderLine]),
    total_amount := sum(.elements.<order[is OrderLine].qty * .elements.<order[is OrderLine].price)
}

说明:.elements 是该组内的订单头集合;.elements.<order[is OrderLine] 是这些订单头下的所有订单行的并集,count 得到行数,sum(...qty * price) 得到行金额合计。

结果示意:

Copy
[
  { "status": "draft", "order_count": 2, "total_lines": 3, "total_amount": 298.00 },
  { "status": "confirmed", "order_count": 2, "total_lines": 2, "total_amount": 350.00 },
  { "status": "done", "order_count": 1, "total_lines": 1, "total_amount": 88.00 }
]

按客户中文名和状态分组,统计每个客户在每种状态下的订单数和总金额:

Copy
with OrderGroup := (
    group Order
    using customer_name := <str>.customer.name['zh-cn']
    by customer_name, .status
)

select OrderGroup {
    customer_name := .key.customer_name,
    status := .key.status,
    order_count := count(.elements),
    total_amount := sum(.elements.<order[is OrderLine].qty * .elements.<order[is OrderLine].price)
}

结果示意:

Copy
[
  { "customer_name": "客户甲", "status": "confirmed", "order_count": 1, "total_amount": 350.00 },
  { "customer_name": "客户甲", "status": "draft", "order_count": 1, "total_amount": 99.00 },
  { "customer_name": "客户乙", "status": "confirmed", "order_count": 1, "total_amount": 199.00 }
]

有时不需要聚合统计,只想按维度分组后把每组内的源数据主键收集成一个数组。用 array_agg().elements 的关键字段收集为列表即可。

Copy
with OrderGroup := (
    group Order
    by .status
)

select OrderGroup {
    status     := .key.status,
    order_ids  := .elements.id,
    order_nos  := .elements.order_no
}

结果示意:

status

order_ids

order_nos

draft

[“uuid-001”, “uuid-004”]

[“ORD001”, “ORD004”]

confirmed

[“uuid-002”, “uuid-003”]

[“ORD002”, “ORD003”]

done

[“uuid-005”]

[“ORD005”]

这个模式在业务中很常见:比如「按状态分组后拿到每组的订单 ID 列表」,再传给下游做批量操作。

或者在图表聚合后,点击跳转源数据列表,可以结合UX配置实现该效果。

若想直观看到 key / grouping / elements 的真实内容,可直接 select 分组结果并展开:

Copy
with OrderGroup := (
    group Order
    by .status
)

select OrderGroup {
    key: { status },
    grouping,
    elements: {
        order_no,
        order_date,
        status
    }
}

结果示意——注意 key 中只有分组字段值,没有 id

Copy
[
  {
    "key": { "status": "confirmed" },
    "grouping": ["status"],
    "elements": [
      { "order_no": "ORD002", "order_date": "2023-10-02", "status": "confirmed" },
      { "order_no": "ORD003", "order_date": "2023-10-03", "status": "confirmed" }
    ]
  },
  {
    "key": { "status": "draft" },
    "grouping": ["status"],
    "elements": [
      { "order_no": "ORD001", "order_date": "2023-10-01", "status": "draft" },
      { "order_no": "ORD004", "order_date": "2023-10-04", "status": "draft" }
    ]
  }
]

对比 SQL 的 GROUP BY 只能得到扁平的聚合行:

status

order_count

confirmed

2

draft

2

而 DeepQL 的分组结果既能做聚合(.key.status + count(.elements)),又能展开明细(.elements: { order_no, ... }),甚至可以沿 elements 继续走链接做更深层的聚合——这是传统 SQL 做不到的。

当有多个分组字段时,有时需要同时看到不同维度组合的汇总结果——比如既想按「客户 + 状态」分组,又想看「仅按客户」的小计和「全局」的总计。

DeepQL 提供了与 PostgreSQL 几乎一致的分组集合语法:花括号 { } 定义分组集合ROLLUPCUBE 是常用的快捷写法。

如果你熟悉 PostgreSQL 的 GROUP BY ROLLUP(...) / GROUP BY CUBE(...) / GROUP BY GROUPING SETS(...),概念完全一样,只是 DeepQL 的语法更简洁。

by 子句中使用花括号 { } 表示对多种分组方式分别执行分组,结果合并到一起:

Copy
-- 分别按 status 分组 和 按 customer_name 分组,结果合并
group Order
using customer_name := <str>.customer.name['zh-cn']
by { .status, customer_name }

等价于把 group ... by .statusgroup ... by customer_name 的结果合在一起。

当有多个顶层 by 项时,取笛卡尔积。例如:

Copy
by .status, { customer_name, warehouse_name }

等价于分组集合 { (.status, customer_name), (.status, warehouse_name) }——每条结果要么按 status + 客户名 分,要么按 status + 仓库名 分。

特殊地,空元组 () 表示不按任何字段分组(即全局汇总,所有数据归为一组)。

ROLLUP(a, b, c) 等价于分组集合 { (), (a), (a, b), (a, b, c) }——按字段的前缀逐级分组。非常适合做小计 → 合计的层级汇总报表。

对照表:

ROLLUP 写法

等价分组集合

产出

ROLLUP(a)

{ (), (a) }

按 a 分组 + 全局总计

ROLLUP(a, b)

{ (), (a), (a, b) }

按 (a,b) 分组 + 按 a 小计 + 全局总计

ROLLUP(a, b, c)

{ (), (a), (a, b), (a, b, c) }

三级明细 + 两级小计 + 总计

示例:按客户 + 状态 ROLLUP 汇总

Copy
with groups := (
    group Order
    using customer_name := <str>.customer.name['zh-cn']
    by ROLLUP(customer_name, .status)
)

select groups {
    customer_name := .key.customer_name,
    status        := .key.status,
    grouping,
    order_count   := count(.elements),
    total_amount  := sum(
        .elements.<order[is OrderLine].qty
        * .elements.<order[is OrderLine].price
    )
}
order by array_agg(.grouping)

结果示意:

级别

customer_name

status

grouping

order_count

total_amount

① 全局总计

(null)

(null)

{}

5

736.00

② 按客户小计

客户甲

(null)

{customer_name}

3

548.00

② 按客户小计

客户乙

(null)

{customer_name}

2

188.00

③ 最细粒度

客户甲

confirmed

{customer_name, status}

1

350.00

③ 最细粒度

客户甲

draft

{customer_name, status}

2

198.00

③ 最细粒度

客户乙

confirmed

{customer_name, status}

2

188.00

关键:通过 grouping 字段判断当前行属于哪个汇总级别grouping 为空集时表示全局总计;包含的字段名越多,说明分组越细。非当前级别的 key 字段值为空(null)。

这与 PostgreSQL 的以下 SQL 完全等价:

Copy
-- PostgreSQL 等价写法
SELECT customer_name, status, count(*), sum(amount)
FROM orders
GROUP BY ROLLUP(customer_name, status)
ORDER BY GROUPING(customer_name, status);

CUBE(a, b) 等价于分组集合 { (), (a), (b), (a, b) }——所有字段的幂集组合。比 ROLLUP 多了「仅按 b」这种缺少高维度的分组。适合做交叉报表。

对照表:

CUBE 写法

等价分组集合

CUBE(a)

{ (), (a) }(与 ROLLUP 相同)

CUBE(a, b)

{ (), (a), (b), (a, b) }

CUBE(a, b, c)

所有 2³ = 8 种子集

示例:按客户 × 状态 CUBE 交叉汇总

Copy
with groups := (
    group Order
    using customer_name := <str>.customer.name['zh-cn']
    by CUBE(customer_name, .status)
)

select groups {
    customer_name := .key.customer_name,
    status        := .key.status,
    grouping,
    order_count   := count(.elements)
}
order by array_agg(.grouping)

结果示意:

级别

customer_name

status

grouping

order_count

全局总计

(null)

(null)

{}

5

仅按客户

客户甲

(null)

{customer_name}

3

仅按客户

客户乙

(null)

{customer_name}

2

仅按状态

(null)

confirmed

{status}

3

仅按状态

(null)

draft

{status}

2

最细粒度

客户甲

confirmed

{customer_name, status}

1

最细粒度

客户甲

draft

{customer_name, status}

2

最细粒度

客户乙

confirmed

{customer_name, status}

2

注意加粗的「仅按状态」行——这是 CUBE 比 ROLLUP 多出来的级别。

by .a, .b                   →  只有 (a, b) 一种分组
by ROLLUP(.a, .b)           →  { (), (a), (a, b) }       3 种分组
by CUBE(.a, .b)             →  { (), (a), (b), (a, b) }  4 种分组
by { .a, .b }               →  { (a), (b) }              2 种分组(分组集合)
by .a, ROLLUP(.b)           →  { (a), (a, b) }           笛卡尔积

DeepQL

PostgreSQL

group Order by ROLLUP(.a, .b)

SELECT ... GROUP BY ROLLUP(a, b)

group Order by CUBE(.a, .b)

SELECT ... GROUP BY CUBE(a, b)

group Order by { .a, .b }

SELECT ... GROUP BY GROUPING SETS ((a), (b))

.grouping 字段

GROUPING(a, b) 函数

.key.a 为空(null / {}

对应字段为 NULL

如果你有 PostgreSQL 的 ROLLUP / CUBE 使用经验,DeepQL 的用法几乎可以直接平移,区别仅在于 DeepQL 用 grouping 字段(字符串集合)代替了 PostgreSQL 的 GROUPING() 位图函数,语义更直观。

实际业务中,有时需要把两个完全不同的对象类型合并到一起做分组统计——它们的数据字段不同,但有共同的分组维度(如人员、组织)。

核心思路:

  1. union 合并:用 union 把两类对象合并为一个集合(前提是它们有共同的基类或兼容的链接)。

  2. 形状补齐:合并后的对象字段不同,通过形状中的计算字段,把「各自的值字段」统一映射到同一个字段名上。

  3. 分组聚合:对统一后的集合正常 group + 聚合。

假设有两个对象:

  • ClassHour(课时):有 person(人员链接)、organization(组织链接)、coach_performance(教练绩效,decimal 类型)

  • PerformanceFlow(绩效流水):有 personorganizationamount(金额,decimal 类型)

需求:按 person + organization 分组,把两类数据的值合并求和

Copy
with
    -- ① 合并两类对象为一个集合
    total := ClassHour union PerformanceFlow,

    -- ② 补齐字段:用子查询判断当前对象属于哪一类,取对应的值字段
    total := total {
        obj1 := (select ClassHour filter .id = total.id),
        obj2 := (select PerformanceFlow filter .id = total.id),
    } {
        data := assert_single(
            <decimal>.obj1.coach_performance union .obj2.amount
        ),
    },

    -- ③ 正常分组
    G := (
        group total
        by .person, .organization
    ),

select G {
    person       := .key.person.code,
    organization := .key.organization.code,
    data         := sum(.elements.data),
}

第一步:union 合并

Copy
total := ClassHour union PerformanceFlow,

把两类对象合并为一个集合。union 后的 total 中每个元素要么是 ClassHour,要么是 PerformanceFlow。

第二步:形状补齐——统一数据字段

Copy
total := total {
    obj1 := (select ClassHour filter .id = total.id),
    obj2 := (select PerformanceFlow filter .id = total.id),
} {
    data := assert_single(
        <decimal>.obj1.coach_performance union .obj2.amount
    ),
},

这里用了两层形状(管道式写法):

  • 第一层:对每个 total 元素,分别尝试匹配到 ClassHour 和 PerformanceFlow。如果当前元素是 ClassHour,则 obj1 有值、obj2 为空集;反之亦然。

  • 第二层:用 union 把两个可能的值合并——其中一个一定是空集,所以 union 的结果就是「有值的那个」。assert_single 确保结果是单值。

这个技巧的本质是:用 union + 空集来模拟「if 是 A 类型取字段 x,else 取字段 y」

第三步:正常分组聚合

Copy
G := (
    group total
    by .person, .organization
),

select G {
    person       := .key.person.code,
    organization := .key.organization.code,
    data         := sum(.elements.data),
}

统一了 data 字段后,后续的 group + sum 就跟普通分组完全一样了。

  • 多张业务表的数据需要合并汇总(如不同来源的绩效、不同类型的流水)

  • 各表有共同的分组维度(如人员、组织、时间),但值字段名称/类型不同

  • 类似 SQL 中先 UNION ALLGROUP BY 的模式

总结 group 的思维模型:

源对象集合                    group by .字段
  Order[]          ──────────────────────►   Free Object[]
                                               │
                                               ├── .key         自由对象,仅含分组字段值
                                               ├── .grouping    分组字段名集合
                                               └── .elements    ──► 源对象子集
                                                                      │
                                                    可聚合:count(.elements)
                                                    可展开:.elements: { ... }
                                                    可继续走链接:.elements.<order[is OrderLine]
  1. group 不丢数据:它只是把源对象按维度「装进不同的桶」,每个桶就是一个自由对象。

  2. key 是自由对象,不是数据库对象:key 里只有分组字段的值(如 {status: "draft"}),没有 id,不是对象实例。

  3. elements 是源对象集合:你可以对它做任何集合能做的操作——聚合、展开、继续走链接。

  4. using 用于转换:分组前需要对属性做计算(如取年份、取中文名)时,在 using 中定义别名,by 中引用别名。

  5. 先 group,再 select:group 只分组不输出,需配合 select 指定最终输出的形状。

概念

说明

group ... by .属性

最简分组,直接按属性分

group ... using 别名 := 表达式 by 别名

先转换/计算,再按别名分

by .字段1, .字段2

多字段组合分组

key

自由对象,仅含分组字段值(无 id)

grouping

分组字段名集合,区分不同汇总级别

elements

该组内的源对象集合,可聚合、可展开、可继续走链接

先 group 再 select

group 只分组不输出,需配合 select 指定输出形状

by { .a, .b }

分组集合:分别按 a、按 b 分组,结果合并

ROLLUP(a, b, c)

前缀分组集合 {(), (a), (a,b), (a,b,c)},多级汇总

CUBE(a, b)

幂集分组集合 {(), (a), (b), (a,b)},交叉分析

union + 形状补齐 + group

合并不同对象后统一字段,再分组聚合(类似 SQL 的 UNION ALL + GROUP BY)

下一步可以系统了解表达式与常用函数,见「七、表达式与常用函数」。

回到顶部

咨询热线

400-821-9199

我们使用 ChatGPT,基于文档中心的内容以及对话上下文回答您的问题。

ctrl+Enter to send