# 分页与区间

收尾方法里三兄弟管"只取一部分"：`fetchPage`（总量感知的分页）、`fetchRange`（纯切片）、`fetchCount`（只数行）。

## fetchPage：分页 + 总数

```ts workspace
// ① 模型与视图（完整可运行版见工作区）
import { model, prop, dto } 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 BookView = dto.view(Book, c => [c.id, c.name, c.price])

// ② 分页：pageNo 从 1 起（缺省 1），pageSize 必填
const page = await sqlClient.createQuery(Book, (q, book) => {
    q.orderBy(book.price.desc(), book.id.asc())   // 稳定排序，见「排序」
    return q.select(book.fetch(BookView))
}).fetchPage({ pageNo: 1, pageSize: 2 })

console.log({
    totalRowCount: page.totalRowCount,   // 3
    totalPageCount: page.totalPageCount, // 2
    pageNo: page.pageNo,                 // 1
    isFirstPage: page.isFirstPage,       // true
    isLastPage: page.isLastPage,         // false
    rows: page.rows,                     // 第 1 页 2 行
})
```

`Page<T>` 六件套：`totalRowCount / totalPageCount / pageNo / isFirstPage / isLastPage / rows`。翻到不存在的页（如第 9 页）返回空 `rows`、总数不变——分页状态自己按 `pageNo` 维护即可。

## fetchRange：不要总数的切片

只要"第 offset+1 行起取 limit 行"（列表滚动加载、流式取数），`fetchRange` 不做 count，省一次查询：

```ts
// ③ 区间：跳过第 1 行取 2 行（等价 limit 2 offset 1）
const rows = await sqlClient.createQuery(Book, (q, book) => {
    q.orderBy(book.price.desc(), book.id.asc())
    return q.select(book.fetch(BookView))
}).fetchRange({ limit: 2, offset: 1 })

console.log(rows)   // 第 2、3 名
```

## fetchCount：只数行

```ts
// ④ 计数：满足条件的行数
const n = await sqlClient.createQuery(Book, (q, book) => {
    q.where(book.price.gt(50))
    return q.select(book.fetch(BookView))
}).fetchCount()

console.log(n)   // 2
```

计数同样走完整条件链（where/关联过滤都生效），只是不取行。

## 纪律：切片必须配稳定排序

`fetchPage`/`fetchRange` 都是"按排序序切片"。**排序不唯一时同名行跨页漂移**。规则：切片查询的 `orderBy` 至少包含一个唯一列做决胜：

```ts
q.orderBy(book.price.desc(), book.id.asc())   // 价格相同时按 id 定序
```

## 下一步

单表查询的骨架完整了：根查询 → 条件 → 排序 → 取形 → 分页。下一节进入**聚合与分组**：count/sum/avg 与 groupBy/having。
