zoukankan      html  css  js  c++  java
  • MySql高效分页

    在Percona Performance Conference 2009大会上来自yahoo的Surat Singh Bhati (surat@yahoo-inc.com) 和 Rick James (rjames@yahoo-inc.com)给大家分享了MySQL高效分页的经验。

    一、概述

    • 常见分页方式
    • schema设计和常见的分页方式(偏移)
    • 避免分页偏移过大的技巧
    • 性能对比
    • 重点


    二、常见分页方式

    三.前提
    大记录表要高效分页

    • WHERE条件使用索引完成
    • WHERE条件和排序能够使用同个索引完成

    基础知识


    索引 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 BY a ASC, b DESC, c DESC /* 混合ASC和DESC */
    • WHERE g = const ORDER BY b, c /* 字段g不是索引一部分 */
    • WHERE a = const ORDER BY c /* 没有使用字段b */
    • WHERE a = const ORDER BY a, d /* 字段d不是索引的一部分 */


    四、Schema 设计

    Java代码 复制代码
    1. CREATE TABLE `message` (   
    2. `id` int(11) NOT NULL AUTO_INCREMENT,    
    3. `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,    
    4. `user_id` int(11) NOT NULL,   
    5. `content` text COLLATE utf8_unicode_ci NOT NULL,   
    6. `create_time` int(11) NOT NULL,   
    7. `thumbs_up` int(11) NOT NULL DEFAULT '0'/* 投票数 */  
    8. PRIMARY KEY (`id`),   
    9. KEY `thumbs_up_key` (`thumbs_up`,`id`)   
    10. ) ENGINE=InnoDB   
    11.   
    12. mysql> show table status like 'message' \G   
    13. Engine: InnoDB   
    14. Version: 10  
    15. Row_format: Compact   
    16. Rows: 50000040 /* 5千万 */  
    17. Avg_row_length: 565  
    18. Data_length: 28273803264 /* 26 GB */  
    19. Index_length: 789577728 /* 753 MB */  
    20. Data_free: 6291456  
    21. Create_time: 2009-04-20 13:30:45  
    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', /* 投票数 */
    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 /* 5千万 */
    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


    两个分页例子:

    • 按照time(发布时间)分页,新发布的在前面
    • 按照thumps_up(投票数)分页,票高的在前面


    五、典型的分页查询
    1.统计记录数量
    SELECT count(*) FROM message

    2. 查询当前页
    SELECT * FROM message ORDER BY id DESC LIMIT 0, 20

    • http://domain.com/message?page=1
    •               ORDER BY id DESC LIMIT 0, 20
    • http://domain.com/message?page=2
    •               ORDER BY id DESC LIMIT 20, 20
    • http://domain.com/message?page=3
    •               ORDER BY id DESC LIMIT 40, 20


    提示:id 是自动增长的(auto_increment),通过id就可以取得最新的列表,不需要创建专门记录时间的字段。
    六、explain

    Java代码 复制代码
    1. mysql> explain SELECT * FROM message   
    2. ORDER BY id DESC   
    3. LIMIT 1000020\G   
    4. ***************** 1. row **************   
    5. id: 1  
    6. select_type: SIMPLE   
    7. table: message   
    8. type: index   
    9. possible_keys: NULL   
    10. key: PRIMARY   
    11. key_len: 4  
    12. ref: NULL   
    13. rows: 10020  
    14. Extra:   
    15.  1 row in set (0.00 sec)  
    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 需要读取前10000行,然后获取后面的20行


    六、瓶颈

    • 较大的偏移(OFFSET)会增加结果集, MySQL has to bring data in memory that is never returned to caller.
    • Performance issue is more visible when your have database that can't fit in main memory.
    • 小比例的低效分页足够产生磁盘I/O瓶颈
    • 为了显示“第 21条 至 40条 (共 1000000),需要统计1000000行


    七、简单的解决方法

    1. 不显示记录总数,没用户在乎这个数字
    2. 不让用户访问页数比较大的记录,重定向他们


    八、避免count(*)

    1. 不显示总数,让用户通过“下一页”来翻页
    2. 缓存总数,显示一个大概值,没有用户在乎是324533条还是324633 (译:测试在乎-_-!!)
    3. Display 41 to 80 of Thousands
    4. 单独统计总数,在插入和删除时递增/递减


    九、解决偏移查询

    1. 更改ui,不提供跳到某页的按钮
    2. LIMIT N 是高效的, 但不要使用 LIMIT M,N
      • 从WHERE条件里找到分页(LIMIT N)的线索
      • Find the desired records using more restricted WHERE using given clue and ORDER BY and LIMIT N without OFFSET)


    十、寻找线索

    译:last_seen是id。这里的分页只有“上一页”、“下一页” 按钮
    十一、根据线索解决方案
    下一页:

    Java代码 复制代码
    1. http://domain.com/forum?page=2&last_seen=100&dir=next   
    2.   
    3. WHERE id < 100 /* last_seen */  
    4. ORDER BY id DESC LIMIT $page_size /* 没有偏移 */  
    http://domain.com/forum?page=2&last_seen=100&dir=next
    
    WHERE id < 100 /* last_seen */
    ORDER BY id DESC LIMIT $page_size /* 没有偏移 */


    上一页:

    Java代码 复制代码
    1. http://domain.com/forum?page=1&last_seen=98&dir=prev   
    2.   
    3. WHERE id > 98 /* last_seen */  
    4. ORDER BY id ASC LIMIT $page_size /* 没有偏移 */  
    http://domain.com/forum?page=1&last_seen=98&dir=prev
    
    WHERE id > 98 /* last_seen */
    ORDER BY id ASC LIMIT $page_size /* 没有偏移 */
    


    译:通过每页第一条或最后一条记录的id来做条件筛选,再配合降序和升序获得上/下一页的结果集
    十二、根据线索解决方案

    Java代码 复制代码
    1. mysql> explain   
    2. SELECT * FROM message   
    3. WHERE id < '49999961'  
    4. ORDER BY id DESC LIMIT 20 \G   
    5. *************************** 1. row ***************************   
    6. id: 1  
    7. select_type: SIMPLE   
    8. table: message   
    9. type: range   
    10. possible_keys: PRIMARY   
    11. key: PRIMARY   
    12. key_len: 4  
    13. ref: NULL   
    14. Rows: 25000020 /* 忽略这里 */  
    15. Extra: Using where   
    16. 1 row in set (0.00 sec)  
    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 /* 忽略这里 */
    Extra: Using where
    1 row in set (0.00 sec)


    十三、当你排序的字段不是唯一的,怎么办?

    引用
    99
    99
    98 第一页
    98
    98

    98
    98
    97 第二页
    97
    10


    我们不能这样查询:

    Java代码 复制代码
    1. WHERE thumbs_up < 98  
    2. ORDER BY thumbs_up DESC /* 结果将返回重复的记录 */  
    WHERE thumbs_up < 98
    ORDER BY thumbs_up DESC /* 结果将返回重复的记录 */


    我们可以这样查询:

    Java代码 复制代码
    1. WHERE thumbs_up <= 98  
    2. AND <额外的条件>   
    3. ORDER BY thumbs_up DESC  
    WHERE thumbs_up <= 98
    AND <额外的条件>
    ORDER BY thumbs_up DESC


    十四、额外的条件

    • 考虑到 thumbs_up 是“主要字段”,如果我们添加一个“次要字段”,我们可以使用“主要字段”和“次要字段”作为查询条件
    • 其次,我们可以考虑使用id(primary key)作为我们的次要字段


    十五、解决方案
    第一页:

    Java代码 复制代码
    1. SELECT thumbs_up, id   
    2. FROM message   
    3. ORDER BY thumbs_up DESC, id DESC   
    4. LIMIT $page_size   
    5. +-----------+----+   
    6. | thumbs_up | id |   
    7. +-----------+----+   
    8. 99 | 14 |   
    9. 99 | 2 |   
    10. 98 | 18 |   
    11. 98 | 15 |   
    12. 98 | 13 |   
    13. +-----------+----+  
    SELECT thumbs_up, id
    FROM message
    ORDER BY thumbs_up DESC, id DESC
    LIMIT $page_size
    +-----------+----+
    | thumbs_up | id |
    +-----------+----+
    | 99 | 14 |
    | 99 | 2 |
    | 98 | 18 |
    | 98 | 15 |
    | 98 | 13 |
    +-----------+----+



    下一页:

    Java代码 复制代码
    1. SELECT thumbs_up, id   
    2. FROM message   
    3. WHERE thumbs_up <= 98 AND (id < 13 OR thumbs_up < 98)   
    4. ORDER BY thumbs_up DESC, id DESC   
    5. LIMIT $page_size   
    6. +-----------+----+   
    7. | thumbs_up | id |   
    8. +-----------+----+   
    9. 98 | 10 |   
    10. 98 | 6 |   
    11. 97 | 17 |  
    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 $page_size
    +-----------+----+
    | thumbs_up | id |
    +-----------+----+
    | 98 | 10 |
    | 98 | 6 |
    | 97 | 17 |


    十六、优化
    查询:

    Java代码 复制代码
    1. SELECT * FROM message   
    2. WHERE thumbs_up <= 98  
    3. AND (id < 13 OR thumbs_up < 98)   
    4. ORDER BY thumbs_up DESC, id DESC   
    5. LIMIT 20  
    SELECT * FROM message
    WHERE thumbs_up <= 98
    AND (id < 13 OR thumbs_up < 98)
    ORDER BY thumbs_up DESC, id DESC
    LIMIT 20


    我们可以这样写:

    Java代码 复制代码
    1. SELECT m2.* FROM message m1, message m2   
    2. WHERE m1.id = m2.id   
    3. AND m1.thumbs_up <= 98  
    4. AND (m1.id < 13 OR m1.thumbs_up < 98)   
    5. ORDER BY m1.thumbs_up DESC, m1.id DESC   
    6. 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;


    十七、explain

    Java代码 复制代码
    1. *************************** 1. row ***************************   
    2. id: 1  
    3. select_type: SIMPLE   
    4. table: m1   
    5. type: range   
    6. possible_keys: PRIMARY,thumbs_up_key   
    7. key: thumbs_up_key /* (thumbs_up,id) */  
    8. key_len: 4  
    9. ref: NULL   
    10. Rows: 25000020 /* 忽略这里 */  
    11. Extra: Using where; Using index /* Cover 译:Cover就是说所需要的数据之从索引里获取就可以满足了 */  
    12. *************************** 2. row ***************************   
    13. id: 1  
    14. select_type: SIMPLE   
    15. table: m2   
    16. type: eq_ref   
    17. possible_keys: PRIMARY   
    18. key: PRIMARY   
    19. key_len: 4  
    20. ref: forum.m1.id   
    21. rows: 1  
    22. Extra:  
    *************************** 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 /* 忽略这里 */
    Extra: Using where; Using index /* Cover 译: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:


    十八、性能提升



    十九、吞吐量提升
    每页30条记录,查看第一页的话,使用 LIMIT OFFSET, N方式,可以达到 600 次查询/秒,如果使用 LIMIT N (无偏移)方式,提升到 3.7k 次查询/秒
    二十、Bonus Point
    Product issue with LIMIT M, N

    User is reading a page, in the mean time some records may be added to
    previous page.

    Due to insert/delete pages records are going to move forward/backward
    as rolling window:
    – User is reading messages on 4th page
    – While he was reading, one new message posted (it would be there on page
    one), all pages are going to move one message to next page.
    – User Clicks on Page 5
    – One message from page got pushed forward on page 5, user has to read it
    again

    No such issue with news approach
    二十一、不足
    SEO专家会说:Let bot reach all you pages with fewer number of deep dive

    两个解决方案:
    Two Solutions:
    • Read extra rows
    – Read extra rows in advance and construct links for few previous & next pages
    • Use small offset
    – Do not read extra rows in advance, just add links for few past & next pages
    with required offset & last_seen_id on current page
    – Do query using new approach with small offset to display desired page


    Additional concern: Dynamic urls, last_seen is not constant over time.

    原文地址:http://willko.javaeye.com/blog/608921

  • 相关阅读:
    【JAVAWEB学习笔记】网上商城实战3:购物模块和订单模块
    JAVAEE学习——hibernate01:简介、搭建、配置文件详解、API详解和CRM练习:保存客户
    【JAVAWEB学习笔记】网上商城实战2:异步加载分类、Redis缓存分类和显示商品
    征稿延期 | 2019亚洲语言处理国际大会(IALP2019)征稿延期
    征稿通知 | 2019亚洲语言处理国际大会(IALP2019)开始征稿
    ECNU·AntNLP主页船新上线!
    [NAACL19]无监督循环神经网络文法 (URNNG)
    [ICLR18]联合句法和词汇学习的神经语言模型
    [NAACL19]一个更好更快更强的序列标注成分句法分析器
    [EMNLP18]用序列标注来进行成分句法分析
  • 原文地址:https://www.cnblogs.com/yiki/p/1808019.html
Copyright © 2011-2022 走看看