【MySQL】分頁優化

前段時間因爲項目的緣由,對一個因爲分頁而形成性能較差的SQL進行優化,如今將優化過程當中學習到關於分頁優化的知識跟你們簡單分享下。html

分頁不外乎limit,offset,在這兩個關鍵字中,limit其實不是性能瓶頸的主要緣由,若是sql中定義了比較大的limit,說明了確實有一次性取出較多數據的需求,若是不是,就須要考慮limit參數是否須要調整了。這篇文章主要以offset爲優化方向,介紹高offset下的性能優化手段。業界主要使用的仍是Innodb引擎,本文中的分析和方案主要針對Innodb引擎,其餘存儲未必適用。sql

查詢執行原理

在開始介紹以前,咱們首先寫一個最簡單的分頁sql,而且簡單介紹下查詢的數據讀取原理,以便後面具體解決方案中進行對比。數據庫

Query1: select * from table order by id asc limit 1000 offset 9000;緩存

對於這個查詢,咱們首先來看看它的執行過程。性能優化

上圖是一個Innodb聚簇索引的圖示。因爲咱們的查詢沒有使用where子句,並且要求返回全部列,因此查詢會到聚簇索引中查詢。查詢在數據庫中的掃描過程爲:網絡

查詢首先會定位到第一條合乎條件的數據,也就是(15,34,Bob)這一行。 而後,根據這個葉子節點的首部信息,能夠得知這個葉子節點中有多少行的數據。 若是這個葉子節點的數據達到offset的要求,讀出數據直至達到limit要求。 若這個葉子節點沒法完成查詢,則經過指向兄弟節點的指針,根據排序要求,往下一個兄弟節點繼續查詢,直到知足limit與offset要求爲止。數據結構

緩存子查詢

咱們能夠想象一下大多數用戶翻頁的習慣,通常來講,用戶都會一頁一頁地翻。利用用戶的這一習慣,咱們能夠將上一頁的排序的最大/小值進行緩存,而後以此值做爲查詢傳遞到下一查詢中。 仍是以上面的那條sql爲例,咱們能夠把那條sql拆成相贊成義的父子查詢。咱們來看兩條查詢:性能

Query2: select * from table order by id limit 1000 offset 8000;學習

Query3: select * from table where id > (select id from test order by id limit 1 offset 8999) order by id limit 1000;測試

上面兩條sql,2查詢了8001~9000行,3查詢了9001~10000行,3中的子查詢返回了第9000行的id值。3不管意義仍是執行過程都與1相同,惟一不一樣的,就是它是繼承於2的。咱們能夠看到,它是以2的結果數據中,id最大的那條數據爲基礎進行查詢的。也就是說,在上一頁查詢後,咱們能夠將第9000條數據的id進行緩存,當須要查詢下一頁數據時,咱們就能夠直接將id替換3的子查詢。這樣遍歷就不須要從第1個節點開始,節省了接近9000次的遍歷。 注意與缺陷:

  • 須要肯定sql查詢時的數據順序:數據庫對錶及索引的維護默認採用升序維護,但在查詢使用不一樣的select對象時,選擇的索引是不肯定的,因此建議在sql中加上order by子句。
  • 數據須要相對靜態:若是數據在排序字段上變更頻繁,如上例中取出第9000條數據的id進行緩存,可是在進行下一頁查詢以前,前面任意一條數據被刪除,緩存的id就會失效。

覆蓋索引法

在查詢時,使用聚簇索引進行查詢,一定會進行大量的IO。這是,能夠考慮使用覆蓋索引,將查詢IO限定在索引中。在查詢時,有時咱們實際須要的並非整行的數據,而只是其中的某幾個字段,而當咱們select的數據列從索引中已經能夠取出,數據庫就不會對總表進行查詢。 好比如今須要取出資產排名第1000用戶的user_id,咱們只須要對assets與user_id創建聯合索引,查詢sql爲:

Query4: select user_id from table order by assets desc limit 1 offset 999;

因爲查詢的where條件與返回列都在同一個索引能夠知足篩選和返回的要求,查詢不會再到總表中進行大量IO。 這裏簡單介紹一下覆蓋索引優化的原理:

上圖是一個Innodb的二級索引存儲結構圖。圖中能夠看到,二級索引的葉子節點存儲的是最後一級的索引值、id、以及一個指向兄弟節點的指針。咱們假設一個塊存儲一個葉子節點。對於sql④,須要掃描1000個user_id,若是在總表中掃描,因爲每行數據佔用空間大,假設一個塊中能存100行,讀取一個塊只能掃描100個user_id,須要讀取10個塊才能完成;但若是將查詢限定在索引中,對於上述葉子節點中的數據結構,假設一個塊能存儲5000個索引值,只須要讀取一個塊就能完成咱們須要的offset 1000了。 若select的列是一個較大的數據列,如一段文本,或須要對全行數據取出時,能夠經過子查詢先將id查詢出來,而後使用id到總表中查詢。這是由於在二級索引葉子節點存儲了id值,因此id就是一個天生的覆蓋索引列。如上例中若是須要將全行數據取出,而不單是user_id一列,查詢能夠改成(深度學習能夠參考:http://www.cnblogs.com/zhiqian-ali/p/4916064.html):

$ = select id from table order by assets desc limit 1 offset 999;
select * from table where id = $;
or
select * from table a,( select id from table order by assets desc limit 1 offset 999 ) b where a.id=b.id;

反向查找法

在使用offset進行查詢時,參數值的大小對查詢性能的影響很是大:當offset參數較小時,查詢的性能很是高;但當offset的值逐漸增加,查詢的耗時開始變得不可控制,須要一個方法將高offset查詢的性能進行控制。優化的方法其實很簡單:在一個1000行的表中,順着數第1000條的數據,也正是倒着數第1條的數據。咱們只須要將數據進行倒序遍歷,就能夠將本來線性增加的查詢耗時,轉變爲一箇中間高、兩頭低的性能曲線了。如上述查詢可優化爲:

$ = select count(*) from table;
select * from table order by assets desc limit 1 offset ($ - 999);

在測試反向查找法的過程當中,發現耗時最高的並非在50%的位置,而是在60%的位置。這是由於在按照索引排序的順序進行遍歷時,索引的排布順序與磁盤讀取一致。當索引比較緊湊地存儲在連續的幾個塊時,因爲磁盤預讀,在一次IO中多個有效塊會被讀出,而反向遍歷時則沒法享受磁盤預讀帶來的IO優化。 磁盤預讀的優點依賴於索引的連續排布。當索引頻繁移動時,數據的連續排布沒法獲得保證。在決定反向遍歷臨界值時,須要考慮數據索引值變更的頻率的影響。若重建索引的是容許的,能夠按期進行索引重建,使得索引緊密排布。

反向查找法存在一個比較致命的缺陷,就是須要對錶進行count的操做:要想肯定反向offset參數,必須先得到總數量。對於行數量比較穩定的表,能夠直接使用定時刷新的緩存值;對於不須要進行事務操做的表,能夠考慮採用MyISAM引擎;而對於其餘狀況,目前還沒找到比較好的解決方案。

總結

上述的幾個方法各有優劣,沒有辦法選出一個解決分頁性能瓶頸的萬金油,須要結合實際應用場景進行一個或多個方案選用。

  • 緩存子查詢的方法適用於順序翻頁的場景,但要求數據在指定排序上的序號是穩定的,才能保證緩存值有效。
  • 覆蓋索引是一個適用性比較強的方法,與經常使用的利用索引優化查詢性能的方案同樣,它的缺點在於表修改時的性能降低,並且若是索引列的數據須要頻繁更新,會致使索引排布不整齊,查詢性能波動。
  • 反向查找法能夠對偏移量較大的查詢進行優化,但須要進行較高耗時的count查詢,對於count查詢的優化,目前只想到緩存與使用MyISAM引擎的辦法。

上述知識大部分經過網絡的資料蒐集,結合實驗測試進行總結,並無實際考察Mysql中的代碼實現,如存在錯誤請幫忙訂正。

相關文章
相關標籤/搜索