PostgreSQL/MySQL的光标分页
#教程 #postgres #go #mysql

光标分页是提高显示大量数据的Web应用程序的性能和可用性的有用技术。

使用光标分页,服务器将数据页与光标一起发送给客户端,该数据标识了页面中最后一项的位置。客户端可以使用此光标请求数据的下一页,将光标作为参数传递给服务器。

介绍

通常,您可以使用LIMIT X OFFSET Y通过结果:

SELECT * FROM entries ORDER BY id ASC LIMIT 10 OFFSET 0; -- first page
SELECT * FROM entries ORDER BY id ASC LIMIT 10 OFFSET 10; -- second page
SELECT * FROM entries ORDER BY id ASC LIMIT 10 OFFSET 20; -- third page

这种分页方法效果很好,但是您可能会注意到,随着偏移量的增长,查询变得越来越慢。之所以发生这种情况,是因为OFFSET 100000告诉数据库读取并丢弃100,000行,这使得绩效使大偏移无法接受。这里的通常响应是限制允许的偏移范围,例如,您可以将允许页面的数量限制为1000。

但是,如果您不能限制页面数量怎么办?例如,GitHub必须允许用户在存储库中查看所有提交,无论存储库有多大。答案是光标分页。

光标分页

基于光标的分页可以通过将指针(光标)返回到页面上的最后一项来起作用。为了获取下一页,客户端将光标传递到服务器,并且在给定光标之后,服务器返回结果。这种方法的主要限制是客户不能跳到特定页面,也不知道页面的总数。

::: tip
基于光标的分页提供了比经典分页更糟糕的用户体验。
仅在必须时才使用。
:::

由于光标必须明确标识该行,因此您只能在具有唯一约束的主键或列上使用基于光标的分页。这也确保查询使用索引并可以快速跳过已经分页的行。

光标分页与偏移分页

与传统的基于页面的分页相比,光标分页具有多个优点:

  • 性能。光标分页减少了需要从数据库中检索的数据量,从而导致页面加载时间更快并减少了服务器加载。

  • 稳定性。与基于页面的分页相比,光标分页提供了更稳定和可预测的分页,如果在导航页面时添加或删除数据,这可能会导致分页不一。

所有这些都以降低的灵活性。光标分页不允许用户跳至数据集中的任何点,而不必遍历所有以前的页面。

例子

让我们使用主键作为指针划分以下模型:

type Entry struct {
    ID   int64
    Text string
}

我们的助手Cursor结构可能看起来像这样:

type Cursor struct {
    Start int64 // pointer to the first item for the previous page
    End   int64 // pointer to the last item for the next page
}

要检索下一页,我们需要从光标指向最后一项:

func selectNextPage(ctx context.Context, db *bun.DB, cursor int64) ([]Entry, Cursor, error) {
    var entries []Entry
    if err := db.NewSelect().
        Model(&entries).
        Where("id > ?", cursor).
        OrderExpr("id ASC").
        Limit(10).
        Scan(ctx); err != nil {
        return nil, Cursor{}, err
    }
    return entries, NewCursor(entries), nil
}

要检索上一页,我们需要从光标指向第一个项目开始向后迭代:

func selectPrevPage(ctx context.Context, db *bun.DB, cursor int64) ([]Entry, Cursor, error) {
    var entries []Entry
    if err := db.NewSelect().
        Model(&entries).
        Where("id < ?", cursor).
        OrderExpr("id DESC").
        Limit(10).
        Scan(ctx); err != nil {
        return nil, Cursor{}, err
    }
    return entries, NewCursor(entries), nil
}

我们可以使用这样的方法:

page1, cursor, err := selectNextPage(ctx, db, 0)
if err != nil {
    panic(err)
}

page2, cursor, err := selectNextPage(ctx, db, cursor.End)
if err != nil {
    panic(err)
}

prevPage, _, err := selectPrevPage(ctx, db, cursor.Start)
if err != nil {
    panic(err)
}

有关详细信息,请参见example

监视性能

to monitor Bun performance,您可以使用包含bun的opentelemetry仪器。

通过使用OpentElemetry,开发人员可以对其应用程序的性能以及不同组件之间的相互作用获得宝贵的见解,从而更容易解决问题,优化性能并提高分布式系统的整体可靠性。

>

UpTrace是一个支持分布的跟踪,指标和日志的OpenTelemetry backend。您可以使用它来监视应用程序并解决问题。

Uptrace overview

UpTrace带有直观的查询构建器,丰富的仪表板,通知的警报规则以及大多数语言和框架的集成。

UpTrace可以在单个服务器上处理数十亿个跨度和指标,并允许您以较低的成本监视应用程序。

在短短几分钟内,您可以通过访问cloud demo(无需登录)或使用Docker在本地运行它来尝试上升轨道。源代码可在GitHub上找到。