大表的展示优化

开发中可能会遇到一些数据量非常大的表,需要前台进行展示,无论怎么优化都非常慢。比如订单表,日志表。这里分享一下我的解决方案。就是

'避免使用 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

旧版本

new

去掉后

接着是具体的工作,如何最小改动现在的代码来优化性能。

代码基于 ruoyi框架

在分页这里,增加了一个入参来选择是否 进行count,默认为true,只在几个大表默认关闭count查询,

在关闭了count的同时,我会多查一条数据,以此来判定有没有下一页

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查询来返回不同的值,

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;
    }

例如当前page1pageSize20,如果有100条数据的话,返回的total就是21,这时候前端bootstrap-table自然就会显示两个页面,当你点击第二个页面时,返回就是41,这时候前端又会显示第三个页面。

效果如下

image-20220209200943262

image-20220209201008061

不过很明显的是,这里的总共2141条数据会对用户造成困扰,所以我打算隐藏这个值,并且加上了一个查询总数的按钮,当需要获取总共条数时,可以手动获取。

所以,我又写了个jquery插件。

/**
 * 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);

这里代码就不做太多的解释了,还要感谢这位老哥的博客,照着他的代码写了一个插件。大概就是在原代码初始化页码之后,删除原先的统计,换上一个自定义按钮,然后绑定上事件。当前有些值时通过外界传来的,方便扩展。

最后,上一下最终效果

image-20220209201646265

最终达到了低入侵的优化效果,只需要在展示慢的表格上分页时指定countfalse即可。


结尾

当初之所以冒出这个想法是因为之前看过的一些讨论,有些 to c 的网站其实都没有进行 count 总数查询的,也不允许用户查询太老的数据,一是分页的页码太大,即使有索引也会太慢,更何况还有 order排序这种场景,二是可能他们会把太老的数据给存档,迁移到别的地方,或者分库分表等,允许用户随意查看势必会增加架构上的复杂性。去掉了 count 这次查询之后,起码会提高 50% 的性能,如果需要总数的话,还可以进行手动点击获取。

在和我组长讨论了一下后,确定了业务上并没有必要,所以就有了这个实践。

感谢你看到这里,希望我的想法能给你带来帮助,有兴趣的话也欢迎关注我的个人博客

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值