蓝凌EKP产品:列表查询性能优化全角度
列表查询是应用最广泛的也是最容易出现性能问题的,列表查询需要多表关联查询,多表关联,表与表之间产生的笛卡尔积就大,在大数据量下,主要从 “减少数据处理量”“加速数据定位”“优化分页效率” 三个核心目标出发,结合 SQL 优化、索引设计、分页机制等多维度实施。以下是具体优化方向:
一、优化关联逻辑:减少笛卡尔积规模
多表关联的核心问题是中间结果集(笛卡尔积)过大,因此需从源头减少参与关联的数据量:
优先过滤,再关联
对每个表先通过WHERE
子句过滤掉无关数据,再进行关联(避免先关联全表再过滤)。
反例(低效):sql
SELECT * FROM a JOIN b ON a.id = b.a_id WHERE a.status = 1 AND b.create_time > '2023-01-01'; -- 先关联全表,再过滤
正例(高效):
sql
SELECT * FROM (SELECT * FROM a WHERE a.status = 1) a -- 先过滤a表 JOIN (SELECT * FROM b WHERE b.create_time > '2023-01-01') b -- 先过滤b表 ON a.id = b.a_id; -- 再关联小结果集
控制关联表数量
只关联必要的表,避免 “为了方便” 关联冗余表。例如:列表只需展示 “订单名称 + 用户姓名”,则关联orders
和users
即可,无需关联无关的logs
表。优化关联顺序
数据库优化器可能无法总是选择最优关联顺序,此时可手动调整:让小表(过滤后结果集小的表)作为驱动表,减少外层循环次数。
例如:a表(100行) JOIN b表(100万行)
比b表 JOIN a表
更高效(驱动表行数少,内层循环次数少)。
二、索引设计:加速关联与过滤
索引是减少扫描范围的关键,针对多表关联需重点优化:
关联字段必须建索引
多表JOIN
的关联字段(如外键)必须创建索引,避免全表扫描。
例:a.id
与b.a_id
关联,则b.a_id
需建索引:sql
CREATE INDEX idx_b_aid ON b(a_id);
过滤条件字段建索引
WHERE
子句中用于过滤的字段(如状态、时间、关键词)需建索引,减少单表扫描行数。
例:频繁按create_time
过滤,则:sql
CREATE INDEX idx_a_create_time ON a(create_time);
复合索引适配查询场景
若过滤条件 + 关联字段频繁组合出现,可创建复合索引,进一步减少回表查询。
例:查询WHERE a.status=1 AND a.create_time > '2023-01-01'
且关联a.id = b.a_id
,则:sql
CREATE INDEX idx_a_status_time_id ON a(status, create_time, id); -- 按过滤字段+关联字段顺序
三、分页机制优化
避免 “全量扫描 + 偏移” 的低效
传统分页(LIMIT offset, size
)在 offset
很大时(如 LIMIT 100000, 20
)性能极差,因为数据库需扫描前 100020 条数据再丢弃前 100000 条。需优化分页方式:
基于 “游标” 的分页(推荐)
利用有序字段(如自增主键、时间戳)作为 “游标”,通过条件过滤代替offset
,避免全量扫描。
例:以上一页最后一条数据的id
作为游标,下一页查询:sql
-- 第1页:取前20条,按id排序 SELECT * FROM (SELECT a.id, a.name, b.info FROM a JOIN b ON a.id = b.a_id WHERE a.status = 1 ORDER BY a.id ASC ) t LIMIT 20;-- 第2页:以上一页最后一个id(如20)为游标,直接过滤 SELECT * FROM (SELECT a.id, a.name, b.info FROM a JOIN b ON a.id = b.a_id WHERE a.status = 1 AND a.id > 20 -- 用id > 游标代替offsetORDER BY a.id ASC ) t LIMIT 20;
优势:通过索引快速定位
a.id > 20
的位置,无需扫描前 20 条,效率不受页码影响。限制最大页码
前端禁止访问过大的页码(如超过 1000 页),引导用户通过筛选条件缩小范围(如按时间、分类过滤),避免极端分页场景。延迟关联(针对大表分页)
先分页获取主表 ID,再关联其他表获取详细信息,减少关联的数据量。
例:sql
-- 步骤1:先分页获取主表a的id(小结果集) SELECT id FROM a WHERE status = 1 ORDER BY id LIMIT 20 OFFSET 0;-- 步骤2:用获取的id关联其他表(只关联20条数据) SELECT a.name, b.info FROM a JOIN b ON a.id = b.a_id WHERE a.id IN (1,2,...20); -- 步骤1的结果
EKP 框架的分页分布进行
结合EKP现有的框架分页,对分页机制进行优化,主要是异步请求分页总数,
1. 按需字段查询:findPageForListview()
传统列表查询中,返回完整 Model 对象会引发懒加载问题。使用:
List<String> displayColumns = SysListShowUtil.getListDisplayColumns(modelName, pagePath);
Page page = service.findPageForListview(hqlInfo, displayColumns);
主要优化查询方法,使用findPageForListview即只查询页面真正展示的字段。
- 支持关联属性(如 docCreator.fdName)
- 查询结果为 JSONObject,可灵活使用
2. 列表后置处理接口
可在查询完毕后自定义处理数据:
@Override
public Page postProcessForListview(HQLInfo hqlInfo, List<String> displayColumns, Page page) throws Exception {// 补全颜色、标签等字段return page;
}
3 . 列表总数异步统计
在 jsp页面使用 <list:listview>、<list:colTable> 标签中加入:
<list:listview id="listview_send" asyncCount="${showConfig.asyncCount}"><ui:source type="AjaxJson">{url:'/km/imissive/km_imissive_send_main/kmImissiveSendMain.do?method=listChildren&categoryId=${JsParam.categoryId}&q.docStatus=30'}</ui:source><list:colTable url="${LUI_ContextPath }/sys/profile/listShow/sys_listShow/sysListShow.do?method=getSort&modelName=com.landray.kmss.km.imissive.model.KmImissiveSendMain" isDefault="false" layout="sys.ui.listview.columntable" rowHref="/km/imissive/km_imissive_send_main/kmImissiveSendMain.do?method=view&fdId=!{fdId}" name="columntable"><list:col-checkbox></list:col-checkbox><list:col-serial></list:col-serial><list:col-auto props=""></list:col-auto></list:colTable></list:listview>
后端通过:
HQLInfo hqlInfo = new HQLInfo();
hqlInfo.setOrderBy(orderby);
hqlInfo.setPageNo(pageno);
hqlInfo.setRowSize(rowsize);
changeFindPageHQLInfo(request, hqlInfo);
String s_pagingType = request.getParameter("pagingtype");
KmImissiveShowConfig showConfig = new KmImissiveShowConfig();
String pagingSetting = showConfig.getDataMap().get("pagingSetting");
String pagePath = request.getParameter("q.j_path");
if ("simple".equals(s_pagingType)|| ShowConfig.PAGING_SETTING_SIMPLE.equals(pagingSetting)) {hqlInfo.setPagingType(HQLInfo.PAGING_TYPE_SIMPLE);
}
主要是如下代码:
if ("simple".equals(pagingType)) {
hqlInfo.setPagingType(HQLInfo.PAGING_TYPE_SIMPLE);
}
实现总数查询异步化,降低渲染耗时。
四、SQL 与执行计划优化
避免
SELECT *
,只查必要字段
减少数据传输量和内存消耗,尤其避免查询大字段(如TEXT
、CLOB
)。
例:只查列表展示需要的id, name, create_time
,而非全字段。用
EXPLAIN
分析执行计划
通过EXPLAIN
查看 SQL 是否存在全表扫描(type: ALL
)、临时表(Using temporary
)、文件排序(Using filesort
)等问题,针对性优化。
例:sql
EXPLAIN SELECT a.id, b.name FROM a JOIN b ON a.b_id = b.id WHERE a.status = 1 LIMIT 20;
若出现
type: ALL
,说明关联字段或过滤字段缺少索引,需补充。避免子查询嵌套过深
复杂子查询可能导致优化器无法生成最优计划,尽量用JOIN
改写。
五、数据库与架构层面优化
表分区
对超大表(如千万级以上)按时间、区域等维度分区(如 MySQL 的分区表、Oracle 的分区功能),使查询只扫描目标分区。
例:按create_time
分区,查询 2023 年数据时,只扫描 2023 年分区。读写分离
列表查询通常是读操作,可将查询路由到从库,减轻主库压力。缓存热点数据
对高频访问的列表(如首页推荐列表),用 Redis 缓存查询结果(设置合理过期时间),避免重复查询数据库。分表分库
若单表数据量过亿,可水平分表(按 ID 哈希或范围拆分),使每个分表数据量控制在百万级,关联查询时只需操作对应分表。
六、应用层配合
前端分页参数校验
限制size
最大值(如最多 100 条 / 页),避免一次性查询过大数据量。异步加载与懒加载
列表滚动到底部时,再加载下一页(懒加载),减少初始加载压力。预加载相邻页
提前缓存下一页数据(如用户浏览第 1 页时,异步加载第 2 页),提升用户体验。
总结
多表关联的列表查询优化核心是 “最小化处理数据量”:通过过滤减少关联数据、索引加速定位、游标分页避免全量扫描,再结合数据库架构和应用层策略,可在大数据量下保持高效查询。实际优化中需结合 EXPLAIN
执行计划和业务场景,优先解决最影响性能的瓶颈(如缺失索引、全表扫描)。