需要按某个(或某几个)维度分组,再对每组做聚合(计数、求和等)时,使用 group 语法。
传统 SQL 的 GROUP BY 直接把分组字段和聚合结果压缩到同一行,源数据行「消失」了——你无法在同一条语句里既拿到分组汇总、又看到组内的明细。
DeepQL 的 group 完全不同:它不会丢弃源数据。group 的结果是一组自由对象(Free Object),每个自由对象包含三个字段:
|
字段 |
类型 |
含义 |
|---|---|---|
|
key |
自由对象(仅含分组字段) |
该组的分组维度值,如 |
|
grouping |
字符串集合 |
分组字段的名称集合,如 |
|
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 中必须用前导点简写(.属性名):
group Order by .status
结果中每个自由对象的 .key 会包含 .status 字段,.grouping 为 {"status"}。
当需要对属性做转换或计算后再分组时,用 using 定义别名,by 中引用别名:
group Order
using year := datetime_get(.order_date, 'year')
by year
这里先从日期中提取年份,再按年份分组。using 中的别名 year 会出现在 .key.year 和 .grouping 中。
by 后可以用逗号分隔多个分组项(属性或 using 别名),结果按所有字段的组合分组:
group Order
using customer_name := <str>.customer.name['zh-cn']
by .status, customer_name
此时 .key 中同时包含 status 和 customer_name 两个字段。
group 后的对象可以附加形状 { }。单纯列出已有属性意义不大(后续 select 时再指定即可),它真正的用途是在形状中定义计算字段,这些计算字段会保留在 elements 的对象上,后续可以直接在 by 中引用,也可以在 select 时使用。
与 using 的区别:
|
using 别名 |
shape 计算字段 | |
|---|---|---|
|
定义位置 |
|
group 后的 |
|
能否在 |
✅ 可以 |
✅ 可以(通过 |
|
是否出现在 elements 中 |
❌ 不会 |
✅ 会——作为 elements 中每个对象的字段 |
|
适用场景 |
只用于分组维度,不需要带到明细 |
既用于分组,又想在 elements 明细中保留 |
示例:定义一个计算字段 line_amount(行金额),既按它做分组条件,又在 elements 中保留:
-- 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 展开时可以直接用:
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 里供后续使用。
# 可选 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 分组,对每组统计:订单数、该组内所有订单行的数量之和、订单行金额之和。
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) 得到行金额合计。
结果示意:
[
{ "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 }
]
按客户中文名和状态分组,统计每个客户在每种状态下的订单数和总金额:
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)
}
结果示意:
[
{ "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 的关键字段收集为列表即可。
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 分组结果并展开:
with OrderGroup := (
group Order
by .status
)
select OrderGroup {
key: { status },
grouping,
elements: {
order_no,
order_date,
status
}
}
结果示意——注意 key 中只有分组字段值,没有 id:
[
{
"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 几乎一致的分组集合语法:花括号 { } 定义分组集合、ROLLUP 和 CUBE 是常用的快捷写法。
如果你熟悉 PostgreSQL 的 GROUP BY ROLLUP(...) / GROUP BY CUBE(...) / GROUP BY GROUPING SETS(...),概念完全一样,只是 DeepQL 的语法更简洁。
在 by 子句中使用花括号 { } 表示对多种分组方式分别执行分组,结果合并到一起:
-- 分别按 status 分组 和 按 customer_name 分组,结果合并
group Order
using customer_name := <str>.customer.name['zh-cn']
by { .status, customer_name }
等价于把 group ... by .status 和 group ... by customer_name 的结果合在一起。
当有多个顶层 by 项时,取笛卡尔积。例如:
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 写法 |
等价分组集合 |
产出 |
|---|---|---|
|
|
|
按 a 分组 + 全局总计 |
|
|
|
按 (a,b) 分组 + 按 a 小计 + 全局总计 |
|
|
|
三级明细 + 两级小计 + 总计 |
示例:按客户 + 状态 ROLLUP 汇总
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) |
|
3 |
548.00 |
|
② 按客户小计 |
客户乙 |
(null) |
|
2 |
188.00 |
|
③ 最细粒度 |
客户甲 |
confirmed |
|
1 |
350.00 |
|
③ 最细粒度 |
客户甲 |
draft |
|
2 |
198.00 |
|
③ 最细粒度 |
客户乙 |
confirmed |
|
2 |
188.00 |
关键:通过 grouping 字段判断当前行属于哪个汇总级别。grouping 为空集时表示全局总计;包含的字段名越多,说明分组越细。非当前级别的 key 字段值为空(null)。
这与 PostgreSQL 的以下 SQL 完全等价:
-- 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 写法 |
等价分组集合 |
|---|---|
|
|
|
|
|
|
|
|
所有 2³ = 8 种子集 |
示例:按客户 × 状态 CUBE 交叉汇总
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) |
|
3 |
|
仅按客户 |
客户乙 |
(null) |
|
2 |
|
仅按状态 |
(null) |
confirmed |
|
3 |
|
仅按状态 |
(null) |
draft |
|
2 |
|
最细粒度 |
客户甲 |
confirmed |
|
1 |
|
最细粒度 |
客户甲 |
draft |
|
2 |
|
最细粒度 |
客户乙 |
confirmed |
|
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 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
对应字段为 NULL |
如果你有 PostgreSQL 的 ROLLUP / CUBE 使用经验,DeepQL 的用法几乎可以直接平移,区别仅在于 DeepQL 用 grouping 字段(字符串集合)代替了 PostgreSQL 的 GROUPING() 位图函数,语义更直观。
实际业务中,有时需要把两个完全不同的对象类型合并到一起做分组统计——它们的数据字段不同,但有共同的分组维度(如人员、组织)。
核心思路:
union 合并:用 union 把两类对象合并为一个集合(前提是它们有共同的基类或兼容的链接)。
形状补齐:合并后的对象字段不同,通过形状中的计算字段,把「各自的值字段」统一映射到同一个字段名上。
分组聚合:对统一后的集合正常 group + 聚合。
假设有两个对象:
ClassHour(课时):有 person(人员链接)、organization(组织链接)、coach_performance(教练绩效,decimal 类型)
PerformanceFlow(绩效流水):有 person、organization、amount(金额,decimal 类型)
需求:按 person + organization 分组,把两类数据的值合并求和。
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 合并
total := ClassHour union PerformanceFlow,
把两类对象合并为一个集合。union 后的 total 中每个元素要么是 ClassHour,要么是 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
),
},
这里用了两层形状(管道式写法):
第一层:对每个 total 元素,分别尝试匹配到 ClassHour 和 PerformanceFlow。如果当前元素是 ClassHour,则 obj1 有值、obj2 为空集;反之亦然。
第二层:用 union 把两个可能的值合并——其中一个一定是空集,所以 union 的结果就是「有值的那个」。assert_single 确保结果是单值。
这个技巧的本质是:用 union + 空集来模拟「if 是 A 类型取字段 x,else 取字段 y」。
第三步:正常分组聚合
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 ALL 再 GROUP BY 的模式
总结 group 的思维模型:
源对象集合 group by .字段
Order[] ──────────────────────► Free Object[]
│
├── .key 自由对象,仅含分组字段值
├── .grouping 分组字段名集合
└── .elements ──► 源对象子集
│
可聚合:count(.elements)
可展开:.elements: { ... }
可继续走链接:.elements.<order[is OrderLine]
group 不丢数据:它只是把源对象按维度「装进不同的桶」,每个桶就是一个自由对象。
key 是自由对象,不是数据库对象:key 里只有分组字段的值(如 {status: "draft"}),没有 id,不是对象实例。
elements 是源对象集合:你可以对它做任何集合能做的操作——聚合、展开、继续走链接。
using 用于转换:分组前需要对属性做计算(如取年份、取中文名)时,在 using 中定义别名,by 中引用别名。
先 group,再 select:group 只分组不输出,需配合 select 指定最终输出的形状。
|
概念 |
说明 |
|---|---|
|
|
最简分组,直接按属性分 |
|
|
先转换/计算,再按别名分 |
|
|
多字段组合分组 |
|
key |
自由对象,仅含分组字段值(无 id) |
|
grouping |
分组字段名集合,区分不同汇总级别 |
|
elements |
该组内的源对象集合,可聚合、可展开、可继续走链接 |
|
先 group 再 select |
group 只分组不输出,需配合 select 指定输出形状 |
|
|
分组集合:分别按 a、按 b 分组,结果合并 |
|
|
前缀分组集合 |
|
|
幂集分组集合 |
|
|
合并不同对象后统一字段,再分组聚合(类似 SQL 的 UNION ALL + GROUP BY) |
下一步可以系统了解表达式与常用函数,见「七、表达式与常用函数」。
回到顶部
咨询热线
