在 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 BYOFFSET .. 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
SELECTORDER BYDISTINCT*FIRST_NAMELAST_NAMEACTOR_IDORDER BYuTOTAL_ROWSCOUNT(*) OVER ()ROWROW_NUMBER () OVER (ORDER BY t.actor_id)COUNT(*) OVER ()OVER ()FROMWHEREGROUP BYHAVINGuROW_NUMBER () OVER (ORDER BY u.actor_id)uu.actor_idu.*uOFFSET .. FETCHOFFSETROW015151OFFSET0COUNT(*) OVER ()FROMWHEREGROUP BYHAVINGtMAX_PAGE_SIZEFETCHLIMITtACTUAL_PAGE_SIZEMAX(row) OVER () = total_rowsrowtLAST_PAGEACTUAL_PAGE_SIZE < MAX_PAGE_SIZECOUNT(*) OVER () < :MAX_PAGE_SIZEFIRST_NAMELAST_NAME*ROW / TOTAL_ROWSTOTAL_ROWS / MAX_PAGE_SIZETOTAL_PAGESORDER BY t.actor_idORDER BYDISTINCTUNIONJOINORDER BY我们完成了!
这是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"total_rows"); Field .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