将搜索结果分页

Web 应用通常会在向用户呈现数据时对其进行分页。最终用户会收到一页结果,当他们转到下一页时,系统会检索并显示下一批结果。本页介绍了如何在 Spanner 中执行全文搜索时向搜索结果添加分页。

分页选项

您可以通过两种方式在 Spanner 中实现分页查询:基于键的分页(推荐)和基于偏移量分页

基于键的分页是一种检索搜索结果的方法,可将搜索结果检索为更小、更易于管理的部分,同时确保各个请求的结果一致。网页最后一条结果中的唯一标识符(“键”)用作提取下一组结果的参考点。

Spanner 通常建议使用基于键的分页。虽然基于偏移量的分页更易于实现,但它有两个重大缺点:

  1. 查询费用更高:基于偏移量的分页会重复检索并舍弃相同的结果,导致费用增加且性能下降。
  2. 结果不一致:在分页查询中,每个网页通常会在不同的读取时间戳处检索。例如,第一个网页可能来自下午 1 点的查询,而下一个网页可能来自下午 1:10 的查询。这意味着,搜索结果可能会在每次查询之间发生变化,导致不同网页上的搜索结果不一致。

另一方面,基于键的分页使用网页最后一条结果中的唯一标识符(键)提取下一组结果。这可确保高效检索和一致的结果,即使底层数据发生变化也是如此。

为了确保网页结果的稳定性,应用可以在同一时间戳发出针对不同网页的所有查询。不过,如果查询超出版本保留期限(默认 1 小时),则可能会失败。例如,如果 version_gc 为 1 小时,而最终用户在下午 1 点提取了第一批结果,并在下午 3 点点击了下一步,就会发生这种失败情况。

使用基于键的分页

基于键的分页会记住上一页的最后一项,并将其用作下一页查询的起点。为此,查询必须返回 ORDER BY 子句中指定的列,并使用 LIMIT 限制行数。

为了实现基于键的分页,查询必须按某种严格的总排序对结果进行排序。若要获得此类排名,最简单的方法是选择任何总订单数,然后根据需要添加平分排名列。在大多数情况下,总排序是搜索索引排序,而唯一的列组合是基表主键。

使用 Albums 示例架构,对于第一页,查询如下所示:

SELECT AlbumId, ReleaseTimestamp
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")
ORDER BY ReleaseTimestamp DESC, AlbumId
LIMIT 10;

由于 ReleaseTimestamp 不是键,因此 AlbumId 是用于决胜的依据。可能有两个不同的影集具有相同的 ReleaseTimestamp 值。

如需继续,应用会再次运行相同的查询,但会使用限制上一个页面中的结果的 WHERE 子句。额外的条件需要考虑键方向(升序与降序)、平分情况的处理方式,以及可为 null 的列的 null 值的顺序。

在我们的示例中,AlbumId 是唯一的键列(按升序排列),且不能为 NULL,因此条件如下所示:

SELECT AlbumId, ReleaseTimestamp
FROM Albums
WHERE (ReleaseTimestamp < @last_page_release_timestamp
       OR (ReleaseTimestamp = @last_page_release_timestamp
           AND AlbumId > @last_page_album_id))
      AND SEARCH(AlbumTitle_Tokens, @p)
ORDER BY ReleaseTimestamp DESC, AlbumId ASC
LIMIT @page_size;

Spanner 会将此类条件解读为可跳转。这意味着,Spanner 不会读取要滤除的文档的索引。正是这项优化使得基于键的分页比基于偏移的分页高效得多。

使用基于偏移的分页

基于偏移量的分页功能利用 SQL 查询的 LIMITOFFSET 子句来模拟页面。LIMIT 值表示每页的结果数。OFFSET 值为第一个页面设为零,为第二个页面设为页面大小,为第三个页面设为页面大小的两倍。

例如,以下查询会提取第三页,页面大小为 50:

SELECT AlbumId
FROM Albums
WHERE SEARCH(AlbumTitle_Tokens, "fifth symphony")
ORDER BY ReleaseTimestamp DESC, AlbumId
LIMIT 50 OFFSET 100;

使用说明:

  • 强烈建议使用 ORDER BY 子句,以确保页面之间的排序一致。
  • 在生产查询中,使用查询参数(而非常量)指定 LIMITOFFSET,以提高查询缓存效率。如需了解详情,请参阅查询参数

后续步骤