分页查询注意事项

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

在排查过程中,发现其他发送邮件与io流写入都耗时很少。那唯一的问题就是在生成excel数据时了,代码如下:

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

			}
		}

以上 查看后发现:

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

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

2、最耗时的地方是

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数量。这种做法是不可取的。

修改后的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 。

猜你喜欢

转载自my.oschina.net/u/2543341/blog/2050055