大數據分頁方案

軟件開發中,經常使用要用到分頁、計算總數,數據量超過千萬、上億的時候,每每count 的須要超過 1s 的執行時間,甚至 3-5s,對於一個追求性能的前沿團隊來講,這個不能忍啊!mysql

爲何會慢?

mysql 會對全部符合的條件作一次掃描。web

select count(*) from table_a where a = '%d' ...

若是 a=%d 的數據有 1000W 條,那麼數據庫就會掃描一次 1000W 條數據庫。若是不帶查詢條件,那這種全表掃描將更可怕。sql

count(*) 和 count(1)、count(0)

  • count(expr) 爲統計 expr 不爲空的記錄數據庫

  • count(*) 它會計算總行數,無論你字段是否有值都會列入計算範圍。緩存

  • coount(0),count(1) 沒有差異,它會計算總行數微信

Example 1:

mysql> explain extended select count(*) from user;
...
1 row in set, 1 warning (0.34 sec)

mysql> show warnings;
+-------+------+--------------------------------------------------+
| Level | Code | Message |
+-------+------+--------------------------------------------------+
| Note | 1003 | select count(0) AS `count(*)` from `user` |

Example 2:

mysql> select count(*) from login_log
 -> ;
+----------+
| count(*) |
+----------+
| 2513 |
+----------+
1 rows in set (0.00 sec)

mysql> select count(logoutTime) from login_log;
+-------------------+
| count(logoutTime) |
+-------------------+
| 308 |
+-------------------+
1 rows in set (0.00 sec)

怎麼解決?

MyISAM DB

MyISAM 引擎很容易得到總行數的統計,查詢速度變得更快。由於 MyISAM 存儲引擎已經存儲了表的總行數。
MyISAM 會爲每張表維護一個 row count 的計數器,每次新增長一行,這個計數器就加 1。可是若是有查詢條件,那麼 MyISAM 也 game over 了,MyISAM 引擎不支持條件緩存。性能

On MyISAM, doing a query that does SELECT COUNT(*) FROM {some_table}, is very fast, since MyISAM keeps the information in the index

其餘 DB 引擎

受到 MySIAM DB 的啓發,咱們能夠手動維護總數緩存在表的索引中了。網站

  1. 若是 ID 連續,且基本不會斷開。直接取最大值 IDui

  2. 若是表中存在連續的數字列並設爲索引,那麼經過頁碼便可計算出此字段的範圍,直接做範圍查詢便可:spa

    start = (page-1)*pagesize+1 
    end = page*pagesize 
    select * from table where id >start and id <=end
  3. 涉及到總數操做,專門維護一個總數。新增一個用戶,總數值加 1, 須要總數的時候直接拿這個總數, 好比分頁時。若是有多個條件,那麼就須要維護多個總數列。該方案的擴展性更好,隨着用戶表數量增大, 水平切分用戶表,要獲取用戶總數,直接查詢這個總數表便可。

分頁正反偏移

數據庫自帶的 skip 和 limit 的限制條件爲咱們建立了分頁的查詢方式,可是若是利用不對,性能會出現千倍萬倍差別。
簡單一點描述:limit 100000,20 的意思掃描知足條件的 100020 行,扔掉前面的 100000 行,返回最後的 20 行,問題就在這裏。若是我反向查詢 oder by xx desc limit 0,20,那麼我只要索引 20 條數據。

Example 3

mysql> select count(*) from elastic_task_log_copy;
+----------+
| count(*) |
+----------+
| 1705162 |
+----------+
1 rows in set (2.31 sec)

正向偏移查詢。超級浪費的查詢,須要先 skip 大量的符合條件的查詢。

mysql> select id from elastic_task_log_copy order by id asc limit 1705152,10;
+---------+
| id |
+---------+
| 1705157 |
| 1705158 |
| 1705159 |
| 1705160 |
| 1705161 |
| 1705162 |
| 1705163 |
| 1705164 |
| 1705165 |
| 1705166 |
+---------+
10 rows in set (2.97 sec)

反向偏移查詢。一樣的查詢結果,千差萬別的結果。

mysql> select id from elastic_task_log_copy order by id desc limit 0,10;
+---------+
| id |
+---------+
| 1705166 |
| 1705165 |
| 1705164 |
| 1705163 |
| 1705162 |
| 1705161 |
| 1705160 |
| 1705159 |
| 1705158 |
| 1705157 |
+---------+
10 rows in set (0.01 sec)

這兩條 sql 是爲查詢最後一頁的翻頁 sql 查詢用的。因爲一次翻頁每每只須要查詢較小的數據,如 10 條,但須要向後掃描大量的數據,也就是越日後的翻頁查詢,掃描的數據量會越多,查詢的速度也就愈來愈慢。

因爲查詢的數據量大小是固定的,若是查詢速度不受翻頁的頁數影響,或者影響最低,那麼這樣是最佳的效果了(查詢最後最幾頁的速度和開始幾頁的速度一致)。

在翻頁的時候,每每須要對其中的某個字段作排序(這個字段在索引中),升序排序。那麼可不能夠利用索引的有序性 來解決上面遇到的問題。

好比有 10000 條數據須要作分頁,那麼前 5000 條作 asc 排序,後 5000 條 desc 排序,在 limit startnum,pagesize 參數中做出相應的調整。

可是這無疑給應用程序帶來複雜,這條 sql 是用於論壇回覆帖子的 sql,每每用戶在看帖子的時候,通常都是查看前幾頁和最後幾頁,那麼在翻頁的時候最後幾頁的翻頁查詢採用 desc 的方式來實現翻頁,這樣就能夠較好的提升性能。

遊標:上一頁的最大值或者最小值

若是你知道上一頁和下一頁的臨界值,那麼翻頁查詢也是信手拈來了,直接就告訴了數據庫個人起始查詢在哪,也就沒有什麼性能問題了。我更願意稱這個東西爲遊標 (Cursor)。
若是作下拉刷新,那麼就直接避免掉分頁的問題了。根據上一頁的最後一個值去請求新數據。

mysql> select id from elastic_task_log_copy where id >= 1699999 limit 10;
+---------+
| id |
+---------+
| 1699999 |
| 1700000 |
| 1700001 |
| 1700002 |
| 1700003 |
| 1700004 |
| 1700005 |
| 1700006 |
| 1700007 |
| 1700008 |
+---------+
10 rows in set (0.01 sec)

緩存和不精準

數據量達到必定程度的時候,用戶根本就不關心精準的總數, 沒人關心差幾個。看看知乎、微博、微信訂閱號,不精準的統計處處都是。

若是每次點擊分頁的時候都進行一次 count 操做,那速度確定不會快到哪裏去。他們通常也是採用計數器的辦法。每次新增長一個粉絲,就把值加 1,直接在用戶信息存儲一個總數,一段時間後從新查詢一次,更新該緩存。這樣分頁的時候直接拿這個總數進行分頁,顯示的時候直接顯示模糊之就行。

那爲何微信公衆號的閱讀量只有 10W+ 這個量級呢?100W+ 級去哪了!

其餘大神的建議

  1. mysql 的數據查詢, 大小字段要分開, 這個仍是有必要的, 除非一點就是你查詢的都是索引內容而不是表內容, 好比只查詢 id 等等

  2. 查詢速度和索引有很大關係也就是索引的大小直接影響你的查詢效果, 可是查詢條件必定要創建索引, 這點上注意的是索引字段不能太多,太多索引文件就會很大那樣搜索只能變慢,

  3. 查詢指定的記錄最好經過 Id 進行 in 查詢來得到真實的數據. 其實不是最好而是必須,也就是你應該先查詢出複合的 ID 列表, 經過 in 查詢來得到數據

  4. mysql 千萬級別數據確定是沒問題的, 畢竟如今的流向 web2.0 網站大部分是 mysql 的

  5. 合理分表也是必須的, 主要涉及橫向分表與縱向分表, 如把大小字段分開, 或者每 100 萬條記錄在一張表中等等, 像上面的這個表能夠考慮經過 uid 的範圍分表, 或者經過只創建索引表, 去掉相對大的字段來處理.

  6. count() 時間比較長, 可是自己是能夠緩存在數據庫中或者緩存在程序中的, 由於咱們當時使用在後臺因此第一頁比較慢可是後面比較理想

  7. SELECT id 相對 SELECT 差距仍是比較大的, 能夠經過上面的方法來使用 SELECT id + SELECT ... IN 查詢來提升性能

  8. 必要的索引是必須的, 仍是要儘可能返回 5%-20% 的結果級別其中小於 5% 最理想;

  9. mysql 分頁的前面幾頁速度很快, 越向後性能越差, 能夠考慮只帶上一頁, 下一頁不帶頁面跳轉的方法, 呵呵這個比較垃圾可是也算是個方案, 只要在先後多查一條就能解決了. 好比 100,10 你就差 99,12 呵呵,這樣看看先後是否有結果.

  10. 前臺仍是要經過其餘手段來處理, 好比 lucene/Solr+mysql 結合返回翻頁結果集, 或者上面的分表

  11. 總數多是存在內存中, 這樣分頁計算的時候速度很快。累加操做的時候將內存中的值加 1。總數這個值要持久化,仍是要存到磁盤上的,也就是數據庫中 (能夠是關係型數據庫,也能夠是 mongdb 這樣的數據庫很適合存儲計數)。把總數放在內存中,只是避免頻繁的磁盤 i/0 操做 (操做數據庫就要涉及到磁盤讀寫)。

相關文章
相關標籤/搜索