分享好友 数据库首页 频道列表

SQL分页排序的实现与分页数据重复问题——以Oracle rownum为例

数据库其他  2023-02-09 22:350

对于关系数据库来说,直接写SQL拉数据在列表中显示是很常用的做法。但如此便带来一个问题:当数据量大到一定程度时,系统内存迟早会耗光。另外,网络传输也是问题。如果有1000万条数据,用户想看最后一条,这时即便有足够的内存,在网络上传输这么多数据也得一两小时吧,恐怕没几个用户有这么耐心等。因此分页是必须的。

现在网上的论坛、博客什么的,基本上都会有分页功能,有些是SQL分页的,有些可能是NOSQL用其它方法分页,都有很成熟的东西了。本文根据我自己的经验,以Oracle为例,讲下简单的SQL分页和排序问题,对刚接触SQL准备要做分页的人有些帮助吧,大牛们就不必看了。

假设ORALCE数据库中有一个TAB001表,主键为ID,有1000万条记录,索引什么的都有了。我们有一个需求,是在界面上列出指定条件的记录,原始SQL如下:

select ID,NAME,ATYPE,CREATEDATE,CREATOR,ASTATUS from TAB001 where ATYPE='SOME_TYPE'

如果要排序,比如要按CREATOR倒排序,我们会在SQL后面再加一句:order by CREATEOR desc

现在,我们发现这个SQL下来有500万条记录,显然,如果不分页,系统很容易就会翘掉。于是我们准备分页。

分页前,我们可能要在界面上摆上几个按钮和状态显示:上一页、下一页、第一页、最后页、每页X条、共M页、当前第N页、跳到第N页,等。显然,我们分页的步骤如下:

计算总记录数; 根据总记录数和每页记录数,计算总页数; 根据当前要显示的页码,计算起始和结束的记录号; 生成分页SQL,执行之,返回本页数据,显示之。 首先,计算总记录数。这个简单,嵌套一个select count(*)就行了:

select count(*)   from (              select ID,NAME,ATYPE,CREATEDATE,CREATOR,ASTATUS from TAB001 where ATYPE='SOME_TYPE'           ) xx   然后,总页数=ceil(总记录数/每页记录数),不足一页也当一页处理。

接着,假设现在是第N页,则本页的开始、结束记录号为:

  开始记录号=N*每页记录数

  结束记录号=min((N+1)*每页记录数-1,总记录数)

最后,生成分页SQL。由于分页需要有记录号,因此先要嵌套一个子查询生成ROWNUM:

select rownum as recordno   from (              select ID,NAME,ATYPE,CREATEDATE,CREATOR,ASTATUS from TAB001 where ATYPE='SOME_TYPE'           ) xx   这样,我们就有了记录号,可以再对记录号进行过滤,只选出本页开始记录号之后、结束记录号之前的记录:

select xxx.*   from (             select rownum as recordno               from (                          select ID,NAME,ATYPE,CREATEDATE,CREATOR,ASTATUS from TAB001 where ATYPE='SOME_TYPE'                       ) xx            ) xxx where recordno >= :开始记录号     and recordno <= :结束记录号

至此似乎分页SQL已经完成了,表面上看这个SQL挺正确,运行起来似乎也没问题。但经过我们实践检验,其实这个SQL是不安全的,在某些情况下会出错,原因在于它没有排序。在分页情况下,第一页和第二页的数据是来自两次相对独立的SQL,如果没有排序,则SQL第一次和第二次执行时返回的结果是不一致的。 不一致是什么意思?假设有一个无排序的SQL,我们把SQL执行两次:

第一次执行后会返回有1、2、3、4、5共5条记录 第二次执行后还是会返回有1、2、3、4、5共5条记录 大部分情况下,这两次返回结果的顺序是完全一样的。但不幸的是,也许数据库有问题了,也许有人改了数据,反正有时候它会不一样,比如第二次执行时第2条和第4条对调了,返回的是1、4、3、2、5共5条记录,如下:

第一次:1、2、3、4、5 第二次:1、4、3、2、5 假设我们对这个SQL进行分页,每页3条记录,共两页,正常情况下结果是这样的:

拉第一页时,执行第一次SQL,按1、2、3、4、5排序,返回1、2、3三条记录 拉第二页时,执行第一次SQL,按1、2、3、4、5排序,返回4、5两条记录 但如果发生排序混乱的问题,结果会这样:

拉第一页时,执行第一次SQL,按1、2、3、4、5排序,返回1、2、3三条记录 拉第二页时,执行第二次SQL,按1、4、3、2、5排序,返回2、5两条记录 结果我们会发现,分页结果很不正常,2这条记录出现了两次,4则消失了。正常来说,我们不会注意到有数据丢失,但我们会注意到分页的数据有重复。

怎么办呢?那我们就加一个排序吧,排序子句要加在最里层的SQL里,这样分页出来的结果才会是排序后的结果。比如按名称、类别或作者排序的order by子句:

select xxx.*   from (             select rownum as recordno               from (                          select ID,NAME,ATYPE,CREATEDATE,CREATOR,ASTATUS from TAB001 where ATYPE='SOME_TYPE'                           order by NAME,ATYPE,CREATOR                       ) xx            ) xxx where recordno >= :开始记录号     and recordno <= :结束记录号   这样是不是可以了呢?答案还是不行,因为这些字段的值不是唯一的。可考虑一个极端情况,就是这个表里500万条记录的名称、类别和作者都完全一样,会有什么结果呢?结果仍然是无序。

最终解决这个问题的办法,就是一定要用ID主键排序。不管前面有多少个order by字段,最后面一定要加上ID主键:

  select xxx.*   from (             select rownum as recordno               from (                          select ID,NAME,ATYPE,CREATEDATE,CREATOR,ASTATUS from TAB001 where ATYPE='SOME_TYPE'                           order by NAME,ATYPE,CREATOR,ID                       ) xx            ) xxx where recordno >= :开始记录号     and recordno <= :结束记录号   由于主键ID是唯一的,所以只要ID不变,按ID排序就能保证每次执行分页SQL都是一致的顺序了。

本篇文章来源于 Linux公社网站(www.linuxidc.com)  原文链接:http://www.linuxidc.com/Linux/2011-02/32593p2.htm

查看更多关于【数据库其他】的文章

展开全文
相关推荐
反对 0
举报 0
评论 0
图文资讯
热门推荐
优选好物
更多热点专题
更多推荐文章
去重复的sql(Oracle) 去重复的英文
1.利用group by 去重复2.可以利用下面的sql去重复,如下  1) select id,name,sex from (select a.*,row_number() over(partition by a.id,a.set order by name) su from test a ) where su=1  2)select id,name,sex from (select a.*,row_number() over(p

0评论2023-02-10893

Oracle SQL七次提速技巧
以下SQL执行时间按序号递减。1,动态SQL,没有绑定变量,每次执行都做硬解析操作,占用较大的共享池空间,若共享池空间不足,会导致其他SQL语句的解析信息被挤出共享池。create or replace procedure proc1as beginfor i in 1..100000 loop    execute imme

0评论2023-02-10755

SQL ORACLE case when函数用法
case when 用法(1)简单case函数:格式:  case 列名   when 条件值1 then 选项1  when 条件值1 then 选项2......  else 默认值 end例如:  select   case job_level  when '1' then '1111'  when '2' then '2222'   when '3' then '3333

0评论2023-02-10564

mysql下如何执行sql脚本 执行SQL脚本
1.编写sql脚本,假设内容如下:  create database dearabao;  use dearabao;  create table niuzi (name varchar(20));  保存脚本文件,假设我把它保存在F盘的hello world目录下,于是该文件的路径为:F:\hello world\niuzi.sql2.执行sql脚本,可以有2种方法: 

0评论2023-02-10699

MySQL 5.7版本sql_mode=only_full_group_by问题
用到GROUP BY 语句查询时com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'col_user_6.a.START_TIME' which is not functionally dependent on colu

0评论2023-02-10973

Oracle迁移到MySQL性能下降的注意点 oracle数据库迁移需要注意的问题
背景:最近有较多的客户系统由原来由Oracle改造到MySQL后出现了性能问题CPU 100%,或是后台的CRM系统复杂SQL在业务高峰的时候出现堆积导致业务故障。在我的记忆里面淘宝最初从Oracle迁移到MySQL期间也遇到了很多SQL的性能问题,记忆最为深刻的子查询,当初的

0评论2023-02-10580

ORACLE中通过SQL语句(alter table)来增加、删除、修改字段
1.添加字段:alter table  表名  add (字段  字段类型)  [ default  '输入默认值']  [null/not null]  ;2.添加备注:comment on column  库名.表名.字段名 is  '输入的备注';  如: 我要在ers_data库中  test表 document_type字段添加备注  comm

0评论2023-02-10584

MySQL与Oracle 差异比较之六触发器
触发器编号类别ORACLEMYSQL注释1创建触发器语句不同create or replace trigger TG_ES_FAC_UNIT  before insert or update or delete on ES_FAC_UNIT  for each rowcreate trigger `hs_esbs`.`TG_INSERT_ES_FAC_UNIT` BEFORE INSERT on `hs_esbs`.`es_fac_u

0评论2023-02-10914

mysql where条件:某时间字段为今天的sql语句
1.查询:注册时间为今天的所有用户数:select count(*) from customer where TO_DAYS(createtime) = TO_DAYS(NOW())2.获取当前时间到凌晨24点还有多长时间:(Java中可用于判断某时间是否为今天)final Calendar cal = Calendar.getInstance();    ca

0评论2023-02-10717

mysql中的sql
变量用户变量: 在用户变量前加@系统变量: 在系统变量前加@@运算符算术运算符有: +(加), -(减), * (乘), / (除) 和% (求模) 五中运算位运算符有:(位于), | (位或), ^ (位异或), ~ (位取反),(位右移),(位左移)比较运算符有: = (等于),(大于),(小于), = (大

0评论2023-02-10936

Oracle的HINT可以强制指定SQL的执行计划,比如选择索引、表的连接顺序以及表的连接方式等等。(转)
在Oracle中查看所有的表: select * from tab/dba_tables/dba_objects/cat; 看用户建立的表 :  select table_name from user_tables;  //当前用户的表 select table_name from all_tables;  //所有用户的表 select table_name from dba_tables;  //包

0评论2023-02-10857

Oracle sql 子字符串长度判断
Oracle sql 子字符串长度判断 select t.* from d_table t WHEREsubstr(t.col,1,1)='8' and instr(t.col,'/')0 and length(substr(t.col,1,instr(t.col,'/')))5; 字符串的前两位都是数字:select * from d_table t WHERE regexp_like(substr(t.col,1,2), '^[

0评论2023-02-10759

Oracle、MySql、Sql Server比对
MySql:廉价(部分免费):当前,MySQL採用双重授权(DualLicensed),他们是GPL和MySQLAB制定的商业许可协议。假设你在一个遵循GPL的***(开源)项目中使用MySQL,那么你能够遵循GPL协议免费使用MySQL。否则,你须要购买MySQLAB制定的那个商业许可协议。Windows $

0评论2023-02-10441

Oracle 存储过程,临时表,动态SQL测试
--创建事务级别的结果临时表create global temporary table tmp_yshy( c1 varchar2(100), c2 varchar2(100))on commit delete rows;--创建事务级别的存储sql语句的临时表create global temporary table tmp_sql( c1 varchar2(4000))on commit delete rows;测

0评论2023-02-10508

更多推荐