☰
PostgREST 分页与计数全指南:Range 请求头、limit/offset 参数与 Prefer: count 三种计数策略
2026/10/7 3:34:42 网站建设 项目流程

PostgREST 分页与计数全指南:Range 请求头、limit/offset 参数与 Prefer: count 三种计数策略

【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest

PostgREST 将 PostgreSQL 表、视图与函数自动暴露为 REST API,而分页(Pagination)与计数(Count)是任何 API 消费者都必须掌握的基础能力。本文以官方文档 Pagination and Count 为核心骨架,完整讲解如何通过limit/offset查询参数与Range请求头控制返回行数,并深入剖析Prefer: count=exact | planned | estimated三种计数模式的使用场景、精度差异与底层实现原理(结合 RangeQuery.hs、Statements.hs 等源码与 RangeSpec.hs 测试用例)。读完本文,你将能独立实现客户端的分页控件、最后一页跳转、大数据量下的高性能计数,以及基于db-max-rows阈值自适应计数策略。

一、核心机制:基于 HTTP Range 头的 RFC7233 分页方案

PostgREST 使用 HTTP Range 头来描述结果集大小,这是一种遵循 RFC 7233(HTTP Range Requests)的解决方案。每个响应都会携带当前返回的行区间;如果请求了计数,还会附带总数。

一个典型的分页响应头如下:

HTTP/1.1 200 OK Range-Unit: items Content-Range: 0-14/*

这里表示返回了第 0 到第 14 行(共 15 行)。这条信息存在于每一个响应中,客户端可以直接依据它渲染分页控件(例如判断"是否有下一页"、"当前在第几页")。由于区间信息全部放在响应头中,响应体的 JSON 保持干净,不被分页元数据污染——这是与"把 total 塞进 JSON body"方案相比的显著优势。

从源码看,Content-Range头与状态码的生成集中在 RangeQuery.hs 的rangeStatusHeader函数中:

  • 未请求计数(total 为Nothing)时,返回200 OK;
  • 请求了计数且返回行数小于总数时,返回206 Partial Content;
  • 当请求区间下界超出总数时,返回416 Range Not Satisfiable(Content-Range为*/total形式);
  • 返回行数等于总数时,仍为200 OK。

该状态判定逻辑在 test/spec/Feature/Query/RangeSpec.hs 中有完整测试覆盖,例如空结果集返回Content-Range: */0、部分内容返回206 Partial Content等。

二、查询参数方式:limit 与 offset

最简单直接的分页方式是使用查询参数limit和offset。例如,跳过前 30 行、只取接下来的 15 行:

curl "http://localhost:3000/people?limit=15&offset=30"

要点说明:

  • limit控制返回的最大行数,offset控制跳过的行数,二者共同构成"第 N 页"的经典计算方式(offset = (page - 1) * limit)。
  • 这种方式同样适用于嵌入资源(embedded resources),即通过select参数展开的关联表分页,在 Resource Embedding 一节中有更详细的用法。
  • 即使使用查询参数来限制查询,服务器依然会响应 Range 头——Content-Range与Range-Unit: items始终存在,客户端可以统一按头部解析,无需区分请求来源。

从实现上看,limit/offset与Range头在服务端被统一解析为内部区间(NonnegRange)并转换为 SQL 的LIMIT ... OFFSET ...子句,见 SqlFragment.hs 的limitOffsetF:当区间为全量时输出空片段,否则生成LIMIT <n> OFFSET <m>。

三、请求头方式:Range 头

除了查询参数,你也可以直接使用标准 HTTP Range 头来指定期望的行区间。下面这个请求获取前 20 个 people 记录:

curl "http://localhost:3000/people" -i \ -H "Range-Unit: items" \ -H "Range: 0-19"

对应的响应(注意服务器可能返回比你请求的更少的行,因为数据不足或受db-max-rows硬上限约束):

HTTP/1.1 200 OK Range-Unit: items Content-Range: 0-17/*

这里请求0-19,但实际只返回了0-17共 18 行,说明当前数据不足以满足完整请求。

开放区间(open-ended range):你还可以请求只有偏移、没有上限的区间,例如Range: 10-,表示"从第 10 行开始取到末尾"。这在需要"跳过前 N 行、不限制返回条数"的场景非常有用。

从源码看,Range头在 RangeQuery.hs 中通过正则^([0-9]+)-([0-9]*)$解析:上界为空时视为BoundaryAboveAll(即开放上界)。请求头中的Range会被 rangeRequested 提取,查询参数limit/offset则走另一条路径,最终统一收敛为内部区间再生成 SQL。

四、计数:Prefer: count= 请求头

当你需要获取表的总行数(例如渲染分页控件的"最后一页"链接)时,可以通过Prefer: count=<value>请求头来实现。可选值有三个:exact、planned和estimated。

Prefer: count=exact Prefer: count=planned Prefer: count=estimated

计数不仅适用于普通表,也适用于视图(views)以及表函数(table functions,即通过 RPC 调用的返回集合的函数),见 Functions。这也意味着三种计数策略在 Statements.hs 中同时被mainRead(读查询)与mainCall(RPC 调用)两条执行路径复用。

4.1 exact:精确计数

使用Prefer: count=exact会触发 PostgreSQL 对全表执行一次真实的聚合计数(count(*)),因此表越大,这条查询在数据库中运行得越慢。示例如下:

curl "http://localhost:3000/bigtable" -I \ -H "Range-Unit: items" \ -H "Range: 0-24" \ -H "Prefer: count=exact"

服务器会返回所选区间与精确总数(注意此时状态码是206 Partial Content,因为返回行数 25 小于总数 3573458):

HTTP/1.1 206 Partial Content Range-Unit: items Content-Range: 0-24/3573458

精确计数的代价在大表上不可忽视:PostgreSQL 需要扫描(或依赖索引快速路径)以统计全部满足条件的行。从 ApiRequest/Preferences.hs 的源码看,ExactCount是shouldCount为真的两种模式之一,会走真实的count(*)聚合查询,其 SQL 生成逻辑见 SqlFragment.hs 的countF。

4.2 planned:基于统计信息的计划计数

为了规避exact在大表上的性能短板,PostgREST 可以借助 PostgreSQL 的统计信息(即查询规划器估算的行数,来自EXPLAIN的Plan Rows),获得一个相当准确且非常快的计数:

curl "http://localhost:3000/bigtable?limit=25" -I \ -H "Prefer: count=planned"
HTTP/1.1 206 Partial Content Content-Range: 0-24/3572000

注意,该计数的精度取决于 PostgreSQL 统计表的时效性。本例中planned给出 3572000,而精确值是 3573458,误差仅为千分之一量级。如果统计信息过期,误差会明显增大,此时可以运行ANALYZE bigtable让 PostgreSQL 重新收集统计信息以提高准确性(详见 PostgreSQL 的ANALYZE命令文档)。

从源码看,planned计数通过EXPLAIN提取计划行数:在 MainTx.hs 中,decodeExplain从EXPLAIN (FORMAT JSON)的结果中取出Plan Rows字段;而在 Preferences.hs 中shouldExplainCount表明PlannedCount与EstimatedCount都会触发 EXPLAIN。这种方式完全在规划器层面完成,不扫描实际数据,因此速度极快。

4.3 estimated:阈值自适应计数

当你关心计数的相对误差时,问题就出现了:如果planned给出 1000000,而精确值是 1001000,误差可以忽略;但如果planned给出 7,而精确值是 28,这就是一个巨大的误判。一般来说,当行数较小时,估计值应当尽量接近精确值。

PostgREST 的estimated模式正是为这种场景设计:在行数低于阈值时使用精确计数,超过阈值后切换到 planned 计数。这个阈值由db-max-rows配置项定义(详见 configuration.rst 中的 db-max-rows:类型 Int、默认∞、支持热重载,环境变量为PGRST_DB_MAX_ROWS,数据库内配置为pgrst.db_max_rows,同时兼容旧的无前缀写法max-rows;它的本职是限制 PostgREST 从表、视图或函数中获取的行数硬上限,防止意外或恶意请求拉爆 payload)。

假设设置db-max-rows=1000,而smalltable有 321 行,那么estimated会给出精确计数:

curl "http://localhost:3000/smalltable?limit=25" -I \ -H "Prefer: count=estimated"
HTTP/1.1 206 Partial Content Content-Range: 0-24/321

而对拥有 3573458 行的bigtable发出相同请求,则会得到planned 计数:

curl "http://localhost:3000/bigtable?limit=25" -I \ -H "Prefer: count=estimated"
HTTP/1.1 206 Partial Content Content-Range: 0-24/3572000

estimated的底层决策逻辑清晰地体现在 MainTx.hs 中:当preferCount == Just EstimatedCount且真实返回的tableTotal大于configDbMaxRows时,取EXPLAIN的计划行数与实际行数的较大者作为总数;否则直接使用精确计数的结果。与之配套,Statements.hs 会在estimated模式下对计数查询追加LIMIT maxRows + 1,以便在 SQL 层面判定是否超过阈值——这正是"精确到阈值、超过即切换"的实现细节。

五、三种计数模式速查与选型建议

模式请求头计数方式速度精度适用场景
exactPrefer: count=exactPostgreSQL 真实count(*)大表慢完全精确小表、必须精确的分页总数
plannedPrefer: count=plannedEXPLAIN的Plan Rows(基于统计信息)极快依赖统计时效,通常足够准大表、仅需"大致数量"
estimatedPrefer: count=estimated行数 ≤db-max-rows时精确,否则回退 planned自适应小表精确、大表近似对相对误差敏感的通用场景

选型要点:

  • 数据量小、要求绝对精确 →exact;
  • 数据量大、只关心量级(如展示"约 357 万条")→planned;
  • 不确定数据量或希望自动权衡 →estimated,并通过db-max-rows调节切换阈值;同时注意该配置也是行数硬上限,需结合业务合理设置。

另外需注意:在未设置db-max-rows且没有 limit/offset的情况下,PostgREST 会复用页内计数作为总数以避免额外的聚合查询(见 SqlFragment.hs 的注释与实现),这也是理解Content-Range: 0-14/*中*的由来——未请求计数时总数位置即为*。

六、实战组合:完整的分页请求与响应解读

将前文知识组合成一个端到端示例。假设前端要渲染第 2 页(每页 20 条):

curl "http://localhost:3000/people?select=id,name" -i \ -H "Range-Unit: items" \ -H "Range: 20-39" \ -H "Prefer: count=estimated"

可能的响应头:

HTTP/1.1 206 Partial Content Range-Unit: items Content-Range: 20-39/3573458

客户端解析逻辑:

  1. 从Content-Range: 20-39/3573458得到当前页区间20-39与总数3573458;
  2. 计算总页数ceil(3573458 / 20),从而决定是否渲染"最后一页";
  3. 通过修改Range头或limit/offset参数翻页,翻页请求同样携带Prefer: count=以维持总数。

其中状态码206表示"部分内容",200表示"返回了全部结果",416表示"区间越界"(如请求Range: 9999999-且下界超过总数时,Content-Range会形如*/3573458)——这些语义均与 RangeQuery.hs 的实现一一对应,并有 RangeSpec.hs 中的大量断言用例作为行为契约。

七、总结

PostgREST 的分页与计数能力建立在标准 HTTP 语义之上:Range/Content-Range头负责区间传输(RFC 7233 兼容),limit/offset查询参数提供等价的便捷写法,Prefer: count=则提供了三种不同权衡的计数模式——精确、计划与阈值自适应。理解db-max-rows在estimated模式中扮演的阈值角色,以及EXPLAIN Plan Rows与真实count(*)在 Statements.hs、MainTx.hs 中的协同逻辑,可以帮助你在真实业务中精准选型,既保证分页控件的数据正确性,又避免大表精确计数带来的性能灾难。

【免费下载链接】postgrestREST API for any Postgres database项目地址: https://gitcode.com/GitHub_Trending/po/postgrest

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询