分页
分页是指在页面中检索结果,而不是一次性全部检索;这通常针对大型结果集执行,其中显示的用户界面支持用户导航到结果的下一页或上一页。
警告
无论使用哪种分页方法,请始终确保排序是完全唯一的。 例如,如果结果仅按日期排序,但可能存在多个日期相同的结果,则在分页时可以跳过结果,因为它们在两个分页查询中以不同的方式排序。 按日期和 ID(或任何其他唯一属性或属性组合进行排序)使排序完全唯一,并避免此问题。 请注意,关系数据库默认不应用任何排序,即使在主键上也是如此。
注意
Azure Cosmos DB 有自己的分页机制,请参阅专门的文档页面。
偏移分页
使用数据库实现分页的一种常见方法是使用 Skip
和 Take
LINQ 运算符(在 SQL 中为 OFFSET
和 LIMIT
)。 给定页面大小为 10 个结果,可以使用 EF Core 提取第三页,如下所示:
var position = 20;
var nextPage = context.Posts
.OrderBy(b => b.PostId)
.Skip(position)
.Take(10)
.ToList();
遗憾的是,虽然这种技术非常直观,但也存在一些严重的缺点:
- 即使前 20 个条目未返回到应用程序,数据库也必须对其进行处理;这会因为跳过的行数创建可能显著增加的计算负载。
- 如果同时发生任何更新,则分页最终可能会跳过某些条目或显示这些条目两次。 例如,如果用户从第 2 页移动到第 3 页时移除了一个条目,则整个结果集会“向上移动”,并将跳过一个条目。
键集分页
基于偏移的分页的建议替代方法(有时称为 键集分页或基于查找的分页分页)是简单地使用 WHERE
子句跳过行,而不是偏移量。 这意味着要记住提取的最后一个条目中的相关值(而不是其偏移量),并请求在该行之后的下一行。 例如,假设提取的上一页中最后一个条目 ID 值为 55,则只需执行以下操作:
var lastId = 55;
var nextPage = context.Posts
.OrderBy(b => b.PostId)
.Where(b => b.PostId > lastId)
.Take(10)
.ToList();
假设在 PostId
上定义了索引,则此查询会非常高效,并且对 ID 值较低的任何并发更改也不敏感。
键集分页适用于用户向前和向后导航的分页界面,但不支持用户可以跳转到任何特定页面的随机访问。 随机访问分页需要使用如上所述的偏移分页;由于偏移分页的缺点,请仔细考虑你的用例是否确实需要随机访问分页,或者下一页/上一页导航是否足够。 如果需要随机访问分页,则可靠的实现可以在导航到下一页/上一页时使用键集分页,并在跳转到任何其他页面时偏移导航。
多个分页键
使用键集分页时,经常需要按多个属性进行排序。 例如,以下查询会按日期和 ID 分页:
var lastDate = new DateTime(2020, 1, 1);
var lastId = 55;
var nextPage = context.Posts
.OrderBy(b => b.Date)
.ThenBy(b => b.PostId)
.Where(b => b.Date > lastDate || (b.Date == lastDate && b.PostId > lastId))
.Take(10)
.ToList();
这可确保下一页准确选取上一页结束的位置。 随着添加更多的排序键,可以添加其他子句。
注意
大多数 SQL 数据库支持上述做法的更简单且更高效的版本,即使用行值:WHERE (Date, Id) > (@lastDate, @lastId)
。 EF Core 目前不支持在 LINQ 查询中表达此功能,这是由 #26822 跟踪的。
索引
与任何其他查询一样,正确编制索引对于实现良好的性能至关重要:请确保编制与分页排序相对应的索引。 如果按多个列排序,则可以定义这些多个列的索引;这称为复合索引。
有关详细信息,请参阅关于索引的文档页。
其他资源
- 要详细了解基于偏移的分页的缺点和键集分页,请参阅此文章。
- .NET 数据社区站会,我们将在此讨论分页并演示上述所有概念。
- 比较偏移和键集分页的技术深入演示。 虽然内容处理的是 PostgreSQL 数据库,但常规信息也对其他关系数据库有效。
- 有关 EF Core 上可简化键集分页的扩展,请参阅 MR.EntityFrameworkCore.KeysetPagination 和 MR.AspNetCore.Pagination。