分頁查詢注意事項
小弟在修改一位同事的程式碼,主要功能是將資料庫中查詢的資料匯出成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 。