1. 程式人生 > >分頁查詢注意事項

分頁查詢注意事項

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