小弟在修改一位同事的代碼,主要功能是將數據庫中查詢的數據導出成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; 等等條件
兩種優化效果基本一致。我更喜歡第一種,不須要考慮主鍵的類型問題。