大表分頁查詢優化
文章目錄
原文地址:https://www.slideshare.net/suratbhati/efficient-pagination-using-mysql-6187107
1. 分頁查詢核心點
– 使用索引來過濾 rows (解決通過 WHERE)
– 使用相同的索引按排序順序返回行 (解決通過 ORDER)
參考資料:
– http://dev.mysql.com/doc/refman/5.1/en/mysql-indexes.html
– http://dev.mysql.com/doc/refman/5.1/en/order-by-optimization.html
– http://dev.mysql.com/doc/refman/5.1/en/limit-optimization.html
2 索引使用
建立索引 a_b_c (a, b, c)
ORDER
將會使用索引
– ORDER BY a
– ORDER BY a,b
– ORDER BY a, b, c
– ORDER BY a DESC, b DESC, c DESC
WHERE 和 ORDER 都將使用索引:
– WHERE a = const ORDER BY b, c
– WHERE a = const AND b = const ORDER BY c
– WHERE a = const ORDER BY b, c
– WHERE a = const AND b > const ORDER BY b, c
ORDER 將不會使用索引 (文件排序)
– ORDER BY a ASC, b DESC, c DESC /* 多種排序方向 */
– WHERE g = const ORDER BY b, c /* a 前綴不存在 */
– WHERE a = const ORDER BY c /* b 丟失 */
– WHERE a = const ORDER BY a, d /* d 不在索引中 */
3. 使用實例
我們將通過兩種使用情景來分析分頁:
• 通過時間分頁, 最近的消息排序分頁 可以直接使用id代替
• 通過thumps_up(投票數), 點贊數多的排前面
3.1 表結構
CREATE TABLE `message` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
`user_id` int(11) NOT NULL,
`content` text COLLATE utf8_unicode_ci NOT NULL,
`create_time` int(11) NOT NULL,
`thumbs_up` int(11) NOT NULL DEFAULT 0, /* Vote Count */
PRIMARY KEY (`id`),
KEY `thumbs_up_key` (`thumbs_up`,`id`)
) ENGINE=InnoDB
查看錶相關信息
mysql> show table status like message \G;
Engine: InnoDB
Version: 10
Row_format: Compact
Rows: 50000040 /* 50 Million */
Avg_row_length: 565
Data_length: 28273803264 /* 26 GB */
Index_length: 789577728 /* 753 MB */
Data_free: 6291456
Create_time: 2009-04-20 13:30:45
3.2 典型的查詢
3.2.1 獲取總的記錄數
SELECT count(*) FROM message
3.2.2 獲取當前頁信息
SELECT * FROM message ORDER BY id DESC LIMIT 0, 20
注意: id 是自增的, 和 create_time 順序相同, 不需要在create_time創建索引, 節省空間
3.3 查看性能
mysql> explain SELECT * FROM message ORDER BY id DESC LIMIT 10000, 20 \G;
***************** 1. row **************
id: 1
select_type: SIMPLE
table: message
type: index
possible_keys: NULL
key: PRIMARY
key_len: 4
ref: NULL
rows: 10020
Extra:
1 row in set (0.00 sec)
– 能使用索引掃描和讀取行並在找到需要的行數後停止.
– LIMIT 10000, 20 意味着他需要讀取 10020 行,並且拋棄前 10000 行, 然後返回接下來的 20 行.
3.4 性能影響
– 非常大的 OFFSET 將會帶來數據集的顯著增長, MySQL 將攜帶數據到內存並不會返回給調用者
– 當您擁有不適合主內存的數據庫時,性能問題會更加明顯.
– 使用大型OFFSET的小百分比請求將能夠達到磁盤 I/O 瓶頸
– 爲了顯示 1000,000 行數據中的 21 - 40數據 , 有時候需要計算1000,000行.
4. 解決方案
4.1 簡單解決方案
– 不要顯示中的記錄數, 用戶真的會關心嗎?
– 不要讓用戶跳轉到更深層的頁碼所在的頁面
4.2 避免使用Count(*)
- 絕不顯示總的信息, 讓用戶查看更多信息通過點擊next
- 不要計算每個請求, 緩存他, 顯示陳舊的計數, 用戶不會關心是 324533 還是 324633
- 顯示 41 to 80 of Thousands
- 使用預先計算(觸發器或緩存), insert/delete 發生時自增或自減相應的值.
更多參考:https://blog.csdn.net/wujiangwei567/article/details/88721395
4.3 避免使用offset
我們要改變用戶習慣:
– 不要直接跳轉到第 N 頁
– LIMIT N
是好的, 不要使用 LIMIT M,N
– 提供給定起始頁面的額外where
條件
– 使用給定的線索和更多的WHERE
條件 以及ORDER BY
和不帶OFFSET
的 LIMIT N
5 使用更多條件加快搜索
5.1 創建時間分頁
比如說每頁顯示5條信息 對應的頁碼,列表id和相應的上一頁下一頁鏈接如下
頁碼 | 信息id數 | 鏈接 |
---|---|---|
第一頁 | 150 111 102 101 100 | Next: page=2&last_seen=100&dir=next |
第二頁 | 98 97 96 95 94 | Prev: page=1&last_seen=98&dir=prev Next: page=3&last_seen=94&dir=next |
第三頁 | 93 92 91 90 89 | Prev: page=2&last_seen=93&dir=prev Next: page=4&last_seen=89&dir=next |
根據頁碼信息,我們是按照時間排序,而創建時間和id的順序是一致的,所以我們可以進行如下篩選來達到
更快的查詢體驗
Next(下一頁)
WHERE id < 100 /* 通過參數 last_seen 獲取 */
ORDER BY id DESC LIMIT 5 /* 沒有 OFFSET 偏移量,顯示5條數據 */
Prev(前一頁)
WHERE id > 98 /* 通過參數 last_seen 獲取 */
ORDER BY id ASC LIMIT 5 /* 沒有 OFFSET 偏移量,顯示5條數據 */
這裏前一頁的數據返回時如果按照最近的思想,我們需要對結果集做一個倒序的排序
性能解析:
mysql> explain SELECT * FROM message WHERE id < 49999961 ORDER BY id DESC LIMIT 20 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: message
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
Rows: 25000020 /* ignore this */
Extra: Using where
1 row in set (0.00 sec)
5.2 投票數分頁
我們假設 每頁的投票數和對應的鏈接如下:
頁碼 | 投票數 | 鏈接 |
---|---|---|
第一頁 | 99 99 98 98 98 | Next: page=2&last_seen=100&dir=next |
第二頁 | 98 98 97 97 10 | Prev: page=1&last_seen=98&dir=prev Next: page=3&last_seen=94&dir=next |
我們不能使用:
WHERE thumbs_up < 98 ORDER BY thumbs_up DESC /* 他將會返回很少的行 */
那我們可以這樣做麼:
WHERE thumbs_up <= 98 AND <額外的條件> ORDER BY thumbs_up DESC
5.2.1 尋找額外的條件
• 考慮thumbs_up作爲主要的編號
– 如果我們有額外的次要編號,我們可以使用major和minor的組合作爲額外條件
• 找到額外的列 (次要編號)
– 我們可以使用id主鍵作爲次要編號
5.2.2 解決方案
第一頁
mysql> SELECT thumbs_up, id FROM message ORDER BY thumbs_up DESC, id DESC LIMIT 5
+-----------+----+
| thumbs_up | id |
+-----------+----+
| 99 | 14 |
| 99 | 2 |
| 98 | 18 |
| 98 | 15 |
| 98 | 13 |
+-----------+----+
Next(下一頁)
mysql> SELECT thumbs_up, id FROM message WHERE thumbs_up <= 98 AND (id < 13 OR thumbs_up < 98) ORDER BY thumbs_up DESC, id DESC LIMIT 5
+-----------+----+
| thumbs_up | id |
+-----------+----+
| 98 | 10 |
| 98 | 6 |
| 97 | 17 |
+-----------+----+
解釋一下子: 我們這裏投票數 thumbs_up
相同的前提下 id做的是倒序排列,那麼如果是相同的thumbs_up
下一頁的id肯定小於 13 或者 thumbs_up 小於當前數
5.2.3 更好的方式
SELECT *
FROM message
WHERE thumbs_up <= 98 AND (id < 13 OR thumbs_up < 98)
ORDER BY thumbs_up DESC, id DESC LIMIT 20
改寫爲:
SELECT m2.*
FROM message m1, message m2
WHERE m1.id = m2.id AND m1.thumbs_up <= 98 AND (m1.id < 13 OR m1.thumbs_up < 98)
ORDER BY m1.thumbs_up DESC, m1.id DESC LIMIT 20;
我們看一下性能分析
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: m1
type: range possible_keys: PRIMARY,thumbs_up_key
key: thumbs_up_key /* (thumbs_up,id) */
key_len: 4
ref: NULL
Rows: 25000020 /*忽略這個 我們僅僅讀 20 行*/
Extra: Using where; Using index /* Cover */
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: m2
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: forum.m1.id
rows: 1
Extra:
我們僅僅做了一個全連接,m2 僅僅掃描了1行 也是用了key和對應的索引
有的人說使用
SELECT *
FROM
message
WHERE
(thumbs_up,id) > (98,14)
ORDER BY thumbs_up, id
LIMIT 5
這樣寫確實可以提高效率,但是仍然沒有上面的兩種寫法快
6. 爲什麼不能使用offest
主鍵排序 和 次鍵排序 如果使用offest 都會顯著降低性能
通過統計圖表可以看出:
– Using LIMIT OFFSET, N
• 600 query/sec
– Using LIMIT N (no OFFSET)
• 3.7k query/sec
7. google在做什麼
. 估計有多少結果。再一次,谷歌這樣做,沒有人抱怨
. 不要顯示所有結果。甚至谷歌也不會讓你看到百萬分之一的結果。
. 不顯示總計數或其他頁面的中間鏈接。僅顯示“下一個”鏈接。
碼字不易,打賞一下小弟吧