小弟在修改一位同事的代码,主要功能是将数据库中查询的数据导出成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; 等等条件
两种优化效果基本一致。我更喜欢第一种,不须要考虑主键的类型问题。