)
目录一、问题背景与环境信息二、问题复现实验一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.2PageHelper 5.1.x 默认依赖一个用于解析 SQL 语句的 Java 库PageHelper 6.6.1 升级为 4.7 版本数据库PostgreSQL11.1核心问题含特殊ORDER BY子句的查询 SQL生成总数统计 SQL 时未正确处理导致报 “字段需 GROUP BY” 错误。该问题已被 PageHelper 采纳并修复详情可以看我提的PRGitHub PR地址二、问题复现实验一2.1 测试代码XML 映射文件SQL 语句select idselectTest parameterTypecom.jiuaoedu.serviceprofile.pojo.student.StudentDetail resultMapBaseResultMap 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 /selectJava 调用代码PageInfoStudentDetail pageInfo PageHelper.startPage(pageNum, pageSize).doSelectPageInfo( () - studentDetailMapper.selectTest(student) );2.2 运行报错与异常日志生成的错误总数 SQLSELECT 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)sqlSELECT 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(); ListSelectItem 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嵌套原查询”的SQL PlainSelect 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、DISTINCTSELECT列仅为*不含占位符 / 聚合函数因此 PageHelper 直接将*替换为count(0)未生成嵌套查询 —— 这解释了 “疑问 2”。3.3 最终错误成因保留ORDER BY因含占位符 简单查询直接替换count(0)两者叠加生成了 “COUNTORDER 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 修改后的代码XMLselect idselectTest parameterTypecom.jiuaoedu.serviceprofile.pojo.student.StudentDetail resultMapBaseResultMap 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 idselectTest parameterTypecom.jiuaoedu.serviceprofile.pojo.student.StudentDetail resultMapBaseResultMap select * from service_profile.student s order by s.school_area #{schoolArea} desc nulls last /select按照之前的分析结果来看应该会报错并且生成的sql应该如下select count(0) from service_profile.student s order by s.school_area ? desc nulls last按照之前的分析结果来看应该会报错并且生成的sql应该如下select count(0) from service_profile.student s order 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.81.2正常计数JSqlParser 1.2 无法解析 “布尔排序” SQL解析报错后触发降级逻辑自动生成嵌套 COUNT 查询6.6.14.7报错同实验一JSqlParser 4.7 可正常解析 “布尔排序” SQL进入 “简单查询 保留 ORDER BY” 逻辑生成错误 SQL5.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 解析能力提升导致新错误。