分頁查詢注意事項

小弟在修改一位同事的代碼,主要功能是將數據庫中查詢的數據導出成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; 等等條件

兩種優化效果基本一致。我更喜歡第一種,不須要考慮主鍵的類型問題。

相關文章
相關標籤/搜索