首页 > 数据库 > MySQL > 正文

MySQL中分页优化的实例详解

2024-07-24 13:07:11
字体:
来源:转载
供稿:网友

这篇文章主要介绍了MySQL中分页优化的实例详解,分页优化是MySQL优化当中的重点,需要的朋友可以参考下

通常,我们会采用ORDER BY LIMIT start, offset 的方式来进行分页查询。例如下面这个SQL:

 

 
  1. SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 100, 10; 

或者像下面这个不带任何条件的分页SQL:

 

 
  1. SELECT * FROM `t1` ORDER BY id DESC LIMIT 100, 10; 

一般而言,分页SQL的耗时随着 start 值的增加而急剧增加,我们来看下面这2个不同起始值的分页SQL执行耗时:

 

 
  1. yejr@imysql.com> SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 500, 10; 
  2. … 
  3.  
  4. 10 rows in set (0.05 sec) 
  5.  
  6.  
  7. yejr@imysql.com> SELECT * FROM `t1` WHERE ftype=6 ORDER BY id DESC LIMIT 935500, 10; 
  8. … 
  9.  
  10. 10 rows in set (2.39 sec) 

可以看到,随着分页数量的增加,SQL查询耗时也有数十倍增加,显然不科学。今天我们就来分析下,如何能优化这个分页方案。 一般滴,想要优化分页的终极方案就是:没有分页,哈哈哈~~~,不要说我讲废话,确实如此,可以把分页算法交给Sphinx、Lucence等第三方解决方案,没必要让MySQL来做它不擅长的事情。 当然了,有小伙伴说,用第三方太麻烦了,我们就想用MySQL来做这个分页,咋办呢?莫急,且待我们慢慢分析,先看下表DDL、数据量、查询SQL的执行计划等信息:

 

 
  1. yejr@imysql.com> SHOW CREATE TABLE `t1`; 
  2. CREATE TABLE `t1` ( 
  3. `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 
  4. ... 
  5. `ftype` tinyint(3) unsigned NOT NULL
  6. ... 
  7. PRIMARY KEY (`id`) 
  8. ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 
  9.  
  10. yejr@imysql.com> select count(*) from t1; 
  11. +----------+ 
  12. count(*) | 
  13. +----------+ 
  14. | 994584 | 
  15. +----------+ 
  16.  
  17. yejr@imysql.com> EXPLAIN SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 500, 10/G 
  18. *************************** 1. row *************************** 
  19. id: 1 
  20. select_type: SIMPLE 
  21. table: t1 
  22. type: index 
  23. possible_keys: NULL 
  24. keyPRIMARY 
  25. key_len: 4 
  26. ref: NULL 
  27. rows: 510 
  28. Extra: Using where 
  29.  
  30. yejr@imysql.com> EXPLAIN SELECT * FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500, 10/G 
  31. *************************** 1. row *************************** 
  32. id: 1 
  33. select_type: SIMPLE 
  34. table: t1 
  35. type: index 
  36. possible_keys: NULL 
  37. keyPRIMARY 
  38. key_len: 4 
  39. ref: NULL 
  40. rows: 935510 
  41. Extra: Using where 

可以看到,虽然通过主键索引进行扫描了,但第二个SQL需要扫描的记录数太大了,而且需要先扫描约935510条记录,然后再根据排序结果取10条记录,这肯定是非常慢了。 针对这种情况,我们的优化思路就比较清晰了,有两点:

1、尽可能从索引中直接获取数据,避免或减少直接扫描行数据的频率

2、尽可能减少扫描的记录数,也就是先确定起始的范围,再往后取N条记录即可

据此,我们有两种相应的改写方法:子查询、表连接,即下面这样的:

#采用子查询的方式优化,在子查询里先从索引获取到最大id,然后倒序排,再取10行结果集

#注意这里采用了2次倒序排,因此在取LIMIT的start值时,比原来的值加了10,即935510,否则结果将和原来的不一致

 

 
  1. yejr@imysql.com> EXPLAIN SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC/G 
  2. *************************** 1. row *************************** 
  3. id: 1 
  4. select_type: PRIMARY 
  5. table: <derived2> 
  6. type: ALL 
  7. possible_keys: NULL 
  8. keyNULL 
  9. key_len: NULL 
  10. ref: NULL 
  11. rows: 10 
  12. Extra: Using filesort 
  13. *************************** 2. row *************************** 
  14. id: 2 
  15. select_type: DERIVED 
  16. table: t1 
  17. type: ALL 
  18. possible_keys: PRIMARY 
  19. keyNULL 
  20. key_len: NULL 
  21. ref: NULL 
  22. rows: 973192 
  23. Extra: Using where 
  24. *************************** 3. row *************************** 
  25. id: 3 
  26. select_type: SUBQUERY 
  27. table: t1 
  28. type: index 
  29. possible_keys: NULL 
  30. keyPRIMARY 
  31. key_len: 4 
  32. ref: NULL 
  33. rows: 935511 
  34. Extra: Using where 

 

 
  1. #采用INNER JOIN优化,JOIN子句里也优先从索引获取ID列表,然后直接关联查询获得最终结果,这里不需要加10 
  2. yejr@imysql.com> EXPLAIN SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2 USING (id)/G 
  3. *************************** 1. row *************************** 
  4. id: 1 
  5. select_type: PRIMARY 
  6. table: <derived2> 
  7. type: ALL 
  8. possible_keys: NULL 
  9. keyNULL 
  10. key_len: NULL 
  11. ref: NULL 
  12. rows: 935510 
  13. Extra: NULL 
  14. *************************** 2. row *************************** 
  15. id: 1 
  16. select_type: PRIMARY 
  17. table: t1 
  18. type: eq_ref 
  19. possible_keys: PRIMARY 
  20. keyPRIMARY 
  21. key_len: 4 
  22. ref: t2.id 
  23. rows: 1 
  24. Extra: NULL 
  25. *************************** 3. row *************************** 
  26. id: 2 
  27. select_type: DERIVED 
  28. table: t1 
  29. type: index 
  30. possible_keys: NULL 
  31. keyPRIMARY 
  32. key_len: 4 
  33. ref: NULL 
  34. rows: 973192 
  35. Extra: Using where 

然后我们来对比下这2个优化后的新SQL执行时间:

 

 
  1. yejr@imysql.com> SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) T ORDER BY id DESC
  2. ... 
  3. rows in set (1.86 sec) 
  4. #采用子查询优化,从profiling的结果来看,相比原来的那个SQL快了:28.2% 
  5.  
  6. yejr@imysql.com> SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1` WHERE ftype=1 ORDER BY id DESC LIMIT 935500,10) t2 USING (id); 
  7. ... 
  8. 10 rows in set (1.83 sec) 
  9. #采用INNER JOIN优化,从profiling的结果来看,相比原来的那个SQL快了:30.8% 

我们再来看一个不带过滤条件的分页SQL对比:

 

 
  1. #原始SQL 
  2. yejr@imysql.com> EXPLAIN SELECT * FROM `t1` ORDER BY id DESC LIMIT 935500, 10/G 
  3. *************************** 1. row *************************** 
  4. id: 1 
  5. select_type: SIMPLE 
  6. table: t1 
  7. type: index 
  8. possible_keys: NULL 
  9. keyPRIMARY 
  10. key_len: 4 
  11. ref: NULL 
  12. rows: 935510 
  13. Extra: NULL 
  14.  
  15. yejr@imysql.com> SELECT * FROM `t1` ORDER BY id DESC LIMIT 935500, 10; 
  16. ... 
  17. 10 rows in set (2.22 sec) 
  18.  
  19. #采用子查询优化 
  20. yejr@imysql.com> EXPLAIN SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC
  21. *************************** 1. row *************************** 
  22. id: 1 
  23. select_type: PRIMARY 
  24. table: <derived2> 
  25. type: ALL 
  26. possible_keys: NULL 
  27. keyNULL 
  28. key_len: NULL 
  29. ref: NULL 
  30. rows: 10 
  31. Extra: Using filesort 
  32. *************************** 2. row *************************** 
  33. id: 2 
  34. select_type: DERIVED 
  35. table: t1 
  36. type: ALL 
  37. possible_keys: PRIMARY 
  38. keyNULL 
  39. key_len: NULL 
  40. ref: NULL 
  41. rows: 973192 
  42. Extra: Using where 
  43. *************************** 3. row *************************** 
  44. id: 3 
  45. select_type: SUBQUERY 
  46. table: t1 
  47. type: index 
  48. possible_keys: NULL 
  49. keyPRIMARY 
  50. key_len: 4 
  51. ref: NULL 
  52. rows: 935511 
  53. Extra: Using index 
  54.  
  55. yejr@imysql.com> SELECT * FROM (SELECT * FROM `t1` WHERE id > ( SELECT id FROM `t1` ORDER BY id DESC LIMIT 935510, 1) LIMIT 10) t ORDER BY id DESC
  56. … 
  57. 10 rows in set (2.01 sec) 
  58. #采用子查询优化,从profiling的结果来看,相比原来的那个SQL快了:10.6% 
  59.  
  60.  
  61. #采用INNER JOIN优化 
  62. yejr@imysql.com> EXPLAIN SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1`ORDER BY id DESC LIMIT 935500,10) t2 USING (id)/G 
  63. *************************** 1. row *************************** 
  64. id: 1 
  65. select_type: PRIMARY 
  66. table:  
  67. type: ALL 
  68. possible_keys: NULL 
  69. keyNULL 
  70. key_len: NULL 
  71. ref: NULL 
  72. rows: 935510 
  73. Extra: NULL 
  74. *************************** 2. row *************************** 
  75. id: 1 
  76. select_type: PRIMARY 
  77. table: t1 
  78. type: eq_ref 
  79. possible_keys: PRIMARY 
  80. keyPRIMARY 
  81. key_len: 4 
  82. ref: t1.id 
  83. rows: 1 
  84. Extra: NULL 
  85. *************************** 3. row *************************** 
  86. id: 2 
  87. select_type: DERIVED 
  88. table: t1 
  89. type: index 
  90. possible_keys: NULL 
  91. keyPRIMARY 
  92. key_len: 4 
  93. ref: NULL 
  94. rows: 973192 
  95. Extra: Using index 
  96.  
  97. yejr@imysql.com> SELECT * FROM `t1` INNER JOIN ( SELECT id FROM `t1`ORDER BY id DESC LIMIT 935500,10) t2 USING (id); 
  98. … 
  99. 10 rows in set (1.70 sec) 
  100. #采用INNER JOIN优化,从profiling的结果来看,相比原来的那个SQL快了:30.2% 

至此,我们看到采用子查询或者INNER JOIN进行优化后,都有大幅度的提升,这个方法也同样适用于较小的分页,虽然LIMIT开始的 start 位置小了很多,SQL执行时间也快了很多,但采用这种方法后,带WHERE条件的分页分别能提高查询效率:24.9%、156.5%,不带WHERE条件的分页分别提高查询效率:554.5%、11.7%,各位可以自行进行测试验证。单从提升比例说,还是挺可观的,确保这些优化方法可以适用于各种分页模式,就可以从一开始就是用。 我们来看下各种场景相应的提升比例是多少:

MySQL中分页优化的实例详解

结论:这样看就和明显了,尤其是针对大分页的情况,因此我们优先推荐使用INNER JOIN方式优化分页算法。

上述每次测试都重启mysqld实例,并且加了SQL_NO_CACHE,以保证每次都是直接数据文件或索引文件中读取。如果数据经过预热后,查询效率会一定程度提升,但但上述相应的效率提升比例还是基本一致的。

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表