LIMIT语句优化
LIMIT语句使用规则
limit<offset>, <size>
- offset:返回结果第一行的偏移量(想要跳过多少行)
- size:指定返回多少条
举例说明
-- 查询第1页时,花费92ms
SELECT * FROM employees LIMIT 0, 10;
-- 查询第300001页时,花费174ms
SELECT * FROM employees LIMIT 300000, 10;
发现问题
可以看到offset越大,花费的时间就越长
使用EXPLAIN分析
EXPLAIN SELECT * FROM employees LIMIT 300000, 10;
可以看到type是ALL,表示全表扫描,会先扫描300000行数据,再丢弃,之后再取10行。
所以针对offset非常大的情况,就需要做一系列的分页优化
方案一
-- 方案一:覆盖索引,花费108mms
SELECT emp_no FROM employees LIMIT 300000, 10;
使用EXPLAIN分析
EXPLAIN SELECT emp_no FROM employees LIMIT 300000, 10;
可以看到type是index,表示全索引扫描,和ALL类似,只不过index是全盘扫描了索引的数据。index较ALL提升还是很大的
如果我们确实需要返回所有行,怎么办?可以见下面几种方案
方案二
-- 方案二:覆盖索引+join,花费109ms
SELECT * FROM employees e INNER JOIN (SELECT emp_no FROM employees LIMIT 300000, 10) t ON e.emp_no = t.emp_no;
-- 或
SELECT * FROM employees e INNER JOIN (SELECT emp_no FROM employees LIMIT 300000, 10) t USING(emp_no);
方案三
-- 方案三:覆盖索引+子查询,花费126ms
SELECT * FROM employees WHERE emp_no >= (SELECT emp_no FROM employees LIMIT 300000, 1) LIMIT 10;
方案四
-- 方案四:范围查询+limit语句
SELECT * FROM employees LIMIT 0, 10;
SELECT * FROM employees WHERE emp_no > 10010 LIMIT 10;
-- ...
SELECT * FROM employees WHERE emp_no > 499975 LIMIT 10;
这样的好处是,不管查询第几页的结果,需要扫描的行数永远都是10行
但是这种方案的前提是,需要拿到上一页的主键最大值,否则这个方案无法实施!
方案五
-- 方案五:如果能获得起始主键值和结束主键值
SELECT * FROM employees WHERE emp_no BETWEEN 499976 AND 499985;
方案六
禁止传入过大页码。
可以参考百度,例如:https://www.baidu.com/s?wd=a&pn=60&oq=a&ie=utf-8&usm=2&rsv_pq=d46785140005b625&rsv_t=1dd0FWJWTyx3Z%2FiNoz3FWYTNvGJ1i2haq1J1%2BhWFGJRhPGzLWhtJYC7GWnM
这是查询a这个关键字第7页的url,pn与页码相关,如果pn=990代表查询第100页的数据,通过搜索发现无法查询,跳转到第1页。如果pn=750代表查询第76页的数据,通过搜索发现最大页码为76
假设百度是使用mysql,每页展示10条,最多展示76页,这个偏移量offset最大才750,此时sql随便写不需要优化。
而且,百度这种方式,大部分情况下还是比较合理的,因为很少有人查询某个内容需要到特别靠后的页,76页完全够用。
总结
以上方案全都可以用到生产中,如果查询字段能覆盖需求字段,则优先方案一,方案二和方案三为业界最广泛的解决方案。方案四和方案五是针对能获取到起始主键值和结束主键值。方案六则告诉我们sql写得再好不如从业务角度着手。