首页 / Prisma ORM 入门教程 / 排序与分页:orderBy、offset 与 cursor

Prisma ORM 入门教程

排序与分页:orderBy、offset 与 cursor

本教程共 54 篇 · 第 23 篇 · 更新于 2026-08-11 · 约 4 分钟阅读

orderBy分页cursoroffset排序查询

本节目标:学会多字段排序、按关系排序,以及两种分页方案的选择与实现。

查询结果按什么顺序返回、一次返回多少,是列表接口的两件大事。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 通过索引直接定位,翻多少页都快;中途新增、删除数据也不会让页与页之间错位。

Note

cursor 分页要求 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)