1. 程式人生 > 實用技巧 >百萬級資料,分頁如何處理

百萬級資料,分頁如何處理

最近遇到了這麼一個情況,資料庫裡面的資料由於長期的堆積,導致資料量不斷的上升,而後臺的系統每次進行分頁查詢的時候,效率都會降低很多。後來查看了一下之後,發現此時的分頁原理主要是採用了傳統的物理分頁 limit n,m 的方式。

為了方便演示,我特意建立了以下幾張表進行例項演練:

表分別是商品表,使用者表,使用者選購商品記錄表:

goods user g_u

三張表的關係比較簡單,user的id和goods裡面的id合併生成關聯資料,儲存在了g_u裡面。三張資料庫表的設計如下所示:

CREATE TABLE `goods` (
  `id` int(11) NOT NULL,
  `name` 
varchar(60) COLLATE utf8_unicode_ci NOT NULL, `price` decimal(6,1) NOT NULL, `des` varchar(40) COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(20) COLLATE utf8_unicode_ci NOT
NULL, `age` tinyint(3) NOT NULL, `sex` tinyint(1) NOT NULL COMMENT '年齡', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci CREATE TABLE `g_u` ( `id` int(11) NOT NULL AUTO_INCREMENT, `g_id` int(11) NOT NULL COMMENT '商品id', `u_id` int(11) NOT
NULL COMMENT '使用者id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2800001 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

這個模擬的應用場景非常簡單,使用者和商品之間的關係維持在了一對多的關聯中。為了方便進行後續的測試,我用jmeter批量建立了1900000條測試資料,模擬一次百萬級的資料查詢場景。

相應的資料指令碼也已經存在百度雲中了,需要的同學可以前往下載:
地址:

連結:https://pan.baidu.com/s/1BfddJ8MBtnpeiV84gNmClA

提取碼:4kmp

假設現在需求裡面有這樣的一個業務場景,需要我們對購買記錄表裡面的資料進行分頁查詢,那麼對於常規的分頁查詢操作,常人會想到的方式可能是通過下述的語句:

SELECT * from g_u as gu ORDER BY id limit 1850000,100

測試一下發現,查詢的時間為:

當我們搜尋的資料越靠後邊的時候,搜尋的速度就會越低下,因此這個時候,適當的建立索引就顯得比較重要了。

首先我們來做一次explain的sql檢測,檢測結果為如下所示:

由於我們查詢的時候,使用的是根據主鍵索引id進行排序,因此查詢的時候key一項為PRIMARY。

 SELECT * FROM g_u WHERE id >=(SELECT id FROM g_u LIMIT 1850000,1) ORDER BY id  LIMIT 100

此時查詢有了一些許的提升,但是依舊查詢緩慢

通過explain執行計劃分析結果可見:

子查詢用到了索引,外部查詢用到了where的輔助索引

這個時候我們不妨可以試下通過利用主鍵id來提升我們的查詢效率:

SELECT * FROM g_u as gu WHERE gu.id>($firstId+$pageSize*$pageSize)  limit 100

查詢的時間一下子大大縮短了許多:

通過explain分析一下該sql:

這裡面,sql在執行的時候藉助了主鍵索引的幫助,因此效率大大提升了。

但是這個時候,可能你會有這麼一個疑惑。如果說資料的索引不是連續的該如何處理分頁時候每頁資料的完整性和一致性?

這裡不妨可以試試另外的一種思路,通過建立一張第三方的表g_u_index表,將原本亂序的id儲存在g_u_index中,在g_u_index一表中,我們可以通過該表有序的g_u_index.id來對應原本相應的無序的g_u.id。建表的sql語句如下所示:

CREATE TABLE `g_u_index` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `index` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_id_index` (`id`,`index`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1900024 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

ps: 可以為id和index兩者建立一套複合索引,提升查詢的效率。

這裡我們需要保證一點就是,g_u表中插入的資料順序需要和g_u_index表中插入的順序是一致的。然後查詢分頁指定的index時候可以這麼來查:

 SELECT g_u_index.index FROM g_u_index  WHERE id=($firstId+$pageSize*$pageSize)  limit 1

通過執行explain分析後,結果變成如下所示:

查詢時間為:0.001s

有了第三方表的幫助下,此時分頁的sql優化可以調整為以下這種方式:

SELECT * FROM g_u as gu where gu.id>(
SELECT g_u_index.index FROM g_u_index  WHERE id=($firstId+$pageSize*$pageSize) limit 1
) limit 100

通過構建了第三方表之後,資料的查詢時間一下子大大縮減了:

查詢的時候為了更加人性化,通常不需要顯示這些無意義的id,需要的是商品名稱和使用者姓名,假設我們還是隻採用最原始的無第三方表的方式進行查詢的話,效率會比較底下:

SELECT gu.id,goods.`name`,`user`.username FROM g_u as gu ,goods ,`user` 
where goods.id=gu.g_id AND `user`.id=gu.u_id 
ORDER BY id limit 1500000,1000

結果:

因此如果藉助了第三方表查詢的話,sql可以調整成下方這種型別:

SELECT goods.`name`,`user`.username FROM g_u as gu ,goods ,`user` 
where goods.id=gu.g_id AND `user`.id=gu.u_id 
and 
gu.id>=(
SELECT g_u_index.index FROM g_u_index  WHERE id=(9+1000*1900) limit 1
) limit 100

查詢的時間會大大減少:

通過explain執行計劃分析之後,結果如下:

在實際的業務場景中,一張原來就有上百萬資料的表要做出這樣的id拆分,並且同步到第三方表的確實不太容易,這裡推薦一種思路,可以藉助阿里的中介軟體canal來實現對於資料庫日誌的訂閱,然後自定義進行資料的同步操作。

對於sql的優化需要結合實際的業務需求來開展,總的來說,這部分還是需要有一定的實戰演練才能變強。

常用的sql優化技巧小結:

1.資料量大的時候,應儘量避免全表掃描,應考慮在 where及 order by 涉及的列上建立索引,建索引可以大大加快資料的檢索速度。

2.適當的使用Explain可以對sql進行相應的深入分析。

3.當只要一行資料時使用LIMIT 1。

4.在使用索引欄位作為條件時,如果該索引是複合索引,那麼必須使用到該索引中的第一個欄位作為條件時才能保證系統使用該索引,否則該索引將不會被使用,並且應儘可能的讓欄位順序與索引順序相一致。

5.不要在 where子句中的“=”左邊進行函式、算術運算或其他表示式運算,否則系統將可能無法正確使用索引。

6.適當的時候採用覆蓋索引可以提高查詢的效率。