大数据分页方案

软件开发中,经常使用要用到分页、计算总数,数据量超过千万、上亿的时候,每每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 操做 (操做数据库就要涉及到磁盘读写)。

相关文章
相关标签/搜索