iBatis (現 MyBatis 前身) 提供的 executeQueryForList(statement, params, skipResults, maxResults) 看似方便,實則**在 JDBC 層獲取全量 ResultSet 後,在 Java 內存中跳過前 skipResults 條,再取 maxResults 條**。
<settings>
<setting name="lazyLoadingEnabled" value="false"/>
<setting name="cacheModelsEnabled" value="false"/>
<setting name="enhancementEnabled" value="false"/>
<setting name="maxRequests" value="512"/>
<setting name="maxSessions" value="128"/>
<setting name="maxTransactions" value="32"/>
</settings>
這僅禁用 iBatis 二級緩存,**不改變分頁時全量獲取 ResultSet 的本質**。
利用數據庫原生分頁語法,僅讓 DB 返回當前頁數據,極大降低內存、網絡、CPU 開銷。
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (
SELECT * FROM orders WHERE status = 'PAID' ORDER BY create_time DESC
) a WHERE ROWNUM <= :endRow -- endRow = pageNum * pageSize
) WHERE rn > :startRow; -- startRow = (pageNum - 1) * pageSize
ROWNUM 在 ORDER BY **之前**賦值,故必須雙層嵌套保證排序正確。SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
OFFSET :startRow ROWS FETCH NEXT :pageSize ROWS ONLY;
標準 SQL:2008 語法,優化器原生支持,計劃更優。
SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
LIMIT :startRow, :pageSize; -- LIMIT offset, row_count
LIMIT 1000000, 20 仍需掃描前 100 萬行。優化:使用延遲關聯或書籤記錄上一頁最大 ID:WHERE id > :lastMaxId ORDER BY id LIMIT 20。SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
OFFSET @startRow ROWS FETCH NEXT @pageSize ROWS ONLY;
SELECT * FROM orders
WHERE status = 'PAID'
ORDER BY create_time DESC
LIMIT :pageSize OFFSET :startRow;
<select id="selectOrdersByPage" resultType="Order">
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (
SELECT * FROM orders
WHERE status = #{status}
ORDER BY create_time DESC
) a WHERE ROWNUM <= #{endRow}
) WHERE rn > #{startRow}
</select>
Map params = new HashMap<>();
params.put("status", "PAID");
params.put("startRow", (pageNum - 1) * pageSize);
params.put("endRow", pageNum * pageSize);
List list = sqlMap.queryForList("selectOrdersByPage", params);
<select id="countOrders" resultType="int">
SELECT COUNT(*) FROM orders WHERE status = #{status}
</select>
public class PageContext {
private static final ThreadLocal TL = new ThreadLocal<>();
public static void setPage(int pageNum, int pageSize) {
TL.set(new Page(pageNum, pageSize));
}
public static Page getPage() { return TL.get(); }
public static void clear() { TL.remove(); }
public static class Page {
public final int startRow, endRow, pageSize;
public Page(int pageNum, int pageSize) {
this.pageSize = pageSize;
this.startRow = (pageNum - 1) * pageSize;
this.endRow = pageNum * pageSize;
}
}
}
// MyBatis 攔截器自動攔截並注入分頁 SQL (生產環境推薦 PageHelper)
public class PaginationInterceptor implements Interceptor {
@Override public Object intercept(Invocation invocation) throws Throwable {
Page page = PageContext.getPage();
if (page != null) {
// 修改 BoundSql 注入 LIMIT/OFFSET 或 ROWNUM
}
return invocation.proceed();
}
}
skipResults/maxResults 內存分頁。PageHelper 攔截器自動改寫 SQL;Hibernate 使用 setFirstResult/setMaxResults (底層同樣依賴方言原生分頁)。COUNT(*) 查詢,可緩存或異步執行。將分頁責任交給數據庫,是處理海量數據分頁的**唯一正確架構決策**。