PageHelper 分页框架查询总数 SQL 错误解决方案:从源码逻辑到版本影响(含实验验证)
目录
一、问题背景与环境信息
二、问题复现(实验一)
2.1 测试代码
XML 映射文件(SQL 语句)
Java 调用代码
2.2 运行报错与异常日志
2.3 核心疑问
三、问题原因分析(基于源码)
3.1 PageHelper 对ORDER BY的处理逻辑
3.2 PageHelper 生成总条数 SQL 的判断逻辑
3.3 最终错误成因
四、解决方案
4.1 核心原理
4.2 修改后的代码(XML)
五、补充实验与版本影响(实验二)
5.1 实验二:布尔类型ORDER BY的特殊情况
测试 SQL:不再使用case when 来进行排序(布尔排序:指定校区优先降序)
不同版本下的结果对比
5.2 版本影响结论
六、总结
一、问题背景与环境信息
本文针对 PageHelper 分页框架在生成查询总数 SQL 时出现的语法错误问题展开分析,涉及工具及版本信息如下:
- PageHelper 版本:5.1.8(后续补充 6.6.1 版本对比测试)
- JSqlParser 版本:1.2(PageHelper 5.1.x 默认依赖,一个用于解析 SQL 语句的 Java 库;PageHelper 6.6.1 升级为 4.7 版本)
- 数据库:PostgreSQL:11.1
- 核心问题:含特殊
ORDER BY
子句的查询 SQL,生成总数统计 SQL 时未正确处理,导致报 “字段需 GROUP BY” 错误。
二、问题复现(实验一)
2.1 测试代码
XML 映射文件(SQL 语句)
<select id="selectTest" parameterType="com.jiuaoedu.serviceprofile.pojo.student.StudentDetail" resultMap="BaseResultMap">select *from service_profile.student s-- 按“指定校区优先(0)→其他校区(1)”排序,再按校区ID降序,NULL值后置order by CASE WHEN s.school_area = #{schoolArea} THEN 0 ELSE 1 END,s.school_area DESC NULLS LAST
</select>
Java 调用代码
PageInfo<StudentDetail> pageInfo = PageHelper.startPage(pageNum, pageSize).doSelectPageInfo(() -> studentDetailMapper.selectTest(student)
);
2.2 运行报错与异常日志
- 生成的错误总数 SQL:
SELECT count(0) FROM service_profile.student s ORDER BY CASE WHEN s.school_area = ? THEN 0 ELSE 1 END, s.school_area DESC NULLS LAST
- 报错原因:
COUNT(0)
聚合查询中包含ORDER BY
子句,且school_area
未参与GROUP BY
,违反 SQL 语法规则。
2.3 核心疑问
- 正常情况下 PageHelper 会过滤
ORDER BY
子句以提升计数性能,为何本次未过滤? - 为何未生成 “外层
COUNT
嵌套原查询” 的正确 SQL(如下),反而直接将*
替换为count(0)
?sql
SELECT count(0) FROM (select * FROM service_profile.student s ORDER BY ...) tmp_count
三、问题原因分析(基于源码)
PageHelper 分页核心流程分为两步:
1. 查询总条数;
2. 总条数非 0 时执行分页查询。错误根源在于 “生成总条数 SQL” 的逻辑处理。
3.1 PageHelper 对ORDER BY
的处理逻辑
核心源码片段(orderByHashParameters
方法):
- 逻辑结论:若
ORDER BY
子句包含占位符(如实验一中的#{schoolArea}
对应?
),PageHelper 会保留ORDER BY
,不会过滤 —— 这解释了 “疑问 1”。
3.2 PageHelper 生成总条数 SQL 的判断逻辑
核心源码片段(isSimpleCount
方法与sqlToCount
方法):
/*** 判断是否为“简单查询”,决定是否直接替换查询列为count(0)* @param select 简单查询对象(PlainSelect)* @return 是简单查询返回true,否则false*/
public boolean isSimpleCount(PlainSelect select) {// 1. 含GROUP BY → 非简单查询if (select.getGroupByColumnReferences() != null) {return false;}// 2. 含DISTINCT → 非简单查询if (select.getDistinct() != null) {return false;}// 3. SELECT列含占位符 → 非简单查询for (SelectItem item : select.getSelectItems()) {if (item.toString().contains("?")) {return false;}// 4. SELECT列含聚合函数(非允许列表)→ 非简单查询if (item instanceof SelectExpressionItem) {Expression expression = ((SelectExpressionItem) item).getExpression();if (expression instanceof Function) {// 聚合函数(如SUM、AVG)判断逻辑...}}}return true;
}/*** 将原查询SQL转换为总条数SQL*/
public void sqlToCount(Select select, String name) {SelectBody selectBody = select.getSelectBody();List<SelectItem> COUNT_ITEM = new ArrayList<>();COUNT_ITEM.add(new SelectExpressionItem(new Function("count", new Column(name))));// 若为简单查询,直接替换SELECT列为count(0)if (selectBody instanceof PlainSelect && isSimpleCount((PlainSelect) selectBody)) {((PlainSelect) selectBody).setSelectItems(COUNT_ITEM);} else {// 非简单查询:生成“外层COUNT嵌套原查询”的SQLPlainSelect plainSelect = new PlainSelect();SubSelect subSelect = new SubSelect();subSelect.setSelectBody(selectBody);subSelect.setAlias("tmp_count");plainSelect.setFromItem(subSelect);plainSelect.setSelectItems(COUNT_ITEM);select.setSelectBody(plainSelect);}
}
-
包含GROUP BY子句:因为GROUP BY会使结果聚合,不再是简单计数
-
包含DISTINCT关键字:DISTINCT会去重,影响计数结果
-
SELECT列表中包含参数(用?表示):参数可能会导致执行计划不稳定
-
SELECT列表中包含聚合函数(如SUM、AVG等):这些函数会改变计数逻辑
实验截图:
如果该查询是一个简单的查询就将sql的查询列重置为count(0)
- 逻辑结论:实验一中的原查询满足 “简单查询” 条件(无
GROUP BY
、DISTINCT
,SELECT
列仅为*
不含占位符 / 聚合函数),因此 PageHelper 直接将*
替换为count(0)
,未生成嵌套查询 —— 这解释了 “疑问 2”。
3.3 最终错误成因
保留ORDER BY
(因含占位符)+ 简单查询直接替换count(0)
,两者叠加生成了 “COUNT
+ORDER BY
” 的错误 SQL。
四、解决方案
4.1 核心原理
PageHelper 源码中存在特殊注释标识/*keep orderby*/
,若 SQL 中包含该注释,会强制生成 “外层COUNT
嵌套原查询” 的 SQL(跳过直接替换逻辑),避免错误。
对应源码片段(getSmartCountSql
方法):
public String getSmartCountSql(String sql, String name) {// 若SQL含/*keep orderby*/,直接生成嵌套COUNT查询if (sql.indexOf("/*keep orderby*/") >= 0) {return getSimpleCountSql(sql, name);}// 其他解析逻辑...
}/*** 生成“外层COUNT嵌套原查询”的SQL*/
public String getSimpleCountSql(final String sql, String name) {StringBuilder sb = new StringBuilder(sql.length() + 40);sb.append("select count(").append(name).append(") from (");sb.append(sql);sb.append(") tmp_count");return sb.toString();
}
4.2 修改后的代码(XML)
<select id="selectTest" parameterType="com.jiuaoedu.serviceprofile.pojo.student.StudentDetail" resultMap="BaseResultMap">select *from service_profile.student s/*keep orderby*/ -- 关键注释:强制生成嵌套COUNT查询order by CASE WHEN s.school_area = #{schoolArea} THEN 0 ELSE 1 END,s.school_area DESC NULLS LAST
</select>
实验截图:
- 该 SQL 符合语法规则,可正常执行计数,分页功能恢复正常。
五、补充实验与版本影响(实验二)
5.1 实验二:布尔类型ORDER BY
的特殊情况
测试 SQL:不再使用case when 来进行排序(布尔排序:指定校区优先降序)
<select id="selectTest" parameterType="com.jiuaoedu.serviceprofile.pojo.student.StudentDetail" resultMap="BaseResultMap">select *from service_profile.student sorder by s.school_area = #{schoolArea} desc nulls last
</select>
按照之前的分析结果来看应该会报错,并且生成的sql应该如下
select count(0) from service_profile.student sorder by s.school_area = ? desc nulls last
按照之前的分析结果来看应该会报错,并且生成的sql应该如下
select count(0) from service_profile.student sorder by s.school_area = ? desc nulls last
但是事实却是查询正确,生成的查询总条数sql如下:
select count(0) from (select * from service_profile.student s order by s.school_area = ? desc nulls last) tmp_count
到这儿我就懵了,不应该如此啊,接着debug。
看到这里我就知道了,PageHelper中引入的jsqlparser较低,jsqlparser解析不了该sql报错,然后就直接返回了simpleCountSql
我升级了pageHelper版本至最新版本6.6.1再次尝试上诉所有内容:
实验结果:
实验1:跟第一次没升级版本出现的报错一样
实验2:第一次没升级不会报错,正常分页查询。在升级版本之后,却出现了报错,原因是因为pageHelper6.6.1中jsqlparser升级为了4.7能够正常解析实验2的结果,然后就出现了和实验1一样的报错。
不同版本下的结果对比
PageHelper 版本 | JSqlParser 版本 | 执行结果 | 原因分析 |
---|---|---|---|
5.1.8 | 1.2 | 正常计数 | JSqlParser 1.2 无法解析 “布尔排序” SQL,解析报错后触发降级逻辑,自动生成嵌套 COUNT 查询 |
6.6.1 | 4.7 | 报错(同实验一) | JSqlParser 4.7 可正常解析 “布尔排序” SQL,进入 “简单查询 + 保留 ORDER BY” 逻辑,生成错误 SQL |
5.2 版本影响结论
- PageHelper 5.1.8(低版本 JSqlParser):部分复杂
ORDER BY
因解析失败,可能 “意外正常”,但稳定性差。 - PageHelper 6.6.1(高版本 JSqlParser):解析能力增强,更多
ORDER BY
会被保留,需主动添加/*keep orderby*/
避免错误。
六、总结
- 错误根源:含占位符的
ORDER BY
被保留 + 简单查询直接替换count(0)
,导致 SQL 语法错误。 - 通用解决方案:在含特殊
ORDER BY
(含占位符、布尔排序等)的查询 SQL 中,添加/*keep orderby*/
注释,强制生成嵌套 COUNT 查询。 - 版本建议:升级 PageHelper 后需重点检查
ORDER BY
相关查询,确保添加该注释,避免因 JSqlParser 解析能力提升导致新错误。