大数据量下mysql的分页查询

总结:

为解决此问题效率整理此文: select * from table limit 50000,10,在大数据 量下这样写不行。

解决方法如下:

select a.* from member as a inner join (select id from member where gender=1 limit 300000,1) as b on a.id=b.id;

 

具体细节分析请参看此下转录的两篇文章,能更全面的了解大数据两下的mysql分页查询的效率提升。

案例一:

原文链接:https://blog.csdn.net/fdipzone/article/details/72793837?tdsourcetag=s_pcqq_aiomsg

mysql查询使用select命令,配合limit,offset参数可以读取指定范围的记录。本文将介绍mysql查询时,offset过大影响性能的原因及优化方法。  

准备测试数据表及数据
1.创建表

CREATE TABLE `member` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `name` varchar(10) NOT NULL COMMENT '姓名',
 `gender` tinyint(3) unsigned NOT NULL COMMENT '性别',
 PRIMARY KEY (`id`),
 KEY `gender` (`gender`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `member` (
 `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
 `name` varchar(10) NOT NULL COMMENT '姓名',
 `gender` tinyint(3) unsigned NOT NULL COMMENT '性别',
 PRIMARY KEY (`id`),
 KEY `gender` (`gender`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;


  
2.插入1000000条记录

<?php
$pdo = new PDO("mysql:host=localhost;dbname=user","root",'');

for($i=0; $i<1000000; $i++){
    $name = substr(md5(time().mt_rand(000,999)),0,10);
    $gender = mt_rand(1,2);
    $sqlstr = "insert into member(name,gender) values('".$name."','".$gender."')";
    $stmt = $pdo->prepare($sqlstr);
    $stmt->execute();
}
?>

mysql> select count(*) from member;
+----------+
| count(*) |
+----------+
|  1000000 |
+----------+
1 row in set (0.23 sec)



3.当前数据库版本

mysql> select version();
+-----------+
| version() |
+-----------+
| 5.6.24    |
+-----------+
1 row in set (0.01 sec)


分析offset过大影响性能的原因
1.offset较小的情况

mysql> select * from member where gender=1 limit 10,1;
+----+------------+--------+
| id | name       | gender |
+----+------------+--------+
| 26 | 509e279687 |      1 |
+----+------------+--------+
1 row in set (0.00 sec)

mysql> select * from member where gender=1 limit 100,1;
+-----+------------+--------+
| id  | name       | gender |
+-----+------------+--------+
| 211 | 07c4cbca3a |      1 |
+-----+------------+--------+
1 row in set (0.00 sec)

mysql> select * from member where gender=1 limit 1000,1;
+------+------------+--------+
| id   | name       | gender |
+------+------------+--------+
| 1975 | e95b8b6ca1 |      1 |
+------+------------+--------+
1 row in set (0.00 sec)


当offset较小时,查询速度很快,效率较高。 
  
2.offset较大的情况

mysql> select * from member where gender=1 limit 100000,1;
+--------+------------+--------+
| id     | name       | gender |
+--------+------------+--------+
| 199798 | 540db8c5bc |      1 |
+--------+------------+--------+
1 row in set (0.12 sec)

mysql> select * from member where gender=1 limit 200000,1;
+--------+------------+--------+
| id     | name       | gender |
+--------+------------+--------+
| 399649 | 0b21fec4c6 |      1 |
+--------+------------+--------+
1 row in set (0.23 sec)

mysql> select * from member where gender=1 limit 300000,1;
+--------+------------+--------+
| id     | name       | gender |
+--------+------------+--------+
| 599465 | f48375bdb8 |      1 |
+--------+------------+--------+
1 row in set (0.31 sec)


当offset很大时,会出现效率问题,随着offset的增大,执行效率下降。 
 

分析影响性能原因

select * from member where gender=1 limit 300000,1;


因为数据表是InnoDB,根据InnoDB索引的结构,查询过程为:

通过二级索引查到主键值(找出所有gender=1的id)。
再根据查到的主键值通过主键索引找到相应的数据块(根据id找出对应的数据块内容)。
根据offset的值,查询300001次主键索引的数据,最后将之前的300000条丢弃,取出最后1条。
不过既然二级索引已经找到主键值,为什么还需要先用主键索引找到数据块,再根据offset的值做偏移处理呢?

如果在找到主键索引后,先执行offset偏移处理,跳过300000条,再通过第300001条记录的主键索引去读取数据块,这样就能提高效率了。

如果我们只查询出主键,看看有什么不同

mysql> select id from member where gender=1 limit 300000,1;
+--------+
| id     |
+--------+
| 599465 |
+--------+
1 row in set (0.09 sec)


很明显,如果只查询主键,执行效率对比查询全部字段,有很大的提升。 
 

推测
只查询主键的情况 
因为二级索引已经找到主键值,而查询只需要读取主键,因此mysql会先执行offset偏移操作,再根据后面的主键索引读取数据块。

需要查询所有字段的情况 
因为二级索引只找到主键值,但其他字段的值需要读取数据块才能获取。因此mysql会先读出数据块内容,再执行offset偏移操作,最后丢弃前面需要跳过的数据,返回后面的数据。 
 

证实
InnoDB中有buffer pool,存放最近访问过的数据页,包括数据页和索引页。

为了测试,先把mysql重启,重启后查看buffer pool的内容。

mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('primary','gender') and TABLE_NAME like '%member%' group by index_name;
Empty set (0.04 sec)


可以看到,重启后,没有访问过任何的数据页。

查询所有字段,再查看buffer pool的内容

mysql> select * from member where gender=1 limit 300000,1;
+--------+------------+--------+
| id     | name       | gender |
+--------+------------+--------+
| 599465 | f48375bdb8 |      1 |
+--------+------------+--------+
1 row in set (0.38 sec)

mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('primary','gender') and TABLE_NAME like '%member%' group by index_name;
+------------+----------+
| index_name | count(*) |
+------------+----------+
| gender     |      261 |
| PRIMARY    |     1385 |
+------------+----------+
2 rows in set (0.06 sec)


可以看出,此时buffer pool中关于member表有1385个数据页,261个索引页。 
  
重启mysql清空buffer pool,继续测试只查询主键

mysql> select id from member where gender=1 limit 300000,1;
+--------+
| id     |
+--------+
| 599465 |
+--------+
1 row in set (0.08 sec)

mysql> select index_name,count(*) from information_schema.INNODB_BUFFER_PAGE where INDEX_NAME in('primary','gender') and TABLE_NAME like '%member%' group by index_name;
+------------+----------+
| index_name | count(*) |
+------------+----------+
| gender     |      263 |
| PRIMARY    |       13 |
+------------+----------+
2 rows in set (0.04 sec)


可以看出,此时buffer pool中关于member表只有13个数据页,263个索引页。因此减少了多次通过主键索引访问数据块的I/O操作,提高执行效率。

因此可以证实,mysql查询时,offset过大影响性能的原因是多次通过主键索引访问数据块的I/O操作。(注意,只有InnoDB有这个问题,而MYISAM索引结构与InnoDB不同,二级索引都是直接指向数据块的,因此没有此问题 )。 
  
InnoDB与MyISAM引擎索引结构对比图

优化方法
根据上面的分析,我们知道查询所有字段会导致主键索引多次访问数据块造成的I/O操作。

因此我们先查出偏移后的主键,再根据主键索引查询数据块的所有内容即可优化。

mysql> select a.* from member as a inner join (select id from member where gender=1 limit 300000,1) as b on a.id=b.id;
+--------+------------+--------+
| id     | name       | gender |
+--------+------------+--------+
| 599465 | f48375bdb8 |      1 |
+--------+------------+--------+
1 row in set (0.08 sec)

 

案例二

原文链接:https://www.cnblogs.com/xiaoshen666/p/10824117.html

1、Mysql的limit用法

在我们使用查询语句的时候,经常要返回前几条或者中间某几行数据,这个时候怎么办呢?不用担心,mysql已经为我们提供了这样一个功能。

SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset  
SELECT * FROM table LIMIT [offset,] rows | rows OFFSET offset

LIMIT 子句可以被用于强制 SELECT 语句返回指定的记录数。LIMIT 接受一个或两个数字参数。参数必须是一个整数常量。如果给定两个参数,第一个参数指定第一个返回记录行的偏移量,第二个参数指定返回记录行的最大数目。初始记录行的偏移量是 0(而不是 1): 为了与 PostgreSQL 兼容,MySQL 也支持句法: LIMIT # OFFSET #。

mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15   
  
//为了检索从某一个偏移量到记录集的结束所有的记录行,可以指定第二个参数为 -1:    
mysql> SELECT * FROM table LIMIT 95,-1; // 检索记录行 96-last.   
  
//如果只给定一个参数,它表示返回最大的记录行数目:    
mysql> SELECT * FROM table LIMIT 5; //检索前 5 个记录行   
  
//换句话说,LIMIT n 等价于 LIMIT 0,n。  

 

2、Mysql的分页查询语句的性能分析

      MySql分页sql语句,如果和MSSQL的TOP语法相比,那么MySQL的LIMIT语法要显得优雅了许多。使用它来分页是再自然不过的事情了。

 

2.1最基本的分页方式:

SELECT ... FROM ... WHERE ... ORDER BY ... LIMIT ...  

在中小数据量的情况下,这样的SQL足够用了,唯一需要注意的问题就是确保使用了索引:

举例来说,如果实际SQL类似下面语句,那么在category_id, id两列上建立复合索引比较好:

SELECT * FROM articles WHERE category_id = 123 ORDER BY id LIMIT 50, 10  

2.2子查询的分页方式:

随着数据量的增加,页数会越来越多,查看后几页的SQL就可能类似:

SELECT * FROM articles WHERE category_id = 123 ORDER BY id LIMIT 10000, 10  

一言以蔽之,就是越往后分页,LIMIT语句的偏移量就会越大,速度也会明显变慢。

此时,我们可以通过子查询的方式来提高分页效率,大致如下:

SELECT * FROM articles WHERE  id >=   
 (SELECT id FROM articles  WHERE category_id = 123 ORDER BY id LIMIT 10000, 1) LIMIT 10  

2.3JOIN分页方式

SELECT * FROM `content` AS t1    
JOIN (SELECT id FROM `content` ORDER BY id desc LIMIT ".(page−1)∗page−1)∗pagesize.", 1) AS t2    
WHERE t1.id <= t2.id ORDER BY t1.id desc LIMIT $pagesize;   

 经过我的测试,join分页和子查询分页的效率基本在一个等级上,消耗的时间也基本一致。

explain SQL语句:

id select_type table type possible_keys key key_len ref rows Extra

1 PRIMARY <derived2> system NULL NULL NULL NULL 1  

1 PRIMARY t1 range PRIMARY PRIMARY 4 NULL 6264 Using where

2 DERIVED content index NULL PRIMARY 4 NULL 27085 Using index

----------------------------------------

为什么会这样呢?因为子查询是在索引上完成的,而普通的查询时在数据文件上完成的,通常来说,索引文件要比数据文件小得多,所以操作起来也会更有效率。

实际可以利用类似策略模式的方式去处理分页,比如判断如果是一百页以内,就使用最基本的分页方式,大于一百页,则使用子查询的分页方式。

3、Oracle分页查询语句

Oralce数据库 

从数据库表中第M条记录开始检索N条记录 

SELECT * FROM (SELECT ROWNUM r,t1.* From 表名称 t1 where rownum < M + N) t2    
 where t2.r >= M   

   例如从表Sys_option(主键为sys_id)中从第10条记录开始检索20条记录,语句如下: 

SELECT * FROM (SELECT ROWNUM R,t1.* From Sys_option where rownum < 30 ) t2 
Where t2.R >= 10 

 

3、MSSQLSERVER分页查询语句

 

SQL Server主要利用 SELECT TOP语句分页,具体方案,请参考

------------------------------------- 

分页方案一:(利用Not In和SELECT TOP分页) 

语句形式: 

SELECT TOP 10 * 
FROM TestTable 
WHERE (ID NOT IN 
(SELECT TOP 20 id 
FROM TestTable 
ORDER BY id)) 
ORDER BY ID 
SELECT TOP 页大小 * 
FROM TestTable 
WHERE (ID NOT IN 
(SELECT TOP 页大小*页数 id 
FROM 表 
ORDER BY id)) 
ORDER BY ID 
SELECT TOP 页大小 * 
FROM TestTable 
WHERE (ID > 
(SELECT MAX(id) 
FROM (SELECT TOP 页大小*页数 id 
FROM 表 
ORDER BY id) AS T)) 
ORDER BY ID 

------------------------------------- 

分页方案二:(利用ID大于多少和SELECT TOP分页) 

语句形式:  

SELECT TOP 10 * 
FROM TestTable 
WHERE (ID > 
(SELECT MAX(id) 
FROM (SELECT TOP 20 id 
FROM TestTable 
ORDER BY id) AS T)) 
ORDER BY ID 

  

------------------------------------- 

分页方案三:(利用SQL的游标存储过程分页) 

create procedure XiaoZhengGe 
@sqlstr nvarchar(4000), --查询字符串 
@currentpage int, --第N页 
@pagesize int --每页行数 
as 
set nocount on 
declare @P1 int, --P1是游标的id 
@rowcount int 
exec sp_cursoropen @P1 output,@sqlstr,@scrollopt=1,@ccopt=1,@rowcount=@rowcount output 
select ceiling(1.0*@rowcount/@pagesize) as 总页数--,@rowcount as 总行数,@currentpage as 当前页 
set @currentpage=(@currentpage-1)*@pagesize+1 
exec sp_cursorfetch @P1,16,@currentpage,@pagesize 
exec sp_cursorclose @P1 
set nocount off 

  

其它的方案:如果没有主键,可以用临时表,也可以用方案三做,但是效率会低。 

建议优化的时候,加上主键和索引,查询效率会提高。 

 

通过SQL 查询分析器,显示比较:我的结论是: 

分页方案二:(利用ID大于多少和SELECT TOP分页)效率最高,需要拼接SQL语句 

分页方案一:(利用Not In和SELECT TOP分页) 效率次之,需要拼接SQL语句 

分页方案三:(利用SQL的游标存储过程分页) 效率最差,但是最为通用 

 

在实际情况中,要具体分析。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值