排序与分页:orderBy、offset 与 cursor
本教程共 54 篇 · 第 23 篇 · 更新于 2026-08-11 · 约 4 分钟阅读
本节目标:学会多字段排序、按关系排序,以及两种分页方案的选择与实现。
查询结果按什么顺序返回、一次返回多少,是列表接口的两件大事。orderBy 管排序,take / skip / cursor 管分页。
单字段与多字段排序
await prisma.post.findMany({
orderBy: { createdAt: "desc" },
});
asc 升序、desc 降序。不写 orderBy 时顺序由数据库决定,不能依赖。多字段排序传数组,顺序就是优先级:
await prisma.post.findMany({
orderBy: [{ published: "desc" }, { createdAt: "desc" }],
});
先按是否发布排,再按时间排。数组里每个字段的方向独立。
按关系排序
一对一关系直接嵌字段排序:
await prisma.post.findMany({
orderBy: { author: { name: "asc" } },
});
一对多关系只支持按数量排:
await prisma.user.findMany({
orderBy: { posts: { _count: "desc" } },
take: 10, // 帖子最多的前 10 个用户
});
null 值的位置可以指定,仅限可选标量字段:
await prisma.user.findMany({
orderBy: { lastLogin: { sort: "desc", nulls: "last" } },
});
Note
nulls: "first"/nulls: "last"只能用于可选标量字段,用在必填字段或关系字段上会抛 P2009 错误。nulls 排序的支持情况因数据库而异,使用时以官方文档为准。
按相关性排序
全文搜索的结果按相关度排序,用 _relevance:
await prisma.post.findMany({
where: { title: { search: "prisma" } },
orderBy: {
_relevance: { fields: ["title"], search: "prisma", sort: "desc" },
},
});
Note_relevance 依赖全文搜索能力:PostgreSQL 需开启 fullTextSearchPostgres 预览功能(Preview),MySQL 原生支持。完整用法在第 25 章展开。
offset 分页:简单但有上限
skip 跳过前 N 条,take 取 N 条:
const page = 3,
size = 20;
await prisma.post.findMany({
skip: (page - 1) * size,
take: size,
});
适合页码跳转的管理后台。缺点是 offset 越大越慢:数据库要扫描并丢弃前面所有行。数据到几万条以后,深分页会明显变慢。
cursor 分页:稳定且快
信息流、时间线这类场景用 cursor(游标)分页。游标是上一页最后一条记录的定位标识:
const firstPage = await prisma.post.findMany({
take: 10,
orderBy: { id: "asc" },
});
const lastId = firstPage.at(-1)?.id;
const nextPage = lastId
? await prisma.post.findMany({
take: 10,
skip: 1, // 跳过游标那一条
cursor: { id: lastId },
orderBy: { id: "asc" },
})
: [];
cursor 通过索引直接定位,翻多少页都快;中途新增、删除数据也不会让页与页之间错位。
Notecursor 分页要求 orderBy 稳定且唯一。只用 createdAt 排序,同一时间戳的记录顺序不定,翻页会重复或漏数据。稳妥做法是 (createdAt, id) 双字段排序,或直接用主键 id 排序。反向翻页可以把 take 设为负数取末尾记录,仍需配合 orderBy 与 cursor。
取 N+1 条:判断还有没有下一页
cursor 分页不知道总页数,用「多取一条」判断是否还有下一页:
async function feed(cursor?: number) {
const items = await prisma.post.findMany({
take: 21, // 要 20 条,多取 1 条做判断
cursor: cursor ? { id: cursor } : undefined,
skip: cursor ? 1 : 0,
orderBy: { id: "desc" },
});
const nextCursor = items.length === 21 ? items.pop()!.id : null;
return { items, nextCursor };
}
拿到 21 条说明还有下一页,第 21 条就是下一批的游标;不足 21 条说明到底了。想显示总页数,再单独 count 一次,但大表上 count 很贵,能省则省。
Relay 风格
GraphQL 生态常见的 Relay 分页规范:结果包成 edges 数组,每条含 node 和 cursor,另附 pageInfo 提供 hasNextPage 与 endCursor。cursor 通常编码成不透明的字符串(如 base64),前端不解析内容,原样回传即可。
两种分页怎么选
| 场景 | 方案 |
|---|---|
| 页码跳转、数据量小 | offset |
| 信息流、无限滚动、大数据量 | cursor |
| 排序字段不稳定 | offset(cursor 需要唯一排序) |
一句话:要页码用 offset,要性能与稳定用 cursor。
参考来源
- Prisma 官方文档:Filtering and sorting / Pagination
- Prisma 官方文档:Prisma Client API reference(orderBy)
- Mapagam:Sorting and Ordering Results / Implementing Pagination
- Tech Insider:Prisma ORM Tutorial(Cursor Pagination)