开发中可能会遇到一些数据量非常大的表,需要前台进行展示,无论怎么优化都非常慢。比如订单表,日志表。这里分享一下我的解决方案。就是
'避免使用 `count`'
我们常见的企业后端表格一个查询时都会分页,然后计算页数,展示在表格下面,然而我们很少需要查看每一页,或者跳转到最后几页,尤其是这种数据量特别大的表,我们通常是粗略浏览,或者根据搜索条件搜索我们指定的数据,或者导出数据。这时候查询是进行`count`计算页码就是多余的,而且会消耗巨大的资源。
当前,如果你业务必需要这么做,那也没办法,我下面写的内容可能就不适合你。
首先,这里我说一下我的相关开发框架。后端是 `spring`+`mybatis`+`pagehelper`,前段是`bootstrap``jquery`,数据库为`mysql` ,如果你的方案和我不同也没关系,思路是大致相通的。
------
刚开始的时候,我也去找了其他的解决的方案,效果不尽如人意,治标不治本
> 参考链接
>
> https://stackoverflow.com/questions/22352073/improve-innodb-count-performance
>
> https://stackoverflow.com/questions/57921400/are-full-count-queries-really-so-slow-on-a-large-mysql-innodb-tables
有些我没有尝试,有些说增加索引的,他们多少都不太适合我现在的环境,一是索引会增加数据冗余,二是我没线上数据库的权限,不好测试,而且大表增加索引会很慢,三是即使使用正确的索引,索引也会很大。所以我使用了最简单粗暴的。
先放上一个优化前后的速度对比:
![old](https://s2.loli.net/2022/02/09/243VQjo9NHiY7pd.png)
> 旧版本
![new](https://s2.loli.net/2022/02/09/hry6GM5sxIodCXR.png)
> 去掉后
接着是具体的工作,如何最小改动现在的代码来优化性能。
代码基于 `ruoyi`框架
在分页这里,增加了一个入参来选择是否 进行`count`,默认为`true`,只在几个大表默认关闭`count`查询,
在关闭了`count`的同时,我会多查一条数据,以此来判定有没有下一页
```java
protected void startPage(boolean count) {
PageDomain pageDomain = TableSupport.buildPageRequest();
Integer pageNum = pageDomain.getPageNum();
Integer pageSize = pageDomain.getPageSize();
if (pageNum == null || pageSize == null) {
return;
}
String orderBy = SqlUtil.escapeOrderBySql(pageDomain.getOrderBy());
if (count) {
PageHelper.startPage(pageNum, pageSize,orderBy);
} else {
//不进行count的话手动计算边界,并且页数+1,来计算有没有下一页
int startRow = pageNum > 0 ? (pageNum - 1) * pageSize : 0;
PageHelper.offsetPage(startRow, pageSize+1,false).setOrderBy(orderBy);
}
}
```
在后面获取分页数据并且发送给前端的时候,根据是否进行过`count`查询来返回不同的值,
```java
protected TableDataInfo getDataTable(List<?> list) {
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(0);
rspData.setRows(list);
if (list instanceof Page) {
Page<?> page = (Page) list;
rspData.setTotal(page.getTotal());
//如果没有分页
rspData.setCount(page.isCount());
if (!page.isCount()) {
rspData.setTotal(page.getStartRow() + list.size());
}
} else {
rspData.setTotal(list.size());
}
return rspData;
}
```
例如当前`page`为`1`,`pageSize`为`20`,如果有`100`条数据的话,返回的`total`就是`21`,这时候前端`bootstrap-table`自然就会显示两个页面,当你点击第二个页面时,返回就是`41`,这时候前端又会显示第三个页面。
效果如下
![image-20220209200943262](https://s2.loli.net/2022/02/09/fuBQOG1ULrt8Dg9.png)
![image-20220209201008061](https://s2.loli.net/2022/02/09/9uPXEYjRJfiUyML.png)
不过很明显的是,这里的总共`21`,`41`条数据会对用户造成困扰,所以我打算隐藏这个值,并且加上了一个查询总数的按钮,当需要获取总共条数时,可以手动获取。
所以,我又写了个jquery插件。
```javascript
/**
* bootstrap-table的扩展,适配没有count计数的情况
*/
(function ($) {
'use strict';
$.extend($.fn.bootstrapTable.defaults, {
/**
* 分页显示总数按钮的回调,应该返回一个Promise<Number>或者Number
* 如果为null,则不显示显示总数按钮
* @type {function (): Promise<Number>| Number}
*/
onPageShowTotal: null
});
let BootstrapTable = $.fn.bootstrapTable.Constructor;
let _initPagination = BootstrapTable.prototype.initPagination;
// 扩展已有的初始化分页组件的方法
BootstrapTable.prototype.initPagination = function () {
_initPagination.apply(this, Array.prototype.slice.apply(arguments));
if (this.options.pageHasCount) {
//如果已经有count计数,跳过后续逻辑
return;
}
//如果没有count计算,修改界面展示
let paginationInfo = this.$pagination.find(".pagination-info");
let countTxt = paginationInfo.text();
let showTotal = this.options.onPageShowTotal ? ",<a class='show-total'>显示总数</a> " : "";
paginationInfo.html(countTxt.split(",")[0] + showTotal);
//分页大小下拉框显示全部
let pageSizeSelector = this.$pagination.find(".dropdown-menu");
let pageList = this.options.pageList;
let pageSizeSelectorHtml = "";
pageList.forEach(num => {
pageSizeSelectorHtml += `<li role="menuitem" ${num == this.options.pageSize ? "class='active'" : ""}><a href="#">${num}</a></li>`;
});
pageSizeSelector.html(pageSizeSelectorHtml);
this.$pagination.find('.show-total').off('click').on('click', $.proxy(this.onPageShowTotal, this));
};
/**
* 点击显示总数之后
*/
BootstrapTable.prototype.onPageShowTotal = function () {
let paginationInfo = this.$pagination.find(".pagination-info");
let value = this.options.onPageShowTotal();
if (value instanceof Promise) {
value.then(count => {
this.$pagination.find(".show-total").remove()
let countTxt = paginationInfo.text();
paginationInfo.html(countTxt + `总共 ${count} 条记录`);
})
} else {
this.$pagination.find(".show-total").remove()
let countTxt = paginationInfo.text();
paginationInfo.html(countTxt + `总共 ${value} 条记录`);
}
}
})(jQuery);
```
这里代码就不做太多的解释了,还要感谢[这位](https://www.mk2048.com/blog/blog_j2a10ci1abkj.html)老哥的博客,照着他的代码写了一个插件。大概就是在原代码初始化页码之后,删除原先的统计,换上一个自定义按钮,然后绑定上事件。当前有些值时通过外界传来的,方便扩展。
最后,上一下最终效果
![image-20220209201646265](https://s2.loli.net/2022/02/09/8STruE2ICif7paJ.png)
最终达到了低入侵的优化效果,只需要在展示慢的表格上分页时指定`count`为`false`即可。
------
结尾
当初之所以冒出这个想法是因为之前看过的一些讨论,有些 `to c` 的网站其实都没有进行 `count` 总数查询的,也不允许用户查询太老的数据,一是分页的页码太大,即使有索引也会太慢,更何况还有 `order `排序这种场景,二是可能他们会把太老的数据给存档,迁移到别的地方,或者分库分表等,允许用户随意查看势必会增加架构上的复杂性。去掉了 `count` 这次查询之后,起码会提高 50% 的性能,如果需要总数的话,还可以进行手动点击获取。
在和我组长讨论了一下后,确定了业务上并没有必要,所以就有了这个实践。
感谢你看到这里,希望我的想法能给你带来帮助,有兴趣的话也欢迎关注我的其他平台。
> 后注:其中有些代码细节上还有点问题,但大致思路是这样
大表的展示优化