摘要:刷帖子翻頁需要分頁查詢,搜索商品也需分頁查詢。當遇到上千萬、上億數據量,怎么快速拉取全量數據呢?
本文分享自華為雲社區《大數據量性能優化之分頁查詢》,作者: JavaEdge。
刷帖子翻頁需要分頁查詢,搜索商品也需分頁查詢。當遇到上千萬、上億數據量,怎么快速拉取全量數據呢?
比如:
- 大商家拉取每月千萬級別的訂單數量到自己獨立的ISV做財務統計
- 擁有百萬千萬粉絲的大v,給全部粉絲推送消息
案例
常見錯誤寫法
SELECT * FROM table where kid = 1342 and type = 1 order id asc limit 149420,20;
典型的排序+分頁查詢:
order by col limit N,OFFSET M
MySQL 執行此類SQL時:先掃描到N行,再取 M行。
N越大,MySQL需掃描更多數據定位到具體的N行,這會耗費大量的I/O成本和時間成本。
為什么上面的SQL寫法掃描數據會慢?
- t是個索引組織表,key idx_kid_type(kid,type)
符合kid=3 and type=1 的記錄有很多行,我們取第 9,10行。
select * from t where kid =3 and type=1 order by id desc 8,2;
對於Innodb,根據 idx_kid_type 二級索引里面包含的主鍵去查找對應行。
對百萬千萬級記錄,索引大小可能和數據大小相差無幾,cache在內存中的索引數量有限,而且二級索引和數據葉子節點不在同一物理塊存儲,二級索引與主鍵的相對無序映射關系,也會帶來大量隨機I/O請求,N越大越需遍歷大量索引頁和數據葉,需要耗費的時間就越久。
由於上面大分頁查詢耗時長,是否真的有必要完全遍歷“無效數據”?
若需要:
limit 8,2
跳過前面8行無關數據頁的遍歷,可直接通過索引定位到第9、10行,這樣是不是更快?
這就是延遲關聯的核心思想:通過使用覆蓋索引查詢返回需要的主鍵,再根據主鍵關聯原表獲得需要的數據,而非通過二級索引獲取主鍵再通過主鍵遍歷數據頁。
通過如上分析可得,通過常規方式進行大分頁查詢慢的原因,也知道了提高大分頁查詢的具體方法。
一般分頁查詢
簡單的 limit 子句。limit 子句聲明如下:
SELECT * FROM table LIMIT
[offset,] rows | rows OFFSET offset
limit 子句用於指定 select 語句返回的記錄數,注意:
- offset 指定第一個返回記錄行的偏移量,默認為0
初始記錄行的偏移量是0,而非1 - rows 指定返回記錄行的最大數量
rows 為 -1 表示檢索從某個偏移量到記錄集的結束所有的記錄行。
若只給定一個參數:它表示返回最大的記錄行數目。
實例:
select * from orders_history where type=8 limit 1000,10;
從 orders_history 表查詢offset: 1000開始之后的10條數據,即第1001條到第1010條數據(1001 <= id <= 1010)。
數據表中的記錄默認使用主鍵(id)排序,上面結果等價於:
select * from orders_history where type=8 order by id limit 10000,10;
三次查詢時間分別為:
3040 ms 3063 ms 3018 ms
針對這種查詢方式,下面測試查詢記錄量對時間的影響:
select * from orders_history where type=8 limit 10000,1; select * from orders_history where type=8 limit 10000,10; select * from orders_history where type=8 limit 10000,100; select * from orders_history where type=8 limit 10000,1000; select * from orders_history where type=8 limit 10000,10000;
三次查詢時間:
查詢1條記錄:3072ms 3092ms 3002ms
查詢10條記錄:3081ms 3077ms 3032ms
查詢100條記錄:3118ms 3200ms 3128ms
查詢1000條記錄:3412ms 3468ms 3394ms
查詢10000條記錄:3749ms 3802ms 3696ms
在查詢記錄量低於100時,查詢時間基本無差距,隨查詢記錄量越來越大,消耗時間越多。
針對查詢偏移量的測試:
select * from orders_history where type=8 limit 100,100; select * from orders_history where type=8 limit 1000,100; select * from orders_history where type=8 limit 10000,100; select * from orders_history where type=8 limit 100000,100; select * from orders_history where type=8 limit 1000000,100;
三次查詢時間如下:
查詢100偏移:25ms 24ms 24ms
查詢1000偏移:78ms 76ms 77ms
查詢10000偏移:3092ms 3212ms 3128ms
查詢100000偏移:3878ms 3812ms 3798ms
查詢1000000偏移:14608ms 14062ms 14700ms
隨着查詢偏移的增大,尤其查詢偏移大於10萬以后,查詢時間急劇增加。
這種分頁查詢方式會從DB的第一條記錄開始掃描,所以越往后,查詢速度越慢,而且查詢數據越多,也會拖慢總查詢速度。
優化
- 前端加緩存、搜索,減少落到庫的查詢操作
比如海量商品可以放到搜索里面,使用瀑布流的方式展現數據 - 優化SQL 訪問數據的方式
直接快速定位到要訪問的數據行。推薦使用"延遲關聯"的方法來優化排序操作,何謂"延遲關聯" :通過使用覆蓋索引查詢返回需要的主鍵,再根據主鍵關聯原表獲得需要的數據。 - 使用書簽方式 ,記錄上次查詢最新/大的id值,向后追溯 M行記錄
延遲關聯
優化前
explain SELECT id, cu_id, name, info, biz_type, gmt_create, gmt_modified,start_time, end_time, market_type, back_leaf_category,item_status,picuture_url FROM relation where biz_type ='0' AND end_time >='2014-05-29' ORDER BY id asc LIMIT 149420 ,20; +----+-------------+-------------+-------+---------------+-------------+---------+------+--------+-----+ | id | select_type | table | type | possible_keys | key | key\_len | ref | rows | Extra | +----+-------------+-------------+-------+---------------+-------------+---------+------+--------+-----+ | 1 | SIMPLE | relation | range | ind\_endtime | ind\_endtime | 9 | NULL | 349622 | Using where; Using filesort | +----+-------------+-------------+-------+---------------+-------------+---------+------+--------+-----+
執行時間:
20 rows in set (1.05 sec)
優化后:
explain SELECT a.* FROM relation a, (select id from relation where biz_type ='0' AND end\_time >='2014-05-29' ORDER BY id asc LIMIT 149420 ,20 ) b where a.id=b.id; +----+-------------+-------------+--------+---------------+---------+---------+------+--------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-------------+--------+---------------+---------+---------+------+--------+-------+ | 1 | PRIMARY | <derived2> | ALL | NULL | NULL | NULL | NULL | 20 | | | 1 | PRIMARY | a | eq_ref | PRIMARY | PRIMARY | 8 | b.id | 1 | | | 2 | DERIVED | relation | index | ind_endtime | PRIMARY | 8 | NULL | 733552 | | +----+-------------+-------------+--------+---------------+---------+---------+------+--------+-------+
執行時間:
20 rows in set (0.36 sec)
優化后 執行時間 為原來的1/3 。
書簽
首先獲取符合條件的記錄的最大 id和最小id(默認id是主鍵)
select max(id) as maxid ,min(id) as minid from t where kid=2333 and type=1;
根據id 大於最小值或者小於最大值進行遍歷。
select xx,xx from t where kid=2333 and type=1 and id >=min_id order by id asc limit 100; select xx,xx from t where kid=2333 and type=1 and id <=max_id order by id desc limit 100;
案例
當遇到延遲關聯也不能滿足查詢速度的要求時
SELECT a.id as id, client_id, admin_id, kdt_id, type, token, created_time, update_time, is_valid, version FROM t1 a, (SELECT id FROM t1 WHERE 1 and client_id = 'xxx' and is_valid = '1' order by kdt_id asc limit 267100,100 ) b WHERE a.id = b.id; 100 rows in set (0.51 sec)
使用延遲關聯查詢數據510ms ,使用基於書簽模式的解決方法減少到10ms以內 絕對是一個質的飛躍。
SELECT * FROM t1 where client_id='xxxxx' and is_valid=1 and id<47399727 order by id desc LIMIT 100; 100 rows in set (0.00 sec)
小結
根據主鍵定位數據的方式直接定位到主鍵起始位點,然后過濾所需要的數據。
相對比延遲關聯的速度更快,查找數據時少了二級索引掃描。但優化方法沒有銀彈,比如:
order by id desc 和 order by asc 的結果相差70ms ,生產上的案例有limit 100 相差1.3s ,這是為啥?
還有其他優化方式,比如在使用不到組合索引的全部索引列進行覆蓋索引掃描的時候使用 ICP 的方式 也能夠加快大分頁查詢。
子查詢優化
先定位偏移位置的 id,然后往后查詢,適於 id 遞增場景:
select * from orders_history where type=8 limit 100000,1; select id from orders_history where type=8 limit 100000,1; select * from orders_history where type=8 and id>=(select id from orders_history where type=8 limit 100000,1) limit 100; select * from orders_history where type=8 limit 100000,100;
4條語句的查詢時間如下:
第1條語句:3674ms
第2條語句:1315ms
第3條語句:1327ms
第4條語句:3710ms
- 1 V.S 2:select id 代替 select *,速度快3倍
- 2 V.S 3:速度相差不大
- 3 V.S 4:得益於 select id 速度增加,3的查詢速度快了3倍
這種方式相較於原始一般的查詢方法,將會增快數倍。
使用 id 限定優化
假設數據表的id是連續遞增,則根據查詢的頁數和查詢的記錄數可以算出查詢的id的范圍,可使用 id between and:
select * from order_history where c = 2 and id between 1000000 and 1000100 limit 100;
查詢時間:
15ms
12ms
9ms
這能夠極大地優化查詢速度,基本能夠在幾十毫秒之內完成。
限制是只能使用於明確知道id。
另一種寫法:
select * from order_history where id >= 1000001 limit 100;
還可以使用 in,這種方式經常用在多表關聯時進行查詢,使用其他表查詢的id集合,來進行查詢:
select * from order_history where id in (select order_id from trade_2 where goods = 'pen') limit 100;
臨時表
已經不屬於查詢優化,這兒附帶提一下。
對於使用 id 限定優化中的問題,需要 id 是連續遞增的,但是在一些場景下,比如使用歷史表的時候,或者出現過數據缺失問題時,可以考慮使用臨時存儲的表來記錄分頁的id,使用分頁的id來進行 in 查詢。這樣能夠極大的提高傳統的分頁查詢速度,尤其是數據量上千萬的時候。
數據表的id
一般在DB建立表時,強制為每一張表添加 id 遞增字段,方便查詢。
像訂單庫等數據量很大,一般會分庫分表。這時不推薦使用數據庫的 id 作為唯一標識,而應該使用分布式的高並發唯一 id 生成器,並在數據表中使用另外的字段來存儲這個唯一標識。
先使用范圍查詢定位 id (或者索引),然后再使用索引進行定位數據,能夠提高好幾倍查詢速度。即先 select id,然后再 select *。
參考