# 聚合与分组

聚合函数以 `dsl.count()` / `dsl.sum(...)` 等形式作为 select 投影出现，结果直接是标量（或按组展开）。

## 全局聚合：select 一个标量

```ts workspace
// ① 模型（完整可运行版见工作区）
import { model, prop, dsl } from '@ts-grm/core'
import { sqlClient } from './infra/sql-client'

const Book = model("Book", "id", class {
    id = prop.i64()
    name = prop.str(50)
    edition = prop.i32()
    price = prop.num(10, 2)
})

// ② 全局聚合：书的本数 + 最高价
const summary = await sqlClient.createQuery(Book, (q, book) => {
    return q.select({
        count: dsl.count(),
        maxPrice: dsl.max(book.price),
    })
}).fetchList()

console.log(summary)   // [{ count: 3, maxPrice: 79 }]
```

| 函数 | 语义 |
|---|---|
| `dsl.count()` | 行数 |
| `dsl.sum(expr)` / `dsl.avg(expr)` | 求和 / 平均 |
| `dsl.min(expr)` / `dsl.max(expr)` | 最小 / 最大 |

聚合也可直接 `q.select(dsl.count())` 得到单个标量行的数组（fetchRequired 取第一行）。

## 分组：groupBy

需要**按组聚合**时用 `q.groupBy(...)`，分组列与聚合列一起 select：

```ts
// ③ 按版次分组：每个 edition 的本数
const rows = await sqlClient.createQuery(Book, (q, book) => {
    q.groupBy(book.edition)
    return q.select({
        edition: book.edition,
        count: dsl.count(),
    })
}).fetchList()

console.log(rows)   // 按 edition 分组计数
```

分组列是普通列投影；同时取非分组列（如 `max(book.name)` 于组内取字典序最大）也合法。

## 组合使用

```ts
// ④ 分组 + 条件 + 组内聚合
const rows2 = await sqlClient.createQuery(Book, (q, book) => {
    q.where(book.price.gte(40))          // 先过滤
    q.groupBy(book.edition)              // 再分组
    return q.select({
        edition: book.edition,
        count: dsl.count(),
        best: dsl.max(book.name),
    })
}).fetchList()

console.log(rows2)
```

`where` 先于分组生效（筛的是行）；对**分组后**的过滤（HAVING 语义），0.0.13 起可用 `q.having(...)`
（谓词里可用 `dsl.count()` 等聚合，见上游追踪文档）；也可用子查询实现等价语义（见[「子查询」](../query/sub-query)页）。

## 下一步

聚合能力覆盖日常报表。下一节**子查询**：IN/EXISTS 与关联子查询——多条 SQL 的组合拳。
