分页查询注意事项

小弟在修改一位同事的代码,主要功能是将数据库中查询的数据导出成excel并发送邮件,整个过程要55min,有点长,数据不到20W。怎么回事呢?java

在排查过程当中,发现其余发送邮件与io流写入都耗时不多。那惟一的问题就是在生成excel数据时了,代码以下:mysql

HSSFWorkbook hwb = new HSSFWorkbook();
		HSSFFont font = hwb.createFont();// 建立字体样式
		font.setFontName("宋体");// 使用宋体
		font.setFontHeightInPoints((short) 10);// 字体大小
		// 设置单元格格式
		HSSFCellStyle style1 = hwb.createCellStyle();
		style1.setFont(font);// 将字体注入
		style1.setWrapText(true);// 自动换行
		style1.setAlignment(HSSFCellStyle.ALIGN_CENTER);// 左右居中
		style1.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);// 上下居中
		style1.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());// 设置单元格的背景颜色
		style1.setFillPattern(CellStyle.SOLID_FOREGROUND);
		style1.setBorderTop((short) 1);// 边框的大小
		style1.setBorderBottom((short) 1);
		style1.setBorderLeft((short) 1);
		style1.setBorderRight((short) 1);

		// 建立sheet对象(表单对象)
		HSSFSheet sheet1 = hwb.createSheet("随心转自由转持有金额");

		// 设置每列的宽度
		sheet1.setColumnWidth(0, 20 * 256);
		sheet1.setColumnWidth(1, 20 * 256);
		sheet1.setColumnWidth(2, 20 * 256);
		sheet1.setColumnWidth(3, 20 * 256);
		sheet1.setColumnWidth(4, 20 * 256);

		// 建立sheet的列名
		HSSFRow row1 = sheet1.createRow(0);

		row1.createCell(0).setCellValue("用户id");
		row1.createCell(1).setCellValue("会员等级");
		row1.createCell(2).setCellValue("天才值");
		row1.createCell(3).setCellValue("随心转持有金额");
		row1.createCell(4).setCellValue("自由转持有金额");

//		Date lastDay = DateUtil.afterNDay(DateUtil.todayDate(), -1);

		long pageSize = 500;// 每次查询500条数据
		// 总数
 		long sum = idebtcurrentuserholdingservice.countUserFreeCurrentNum();
		logger.info("======sum= 总数========================"+sum);
		long totalPage = sum % pageSize > 0 ? sum / pageSize + 1 : sum / pageSize;// 分页公式      	总页数
		logger.info("======totalPage===总页数======================"+totalPage);

		if (totalPage > 0) {
			for (int i = 1; i <= totalPage; i++) {
				List<DebtHoldingVo> debtHoldvoList = idebtcurrentuserholdingservice
						.selectUserfreecruentamount(pageSize * (i - 1), pageSize);// 分页去查询
				for (int j = 0; j < debtHoldvoList.size(); j++) {
				          HSSFRow row = sheet1.createRow((int) ((j+1)+pageSize *(i - 1)));
					             row.createCell(0).setCellValue(debtHoldvoList.get(j).getUserId());
				                row.createCell(1).setCellValue(debtHoldvoList.get(j).title());
					            row.createCell(2).setCellValue(debtHoldvoList.get(j).getTalentValue());
					            row.createCell(3).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getCurrentamount()))); //四舍五入保留2位
					            row.createCell(4).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getFreeamount())));
				}

			}
		}

以上 查看后发现:算法

一、使用的是03版本的excel ,根据poi的api可知,03版本的excel一个sheet最大才能存6W+的数据量。而目前数据量是20W左右,虽然生成的数据,但领导也没说正确与否,我估计也不可能正确。因此我修改了poi的版本 支持07版本的excelsql

<dependency>
            <groupId>org.apache.poi</groupId>
            <artifactId>poi</artifactId>
            <version>3.10-FINAL</version>
        </dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi-ooxml</artifactId>
			<version>3.10-FINAL</version>
	    </dependency>
		<dependency>
			<groupId>org.apache.poi</groupId>
			<artifactId>poi-ooxml-schemas</artifactId>
			<version>3.10-FINAL</version>
		</dependency>

此api 支持一次性写入大量数据(104W+)。代码修改成:数据库

long startTimes = System.currentTimeMillis();
        SXSSFWorkbook sxssfWorkbook = new SXSSFWorkbook(1000);
        Font font = sxssfWorkbook.createFont();
        font.setFontName("宋体");// 使用宋体
        font.setFontHeightInPoints((short) 10);

        // 设置单元格格式
        CellStyle cellStyle = sxssfWorkbook.createCellStyle();
        cellStyle.setFont(font);// 将字体注入
        cellStyle.setWrapText(true);// 自动换行
        cellStyle.setAlignment(HSSFCellStyle.ALIGN_CENTER);// 左右居中
        cellStyle.setVerticalAlignment(HSSFCellStyle.VERTICAL_CENTER);// 上下居中
        cellStyle.setFillForegroundColor(IndexedColors.LIGHT_YELLOW.getIndex());// 设置单元格的背景颜色
        cellStyle.setFillPattern(CellStyle.SOLID_FOREGROUND);
        cellStyle.setBorderTop((short) 1);// 边框的大小
        cellStyle.setBorderBottom((short) 1);
        cellStyle.setBorderLeft((short) 1);
        cellStyle.setBorderRight((short) 1);

        Sheet firstSheet = sxssfWorkbook.createSheet("随心转自由转持有金额");
        // 设置每列的宽度
        firstSheet.setColumnWidth(0, 20 * 256);
        firstSheet.setColumnWidth(1, 20 * 256);
        firstSheet.setColumnWidth(2, 20 * 256);
        firstSheet.setColumnWidth(3, 20 * 256);
        firstSheet.setColumnWidth(4, 20 * 256);
        Row row0 = firstSheet.createRow(0);
        row0.createCell(0).setCellValue("用户id");
        row0.createCell(1).setCellValue("会员等级");
        row0.createCell(2).setCellValue("天才值");
        row0.createCell(3).setCellValue("随心转持有金额");
        row0.createCell(4).setCellValue("自由转持有金额");

        long pageSize = 500;// 每次查询500条数据最快
        // 总数
        long sum = idebtcurrentuserholdingservice.countUserFreeCurrentNum();
        logger.info("======sum= 总数========================"+sum);
        long totalPage = sum % pageSize > 0 ? sum / pageSize + 1 : sum / pageSize;// 分页公式      	总页数
        logger.info("======totalPage===总页数======================"+totalPage);
        List<DebtHoldingVo> debtHoldvoList = null;
        if (totalPage > 0) {
            for (int i = 1; i <= totalPage; i++) {
                if(i == 1){
                    debtHoldvoList = idebtcurrentuserholdingservice
                            .selectUserfreecruentamount(pageSize * (i - 1), pageSize);
                }else{
                    //获取最后一个hid
                    DebtHoldingVo vo = debtHoldvoList.get(debtHoldvoList.size() - 1);
                    Long hId = vo.gethId();
                    debtHoldvoList = idebtcurrentuserholdingservice
                            .selectUserfreecruentamount(hId, pageSize);
                }
                for (int j = 0; j < debtHoldvoList.size(); j++) {
                    Row row = firstSheet.createRow((int) ((j+1)+pageSize *(i - 1)));
                    row.createCell(0).setCellValue(debtHoldvoList.get(j).getUserId());
                    row.createCell(1).setCellValue(debtHoldvoList.get(j).title());
                    row.createCell(2).setCellValue(debtHoldvoList.get(j).getTalentValue());
                    row.createCell(3).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getCurrentamount()))); //四舍五入保留2位
                    row.createCell(4).setCellValue(NumberFormat.doubleUpTwoDecimal(NumberFormat.outDataMoney(debtHoldvoList.get(j).getFreeamount())));
                }

            }
        }

修改了上述方案测试了一下 为37min;apache

二、最耗时的地方是api

List<DebtHoldingVo> debtHoldvoList = idebtcurrentuserholdingservice
						.selectUserfreecruentamount(pageSize * (i - 1), pageSize);// 分页去查询

逻辑上确定是没错的,但分页的sql写法有问题,以下:缓存

SELECT DISTINCT
	h.id AS hid,
	u.id,
	u.talent_value,
	(
		SELECT
			IFNULL(sum(amount), 0)
		FROM
			debt_current_user_holding
		WHERE
			asset_type = 'FREE_CURRENT'
		AND debt_repayment_status != 1
		AND user_id = u.id
	) currentamount,
	(
		SELECT
			IFNULL(sum(amount), 0)
		FROM
			debt_current_user_holding
		WHERE
			asset_type = 'FREE_PRODUCT'
		AND debt_repayment_status != 1
		AND user_id = u.id
	) freeamount
FROM
	debt_current_user_holding h
LEFT JOIN users u ON h.user_id = u.id
WHERE
	u.deleted_at IS NULL
AND h.deleted_at IS NULL
AND h.asset_type IN (
	'FREE_PRODUCT',
	'FREE_CURRENT'
)
LIMIT #{startPages}, #{countPage}

打眼一看没啥事。但仔细想一想 就不是那么回事了。这种写法在小数据量前提下确定没问题。一旦数据量过万,就会出现性能瓶颈。并发

随着startPages的增长,查询速度也会愈来愈慢,缘由就是 每次查询时,mysql都是全表查询,而后从指定位置向后取countPage数量。这种作法是不可取的。xss

修改后的sql为:

SELECT DISTINCT
	h.id AS hid,
	u.id,
	u.talent_value,
	(
		SELECT
			IFNULL(sum(amount), 0)
		FROM
			debt_current_user_holding
		WHERE
			asset_type = 'FREE_CURRENT'
		AND debt_repayment_status != 1
		AND user_id = u.id
	) currentamount,
	(
		SELECT
			IFNULL(sum(amount), 0)
		FROM
			debt_current_user_holding
		WHERE
			asset_type = 'FREE_PRODUCT'
		AND debt_repayment_status != 1
		AND user_id = u.id
	) freeamount
FROM
	debt_current_user_holding h
LEFT JOIN users u ON h.user_id = u.id
WHERE
	h.id > #{startPages} 

AND u.deleted_at IS NULL
AND h.deleted_at IS NULL
AND h.asset_type IN (
	'FREE_PRODUCT',
	'FREE_CURRENT'
)
LIMIT #{countPage}

这种查询的好处就是每次查询的时候都会从指定的id后进行查询,使用主键惟一索引每次不会全表查询,前提是主键最好是数字类型,且自增的。 查询后发现数据是按照主键进行升序排列(查询的默认机制asc)。但网上的说法是可能会丢失一部分数据(这种是由于有人为操做数据到致使主键不连续。或者断层很大。或者主键不是有序的。)

这种查询方式适用范围较小,必需要主键是数字类型,没有断层。最好是自增的。

若是不知足上述条件,若是增长了order by 在大数据量的前提下 效率很低。

其余方案还在验证中;

以上方案修改完成后 测试结果为131s 。

 

=============================================================

2019-02-19 第二套方案验证:

上述方案,由于适用范围较小。因此近期又适用了第二套方案。就是子查询方案。

一、首先回顾一下mysql 分页查询的基本用法:

select * from product order by id limit stratRow, pageSize ;

这种写法,原理简单易于理解,但伴随数据量的增长。超过万级数据 ,查询时间就会成大量增长,缘由是由于查询时,先扫描全表,后从指定位置向后筛选,在排除多余数据。浪费大量时间

二、子查询的用法:

首先 咱们先优化上述查询,

select id from product order by id limit stratRow, pageSize ;很是短的时间就能够实现。

而后外面用主查询 in 子查询,而后进行排序和分组等操做。这样时间会大大提高

SELECT * FROM product a inner JOIN (select id from product limit stratRow, pageSize) b ON a.ID = b.id  order by a.xx 等等条件

这种方案 也是利用了mysql  针对主键索引 和索引查询缓存的算法优化,加速了查询。

注意,若是你的主键是数字类型、连续、自增的。那还可使用

SELECT * FROM product a where a.id >= (select id from product limit stratRow, 1)  order by a.xx  limit pageSize 20; 等等条件

两种优化效果基本一致。我更喜欢第一种,不须要考虑主键的类型问题。

相关文章
相关标签/搜索