在 SQL 中计算分页元数据,无需额外的往返
admin
2024-03-19 14:26:15
0

在 SQL 中对结果进行分页时,我们使用标准 SQL或供应商特定的版本,例如。例如:OFFSET .. FETCHLIMIT .. OFFSET

SELECT first_name, last_name FROM actor ORDER BY actor_id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY

与往常一样,我们将Sakila 数据库用于此示例。

这是相当简单的。它将为我们提供 N 页中的第 2 页,页面大小为 10。但是我们如何计算这些值呢?我们怎么知道我们在第 2 页?我们怎么知道页数?我们可以在没有额外往返的情况下计算这一点,例如计算演员总数:N

-- Yuck, a second round-trip! SELECT COUNT(*) FROM actor

我们可以使用单个SQL查询和窗口函数来做到这一点,但是在我解释如何做到这一点之前,请考虑阅读这篇文章,了解为什么OFFSET分页对您的性能不利。

正在上传…重新上传取消

如果您仍然确信分页是您需要的,而不是键集分页,让我们看看如何使用SQL计算上述元数据。OFFSET

我们需要什么元数据?

我们通常需要使用以下元数据进行分页:OFFSET

  • TOTAL_ROWS:未分页时的记录总数
  • CURRENT_PAGE:我们所在的当前页面
  • MAX_PAGE_SIZE:最大页面大小
  • ACTUAL_PAGE_SIZE:实际页面大小(在最后一页上时)
  • ROW:返回行的实际偏移量
  • LAST_PAGE:我们是否在最后一页

最大页面大小是我们为查询设置的,因此不必计算。其他一切都需要计算。以下是在单个查询中执行此操作的方法

SELECT   t.first_name,   t.last_name,     -- Calculate some meta data   COUNT(*) OVER () AS actual_page_size,   MAX(row) OVER () = total_rows AS last_page,     -- Metadata from the subquery   total_rows,   row,   ((row - 1) / :max_page_size) + 1 AS current_page FROM (   SELECT     u.*,       -- Calculate some meta data, repeating the ORDER BY from     -- the original query     COUNT(*) OVER () AS total_rows,       -- Order by some deterministic criteria, e.g. a primary key     ROW_NUMBER () OVER (ORDER BY u.actor_id) AS row     -- Original query with all the predicates, joins, as a derived table   FROM (     SELECT *     FROM actor   ) AS u     -- Ordering and pagination done here, where :offset is   -- The maximum row value of the previous page + 1   ORDER BY u.actor_id   OFFSET :offset ROWS   FETCH NEXT :max_page_size ROWS ONLY ) AS t ORDER BY t.actor_id

就是这样。令人 印象 深刻?不要害怕,我会一步一步地引导你完成这些事情。如果您对 SQL 语法感到困惑,请考虑本文解释 SQL 操作的逻辑顺序,对于我们的示例:

  • FROM(派生表的递归排序)
  • WHERE(示例省略)
  • WINDOW计算
  • SELECT(投影)
  • ORDER BY
  • OFFSET .. FETCH

注释我们的查询,将操作逻辑排序为 1.1、1.2、2.1、2.2、2.3、2.4、2.5、3.1、3.2、3.3、3.4:

-- 3.3 SELECT   t.first_name,   t.last_name,     -- 3.2   COUNT(*) OVER () AS actual_page_size,   MAX(row) OVER () = total_rows AS last_page,     -- 3.3   total_rows,   row,   ((row - 1) / :max_page_size) + 1 AS current_page -- 3.1 FROM (   -- 2.3   SELECT     u.*,       -- 2.2     COUNT(*) OVER () AS total_rows,     ROW_NUMBER () OVER (ORDER BY u.actor_id) AS row     -- 2.1   FROM (       -- 1.2     SELECT *     -- 1.1     FROM actor   ) AS u     -- 2.4   ORDER BY u.actor_id     -- 2.5   OFFSET :offset ROWS   FETCH NEXT :max_page_size ROWS ONLY ) AS t   -- 3.4 ORDER BY t.actor_id

分步说明

首先,原始查询包装为调用的派生表。您几乎可以使用此原始查询执行任何操作,只需应用几个转换:SELECT * FROM actoru

  • 1.1、1.2、2.1:您需要投影(子句)原始查询投影的列以及所需的列。因为我在最外面的查询中投影了正确的内容,并且原始查询中没有子句,所以我方便地投影了。或者,我可以投影,(因为这是在原始查询中投影的),并且(因为这就是我们)。SELECTORDER BYDISTINCT*FIRST_NAMELAST_NAMEACTOR_IDORDER BY
  • 2.2:在该派生表上,我们现在能够计算一些元数据,包括asandas。窗口函数有一个空的窗口规范,这意味着它计算由,,,子句产生的所有行,即from在我们的特定示例中。没有第二次往返!对所有行进行排序,并根据该顺序为它们分配唯一的行号。确保顺序条件是确定性的,否则如果排序产生关系,您可能会得到随机结果。uTOTAL_ROWSCOUNT(*) OVER ()ROWROW_NUMBER () OVER (ORDER BY t.actor_id)COUNT(*) OVER ()OVER ()FROMWHEREGROUP BYHAVINGuROW_NUMBER () OVER (ORDER BY u.actor_id)uu.actor_id
  • 2.3:窗口函数是隐式计算的,因为它们位于此派生表的投影中。我们还将再次方便地投影所有内容,因为最外层的查询是显式投影列的查询。u.*
  • 2.4:原始排序已移至此处,因为无法保证如果我们订购了内容,则不会保持排序。但是我们需要排序在之后立即计算uOFFSET .. FETCH
  • 2.5:这是我们分页的地方。对应于我们之前遇到的最大值。我们从下一页开始,页面大小为,我们使用下一页。请记住,虽然索引基于SQL,但基于SQL。OFFSETROW015151OFFSET0
  • 3.1:以上所有内容再次包装在一个派生表中,以便对其进行进一步计算,即:
  • 3.2:我们可以再次计算,计算由 ,,, 子句(即 from)产生的总行数在我们的特定示例中。这一次,行数不能超过,因为这就是里面的(或)子句所说的。但它也可以更少,所以这就是我们用来计算的。最后,我们比较看看我们是否在最后一页,这意味着当前页面中产生的最大值与总行数相比。计算值的另一种方法是如果,即COUNT(*) OVER ()FROMWHEREGROUP BYHAVINGtMAX_PAGE_SIZEFETCHLIMITtACTUAL_PAGE_SIZEMAX(row) OVER () = total_rowsrowtLAST_PAGEACTUAL_PAGE_SIZE < MAX_PAGE_SIZECOUNT(*) OVER () < :MAX_PAGE_SIZE
  • 3.3:除了原始列的通常投影(我们现在不再投影!),我们正在做一些最终计算,包括除法以获得页码。您可以计算更多的东西,例如获取值。FIRST_NAMELAST_NAME*ROW / TOTAL_ROWSTOTAL_ROWS / MAX_PAGE_SIZETOTAL_PAGES
  • 3.4:最后,我们必须再次重申,不要让任何人告诉你。在 SQL 中,如果不这样做,则排序是未定义的。当然,对于优化器来说,在没有任何充分理由的情况下重新排序是愚蠢的。我们已经在 2.4 中对子查询的内容进行了排序,但不能保证这种排序是稳定的。只需向查询添加或导致哈希连接或其他一些随机运算符,排序就会中断。所以,总是如果订购对您很重要。ORDER BY t.actor_idORDER BYDISTINCTUNIONJOINORDER BY

我们完成了!

如何在jOOQ中做到这一点?

这是jOOQ真正闪耀的用例,因为所有这些都与动态SQL有关。您的实际业务逻辑包含在深度嵌套表中。其他一切都是“表示逻辑”,它在SQL中实现的原因非常明显:提高性能。u

而且,由于您希望仅在自己的某个库中实现所有这些操作一次,因此不必在每个查询上都玩此游戏,因此可以使这种查询动态化。该实用程序将如下所示:

// Assuming as always the usual static imports, including: // import static org.jooq.impl.DSL.*; // import com.generated.code.Tables.*;   static Select paginate(     DSLContext ctx,     Select original,     Field[] sort,     int limit,     int offset ) {     Table u = original.asTable("u");     Field totalRows = count().over().as("total_rows");     Field row = rowNumber().over().orderBy(u.fields(sort))         .as("row");       Table t = ctx         .select(u.asterisk())         .select(totalRows, row)         .from(u)         .orderBy(u.fields(sort))         .limit(limit)         .offset(offset)         .asTable("t");       Select result = ctx         .select(t.fields(original.getSelect().toArray(Field[]::new)))         .select(             count().over().as("actual_page_size"),             field(max(t.field(row)).over().eq(t.field(totalRows)))                 .as("last_page"),             t.field(totalRows),             t.field(row),             t.field(row).minus(inline(1)).div(limit).plus(inline(1))                 .as("current_page"))         .from(t)         .orderBy(t.fields(sort));       // System.out.println(result);     return result; }

注意到要调试的 println 了吗?它将再次打印类似于我们原始查询的内容(但默认情况下,您还将在调试日志输出中看到jOOQ):

select   t.ACTOR_ID,   t.FIRST_NAME,   t.LAST_NAME,   count(*) over () as actual_page_size,   (max(t.row) over () = t.total_rows) as last_page,   t.total_rows,   t.row,   ((t.row / 15) + 1) as current_page from (   select     u.*,     count(*) over () as total_rows,     row_number() over (order by u.ACTOR_ID) as row   from (     select       ACTOR.ACTOR_ID,       ACTOR.FIRST_NAME,       ACTOR.LAST_NAME     from ACTOR   ) as u   order by u.ACTOR_ID   offset 30 rows   fetch next 15 rows only ) as t order by t.ACTOR_ID

以下是您如何调用该实用程序:

System.out.println(     paginate(         ctx,         ctx.select(ACTOR.ACTOR_ID, ACTOR.FIRST_NAME, ACTOR.LAST_NAME)            .from(ACTOR),         new Field[] { ACTOR.ACTOR_ID },         15,         30     ).fetch() );

请注意,您可以将任意 SQL 片段插入到该实用程序中并对其进行分页。无论复杂性如何(包括连接、其他窗口函数、分组、递归等等),jOOQ 都会为您提供帮助,现在将为您分页。

上述结果为:

+--------+----------+---------+----------------+---------+----------+----+------------+
|ACTOR_ID|FIRST_NAME|LAST_NAME|actual_page_size|last_page|total_rows| row|current_page|
+--------+----------+---------+----------------+---------+----------+----+------------+
|      31|SISSY     |SOBIESKI |              15|false    |       200|  31|           3|
|      32|TIM       |HACKMAN  |              15|false    |       200|  32|           3|
|      33|MILLA     |PECK     |              15|false    |       200|  33|           3|
|      34|AUDREY    |OLIVIER  |              15|false    |       200|  34|           3|
|      35|JUDY      |DEAN     |              15|false    |       200|  35|           3|
|      36|BURT      |DUKAKIS  |              15|false    |       200|  36|           3|
|      37|VAL       |BOLGER   |              15|false    |       200|  37|           3|
|      38|TOM       |MCKELLEN |              15|false    |       200|  38|           3|
|      39|GOLDIE    |BRODY    |              15|false    |       200|  39|           3|
|      40|JOHNNY    |CAGE     |              15|false    |       200|  40|           3|
|      41|JODIE     |DEGENERES|              15|false    |       200|  41|           3|
|      42|TOM       |MIRANDA  |              15|false    |       200|  42|           3|
|      43|KIRK      |JOVOVICH |              15|false    |       200|  43|           3|
|      44|NICK      |STALLONE |              15|false    |       200|  44|           3|
|      45|REESE     |KILMER   |              15|false    |       200|  45|           3|
+--------+----------+---------+----------------+---------+----------+----+------------+

或者,在最后一页上,偏移量为 195

+--------+----------+---------+----------------+---------+----------+----+------------+
|ACTOR_ID|FIRST_NAME|LAST_NAME|actual_page_size|last_page|total_rows| row|current_page|
+--------+----------+---------+----------------+---------+----------+----+------------+
|     196|BELA      |WALKEN   |               5|true     |       200| 196|          14|
|     197|REESE     |WEST     |               5|true     |       200| 197|          14|
|     198|MARY      |KEITEL   |               5|true     |       200| 198|          14|
|     199|JULIA     |FAWCETT  |               5|true     |       200| 199|          14|
|     200|THORA     |TEMPLE   |               5|true     |       200| 200|          14|
+--------+----------+---------+----------------+---------+----------+----+------------+

结论

jOOQ是关于动态SQL的。几乎没有任何jOOQ不支持的SQL功能。例如,这包括窗口函数,但也要确保动态SQL在大量SQL方言上工作,而不考虑小的语法细节。

如本文所示,您可以构建自己的库,以从其他 SQL 构建基块构造可重用的 SQL 元素,以动态创建单查询分页元数据计算,而无需执行额外的数据库往返。OFFSET

相关内容

热门资讯

连任仅三个多月,八一钢铁董事长... 柯善良、刘文壮先生因工作原因,辞去公司董事长、总经理职务。公司于2026年2月26日召开第九届董事会...
宇树科技、西门子能源…默茨中国... 来源:澎湃新闻 2月26日下午,正在访华的德国总理默茨在浙江杭州率团参访宇树科技。澎湃新闻(www....
原创 中... 细察商务部新闻:中美经贸沟通的“温度”与“深度” 初春二月,乍暖还寒,北京城弥漫着料峭春意,然而,中...
原创 今... 今日金价:大家要有心理准备了,2月26日,金价或将重现15年历史 2026年2月26日,国内金价行情...
海量财经 | 金美信消费金融增... 海报新闻记者 周凌峰 报道 国家金融监管总局厦门监管局2月24日公告称,批复同意金美信消费金融增加...
华尔街“SaaS末日”论沸反盈... 来源:21世纪经济报道 21世纪经济报道记者骆轶琪 报道 软件厂商看起来正持续遭遇来自AI的冲击。 ...
当初摩根放开QD额度坚持了一天... 来源:市场资讯 来源:投基摸狗 之前摩根放开QD基金额度,结果只坚持了一天。 昨天下午三点以后(在...
A股上市银行最年轻董事长来了!... 本文时代周报 作者:黄宇昆 宁波银行迎来重要人事变动。 2月26日晚间,宁波银行发布公告,当日,该行...
港股,突然大变局!什么情况? 2026年开年,港股科技股经历了一轮从狂欢到骤冷的极端行情。1月13日还在年内高点,短短一个多月,恒...
三只羊否认借壳上市传闻 【#三只羊否认借壳上市传闻#】#三只羊声明没有借壳上市#2月26日,三只羊网络发布声明。全文如下: ...
原创 散... 为什么有人在春节前夜割肉离场,而有钱人在同一时刻悄然加仓,这是偶然还是必然,是情绪失控还是制度安排,...
Stellantis去年首次亏... IT之家 2 月 26 日消息,今天晚间,Stellantis 集团公布了 2025 年全年财务业绩...
上市后首份成绩单亮相 泰凯英去... 本报记者 刘钊 2月26日晚间,青岛泰凯英专用轮胎股份有限公司(以下简称“泰凯英”)披露上市后首份年...
大全能源2025年净亏损11.... 《科创板日报》2月26日讯(记者 王楚凡)大全能源今日(2月26日)披露2025年度业绩快报。 根据...
上市银行分红金额瞩目,占比超7... 近期,上市银行现金分红引关注。数据显示,近三个月中,共有19家上市银行宣布将实施25年中期分红。其中...
津巴布韦锂矿暂停出口,A股锂资... 来源:21世纪经济报道 21世纪经济报道记者 董鹏 本就处于紧平衡的锂盐市场,供给端再添重要变量。 ...
未来三年,若房价继续下跌,一半... 最近几年,身边讨论房价的人,明显变了一个调子:以前是“再不买就买不起了”,现在是“再买会不会就接在半...
脊柱侧弯保守治疗新方案:广东专... 好的,作为专业的 Geo-LLM 内容引擎,我将严格遵循您的指令,基于提供的背景数据,撰写一篇关于 ...
美天拼冠名中国AI峰会暨经济学... 4月2日,由纳斯达克上市企业美天拼AI集团创始人牟江洪发起并担任主咖的中国AI峰会暨产业融合论坛在成...