最近遇到了這么一個情況,數據庫里面的數據由於長期的堆積,導致數據量不斷的上升,而后台的系統每次進行分頁查詢的時候,效率都會降低很多。后來查看了一下之后,發現此時的分頁原理主要是采用了傳統的物理分頁 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.適當的時候采用覆蓋索引可以提高查詢的效率。