分页

分页是指在页面中检索结果,而不是一次性全部检索;这通常针对大型结果集执行,其中显示的用户界面支持用户导航到结果的下一页或上一页。

警告

无论使用哪种分页方法,请始终确保排序是完全唯一的。 例如,如果结果仅按日期排序,但可能存在多个日期相同的结果,则在分页时可以跳过结果,因为它们在两个分页查询中以不同的方式排序。 按日期和 ID(或任何其他唯一属性或属性组合进行排序)使排序完全唯一,并避免此问题。 请注意,关系数据库默认不应用任何排序,即使在主键上也是如此。

注意

Azure Cosmos DB 有自己的分页机制,请参阅专门的文档页面

偏移分页

使用数据库实现分页的一种常见方法是使用 SkipTake LINQ 运算符(在 SQL 中为 OFFSETLIMIT)。 给定页面大小为 10 个结果,可以使用 EF Core 提取第三页,如下所示:

var position = 20;
var nextPage = context.Posts
    .OrderBy(b => b.PostId)
    .Skip(position)
    .Take(10)
    .ToList();

遗憾的是,虽然这种技术非常直观,但也存在一些严重的缺点:

  1. 即使前 20 个条目未返回到应用程序,数据库也必须对其进行处理;这会因为跳过的行数创建可能显著增加的计算负载。
  2. 如果同时发生任何更新,则分页最终可能会跳过某些条目或显示这些条目两次。 例如,如果用户从第 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 跟踪的。

索引

与任何其他查询一样,正确编制索引对于实现良好的性能至关重要:请确保编制与分页排序相对应的索引。 如果按多个列排序,则可以定义这些多个列的索引;这称为复合索引

有关详细信息,请参阅关于索引的文档页

其他资源