千萬級資料深分頁查詢SQL效能最佳化實踐
來源:京東雲開發者
一、系統介紹和問題描述
如何在Mysql中實現上億資料的遍歷查詢?先來介紹一下系統主角:關注系統,主要是維護京東使用者和業務物件之前的關注關係;並對外提供各種關係查詢,比如查詢使用者的關注商品或店鋪列表,查詢使用者是否關注了某個商品或店鋪等。但是最近接到了一個新需求,要求提供查詢關注物件的粉絲列表介面功能。該功能的難點就是關注物件的粉絲數量過多,不少店鋪的粉絲數量都是千萬級別,並且有些大V粉絲數量能夠達到上億級別。而這些粉絲列表資料目前全都儲存在Mysql庫中,然後透過業務物件ID進行分庫分表,所有的粉絲列表資料分佈在16個分片的256張表中。同時為了方便查詢粉絲列表,同一個業務物件的所有粉絲都會路由到同一張表中,每個表的資料量都能夠達到 2 億+。
二、解決問題的思路和方法
資料庫表結構示例如下:
CREATE TABLE follow_fans_[0-255]
(
id bigint(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
biz_content VARCHAR(50) DEFAULT NULL COMMENT '業務物件ID',
source VARCHAR(50) DEFAULT NULL COMMENT '來源',
pin VARCHAR(50) DEFAULT NULL COMMENT '使用者pin',
ext VARCHAR(5000) DEFAULT NULL COMMENT '擴充套件資訊',
status TINYINT(2) DEFAULT 1 COMMENT '狀態,0是失效,1是正常',
created_time DATETIME DEFAULT NULL COMMENT '建立時間',
modified_time DATETIME DEFAULT NULL COMMENT '修改時間',
PRIMARY KEY(id),
UNIQUE INDEX uniq_biz_content_pin (biz_content, pin)
)
ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8 COMMENT = '關注粉絲表';
Limit實現
由於同一個業務物件的所有粉絲都儲存到一張資料庫表中,對於分頁查詢列表介面,首先想到的就是用limit實現,對於粉絲數量很少的關注物件,查詢介面效能還不錯。但是隨著關注物件的粉絲數量越來越多,介面查詢效能就會越來越慢。後來經過介面壓測,當業務物件粉絲列表數量達到幾十萬級別的時候,查詢頁碼數量越大,查詢耗時越多。limit深分頁為什麼會變慢?這就和sql的執行計劃有關了,limit語句會先掃描offset+n行,然後再丟棄掉前offset行,返回後n行資料。也就是說limit 100000,10
,就會掃描100010行,而limit 0,10
,只掃描10行。查詢 sql 示例如下:
select id,biz_content,pin FROM follow_fans_1 where biz_content = #{bizContent} order by id desc limit 10, 10;
•方案缺點:資料量變大時,隨著查詢頁碼的深入,查詢效能越來越差。
標籤記錄法
Limit深分頁問題的本質原因就是:偏移量(offset)越大,mysql就會掃描越多的行,然後再拋棄掉,這樣就導致查詢效能的下降。所以我們可以採用標籤記錄法,就是標記一下上次查詢到哪一條了,下次再來查的時候,從該條開始往下掃描。具體做法方式是,查詢粉絲列表中按照自增主鍵ID倒序查詢,查詢結果中返回主鍵ID,然後查詢入參中增加maxId引數,該引數需要透傳上一次請求粉絲列表中最後一條記錄主鍵ID,第一次查詢時可以為空,但是需要查詢下一頁時就必傳。最後根據查詢時返回的行數是否等於 10 來判斷整個查詢是否可以結束。最佳化後的查詢sql參考如下:
select id,biz_content,pin FROM follow_fans_1 where biz_content = #{bizContent} and id < #{lastId} order by id desc limit 10;
•方案缺點:只能支援按照頁碼順序查詢,不支援跳頁,而且僅能保證前 N-1 頁的查詢效能;如果最後一頁的表中行數量不滿 10 條時,引擎不知道何時終止查詢,只能遍歷全表,所以當表中資料量很大時,還是會出現超時情況。
區間限制法
標籤記錄法最後一頁查詢超時就是因為不知道何時終止查詢,所以我們可以提供一個區間限制範圍來告訴引擎查詢到此結束。
查詢sql再次最佳化後參考如下:
select id,biz_content,pin FROM follow_fans_1 where biz_content = #{bizContent} and id < #{lastId} and id >={minId} order by id desc limit 10;
由於查詢時需要帶上 minId 引數,所以在執行查詢粉絲列表之前,我們就需要先把 minId 查詢出來,查詢 sql 參考如下:
select min(id) from follow_fans_1 where biz_content = #{bizContent}
由於表中資料量太大,每個表中總資料量都是上億級別,導致第一步查詢 minId就直接超時了,根本沒有機會去執行第二步。但是考慮到上一個查詢方案只有最後一頁才會查詢超時,前N-1頁查詢根本用不到 minId 作為區間限制。所以當表中資料量很大時,通常從第一頁到最後一頁查詢之間會存在一定的時間差。我們就可以正好去利用這個時間差去非同步查詢minId,然後將查詢出來的minId儲存到快取中,考慮到這個 minId 可能會被刪除,可以設定一定的過期時間。最後最佳化後的查詢流程如下:
1.呼叫查詢粉絲列表方法時首先查詢快取minId;
2.如果快取minId 為空,則建立非同步任務去執行select min(id) 查詢表中的 minId,然後回寫快取,該非同步任務執行時間可能會很長,可以單獨設定超時時間。
3.如果快取minId不為空,則在查詢sql中拼接查詢條件id >={minId},從而保證查詢最後一頁時不會超時。
但是在上述方案中,如果表中的資料量達到上億級別時,第二步的非同步獲取minId任務還是會存在超時的風險,從而導致查詢最後一頁粉絲列表出現超時。所以我們又引入了離線資料計算任務,透過在大資料平臺離線計算獲取每個biz_content下的minId,然後將計算結果minId推送到快取中。為了保證minId能夠及時更新,我們可以自由設定該離線任務的執行週期,比如每週執行一次。透過大資料平臺的離線計算minId,從而大大減少了在查詢粉絲列表時執行 select min(id)的業務資料庫壓力。只有當快取沒有命中的時候才去執行 select min(id),通常這些快取沒有命中的 minId 也都是一些被離線任務遺漏的少量資料,不會影響介面的整體查詢效能。
•方案優點:避免了資料量變大時,頁碼查詢深入的效能下降問題;經過介面壓測,千萬級資料量時,從第一頁到最後一頁都控制在幾十毫秒內。
•方案缺點:只能支援按照頁碼順序和主鍵ID倒序查詢,不支援跳頁查詢,並且還需要依賴大資料平臺離線計算和額外的快取來儲存 minId。
三、對SQL最佳化治理的思考
透過對以上三種方案的探索實踐,發現每一種方案都有自己的優缺點和它的適用場景,我們不能脫離實際業務場景去談方案的好壞。所以我們要結合實際的業務環境以及表中資料量的大小去綜合考慮、權衡利弊,然後找到更適合的技術方案。以下是總結的幾條SQL最佳化建議:
查詢條件一定要有索引
索引主要分為兩大類,聚簇索引和非聚簇索引,可以透過 explain 檢視 sql 執行計劃判斷查詢是否使用了索引。
聚簇索引 (clustered index):聚簇索引的葉子節點儲存行記錄,InnoDB必須要有且只有一個聚簇索引:
1.如果表定義了主鍵,則主鍵索引就是聚簇索引;
2.如果沒有定義主鍵,則第一個非空的唯一索引列是聚簇索引;
3.如果沒有唯一索引,則建立一個隱藏的row-id列作為聚簇索引。主鍵索引查詢非常快,可以直接定位行記錄。
非聚簇索引 (secondary index):InnoDB非聚簇索引的葉子節點儲存的是行記錄的主鍵值,而MyISAM葉子節點儲存的是行指標。通常情況下,需要先遍歷非聚簇索引獲得聚簇索引的主鍵ID,然後在遍歷聚簇索引獲取對應行記錄。
正確使用索引,防止索引失效
可以參考以下幾點索引原則:
1.最左字首匹配原則,mysql會一直向右匹配直到遇到範圍查詢(>、<、between、like)就停止匹配,比如 a=1 and b=2 and c>3 and d=4 ,如果建立了(a,b,c,d)順序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引則都可以用到,a、b、d的順序可以任意調整。
2.=和in可以亂序,比如 a=1 and b=2 and c=3 建立(a,b,c)索引可以任意順序,mysql的查詢最佳化器會幫助最佳化成索引可以識別的形式。
3.儘量選擇區分度高德列作為索引,區分度公式count(distinct col)/count(*),表示欄位不重複的比例。
4.索引列不能使用函式或參與計算,不能進行型別轉換,否則索引會失效。
5.儘量擴充套件索引,不要新建索引。
減少查詢欄位,避免回表查詢
回表查詢就是先定位主鍵值,在根據主鍵值定位行記錄,需要掃描兩遍索引。解決方案:只需要在一顆索引樹上能夠獲取SQL所需要的所有列資料,則無需回表查詢,速度更快。可以將要查詢的欄位,建立到聯合索引裡去,這就是索引覆蓋。查詢sql在進行explain解析時,Extra欄位為Using Index時,則觸發索引覆蓋。沒有觸發索引覆蓋,發生了回表查詢時,Extra欄位為Using Index condition。
來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70027826/viewspace-3004255/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- MySQL 千萬資料庫深分頁查詢優化,拒絕線上故障!MySql資料庫優化
- 30個MySQL千萬級大資料SQL查詢最佳化技巧詳解MySql大資料
- Elasticsearch從0到千萬級資料查詢實踐(非轉載)Elasticsearch
- elasticsearch查詢之大資料集分頁效能分析Elasticsearch大資料
- MySQL 百萬級資料量分頁查詢方法及其最佳化MySql
- 百億級資料分表後怎麼分頁查詢?
- 百億級資料 分庫分表 後怎麼分頁查詢?
- elasticsearch查詢之大資料集分頁查詢Elasticsearch大資料
- mysql千萬級資料量根據索引最佳化查詢速度MySql索引
- MySQL分頁查詢offset過大,Sql最佳化經驗MySql
- Oracle總結【SQL細節、多表查詢、分組查詢、分頁】OracleSQL
- MyBatis千萬級資料查詢解決方案,避免OOMMyBatisOOM
- 資料庫全表查詢之-分頁查詢優化資料庫優化
- Oracle資料庫中的分頁查詢Oracle資料庫
- indexdb實現分頁查詢Index
- 資料的儲存和查詢分離不利查詢效能 - thenewstack
- JSP怎樣將查詢的資料實現分頁操作JS
- MaxCompute如何對SQL查詢結果實現分頁獲取SQL
- mysql group by 執行原理及千萬級別count 查詢最佳化MySql
- 提高mysql千萬級大資料SQL查詢優化30條經驗(Mysql索引優化注意)MySql大資料優化索引
- 14個Flink SQL效能最佳化實踐分享SQL
- 記某百億級mongodb叢集資料過期效能最佳化實踐MongoDB
- MySQL查詢效能最佳化MySql
- 流式查詢1. mybatis的遊標Cursor,分頁大資料查詢MyBatis大資料
- 資料庫系列:MySQL慢查詢分析和效能最佳化資料庫MySql
- 一句SQL完成動態分級查詢SQL
- 如何使用PL/SQL進行分級查詢WPSQL
- PB級資料實時查詢,滴滴Elasticsearch多叢集架構實踐Elasticsearch架構
- SQL Server實戰四:查詢資料庫的資料SQLServer資料庫
- 如何分頁顯示資料庫查詢結果?資料庫
- Elasticsearch 分頁查詢Elasticsearch
- Iceberg 資料治理及查詢加速實踐
- 千萬級資料庫使用索引查詢速度更慢的疑惑-資料回表問題資料庫索引
- SSM框架實現分頁查詢例子SSM框架
- c# winform 實現分頁查詢C#ORM
- sharding-jdbc分表場景下的分頁查詢最佳化JDBC
- 千萬條資料,Stack Overflow 是如何實現快速分頁的?
- 私藏!資深資料專家SQL效率最佳化技巧 ⛵SQL