如何在排序后限制 Oracle 查询返回的行数?

1324

有没有一种方法可以使 Oracle 查询的行为像包含 MySQL 的 limit 子句一样?

在 MySQL 中,我可以这样做:

select * 
from sometable
order by name
limit 20,10

获取第21到30行(跳过前20行,取接下来的10行)。这些行是在order by之后进行选择的,因此实际上是按字母顺序排列的第20个名称开始。

在Oracle中,人们唯一提到的是rownum伪列,但它在order by之前被评估,这意味着:

select * 
from sometable
where rownum <= 10
order by name

将返回一个按名称排序的随机十行数据集,这通常不是我想要的。它也不能指定偏移量。


21
SQL:2008 中标准化了。 - dalle
17
Tom Kyte 宣布了 Oracle 12c 的限制。 - wolφi
18
获取结果集中的下一页? - Mathieu Longtin
8
特别是,分页搜索会始终使用这种模式。几乎任何带有任何类型搜索功能的应用都会使用它。另一个用例是在客户端仅加载长列表或表格的一部分,并为用户提供展开选项。 - jpmc26
4
除非基础数据由于“ORDER BY”而发生更改,否则您无法获得不同的结果集。这就是首先排序的全部意义。如果基础数据发生更改并且由此更改了您的结果集,那么为什么不向用户显示更新后的结果,而要显示过时的信息?此外,状态管理是一种应尽可能避免的瘟疫。它是复杂性和漏洞的不断来源;这就是为什么函数式变得如此流行的原因。在哪种情况下你会知道在内存中使整个结果集失效?在Web中,您无法知道用户何时离开。 - jpmc26
显示剩余8条评论
15个回答

989

你可以使用子查询来实现,例如:

select *
from  
( select * 
  from emp 
  order by sal desc ) 
where ROWNUM <= 5;

另外,可以查看 Oracle/AskTom 上的主题 On ROWNUM and limiting results 获取更多信息。

更新:为了同时限制结果的上下限,需要使用一些更冗长的代码。

select * from 
( select a.*, ROWNUM rnum from 
  ( <your_query_goes_here, with order by> ) a 
  where ROWNUM <= :MAX_ROW_TO_FETCH )
where rnum  >= :MIN_ROW_TO_FETCH;

(从特定的AskTom文章中复制)

更新2:从Oracle 12c(12.1)开始,有一种语法可用于限制行数或从偏移处开始。

SELECT * 
FROM   sometable
ORDER BY name
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

请参考此答案获取更多例子。感谢Krumia的提示。


5
这绝对是正确的方法,但要注意(正如Ask Tom文章所说)随着max rownum的增加,查询性能会降低。这是一个很好的解决方案,适用于只需要查看前几页查询结果的情况。但如果你将其作为代码浏览整个表格的机制,最好重新构建你的代码。 - Chris Gill
1
+1 你的小写/大写版本实际上帮助我解决了一个问题,即仅有上限的rownum子句严重减慢了我的查询速度。 - Kelvin
2
Leigh Riffel的“仅使用一个嵌套查询的分析解决方案”是最佳选择。 - Darren Hicks
7
AskTom文章还使用了优化提示,它使用SELECT /*+ FIRST_ROWS(n) / a., rownum rnum语句。请注意,结束斜杠应该在星号之前。SO网站正在删除这个星号。 - David Mann
1
请注意,对于Oracle 11,带有ROWNUM的外部SELECT将阻止您在UpdatableResultSet上调用deleteRow(出现ORA-01446错误) - 期待12c R1的更改! - nsandersen
显示剩余3条评论

883

从Oracle 12c R1 (12.1)开始,一个限制行数的子句。它不使用常见的LIMIT语法,但是具有更多选项可以更好地完成工作。您可以在此处找到完整的语法。(还可以阅读有关在Oracle内部如何工作的详细信息,请参见此答案)。

为了回答原始问题,以下是查询:

SELECT * 
FROM   sometable
ORDER BY name
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

(对于早期的Oracle版本,请参考本问题中的其他答案)


示例:

以下示例摘自链接页面,旨在防止链接失效。

设置

CREATE TABLE rownum_order_test (
  val  NUMBER
);

INSERT ALL
  INTO rownum_order_test
SELECT level
FROM   dual
CONNECT BY level <= 10;

COMMIT;

表格里有什么?

SELECT val
FROM   rownum_order_test
ORDER BY val;

       VAL
----------
         1
         1
         2
         2
         3
         3
         4
         4
         5
         5
         6
         6
         7
         7
         8
         8
         9
         9
        10
        10

20 rows selected.

获取前 N 行数据

SELECT val
FROM   rownum_order_test
ORDER BY val DESC
FETCH FIRST 5 ROWS ONLY;

       VAL
----------
        10
        10
         9
         9
         8

5 rows selected.

如果第N行存在并列值,获取所有与第N行并列的行,否则只获取前N

SELECT val
FROM   rownum_order_test
ORDER BY val DESC
FETCH FIRST 5 ROWS WITH TIES;

       VAL
----------
        10
        10
         9
         9
         8
         8

6 rows selected.

x% 的行

SELECT val
FROM   rownum_order_test
ORDER BY val
FETCH FIRST 20 PERCENT ROWS ONLY;

       VAL
----------
         1
         1
         2
         2

4 rows selected.

使用偏移量,对于分页非常有用

SELECT val
FROM   rownum_order_test
ORDER BY val
OFFSET 4 ROWS FETCH NEXT 4 ROWS ONLY;

       VAL
----------
         3
         3
         4
         4

4 rows selected.

你可以将偏移量与百分比结合使用

SELECT val
FROM   rownum_order_test
ORDER BY val
OFFSET 4 ROWS FETCH NEXT 20 PERCENT ROWS ONLY;

       VAL
----------
         3
         3
         4
         4

4 rows selected.

10
我们需要工具支持键集分页。 - Lukasz Szozda
2
只是为了扩展:OFFSET FETCH 语法是一种语法糖。详情 - Lukasz Szozda
2
我们如何在Oracle 11G中获取LIMIT和OFFSET? - PAA
3
11G不支持LIMIT/OFFSET。如果您查看其他答案,它们都以某种方式实现了限制和偏移量。 - sampathsris

198

我对以下几种方法进行了性能测试:

Asktom

select * from (
  select a.*, ROWNUM rnum from (
    <select statement with order by clause>
  ) a where rownum <= MAX_ROW
) where rnum >= MIN_ROW

分析性的

select * from (
  <select statement with order by clause>
) where myrow between MIN_ROW and MAX_ROW

简短替代方案

select * from (
  select statement, rownum as RN with order by clause
) where a.rn >= MIN_ROW and a.rn <= MAX_ROW

结果

该表有1000万条记录,按未建索引的日期时间列进行排序:

  • 执行计划对三个查询显示相同的值(323168)
  • 但是AskTom获胜(紧随其后的是解析函数)

选择前10行所需时间:

  • AskTom:28-30秒
  • 解析函数:33-37秒
  • 简短的替代方案:110-140秒

选择第100,000到100,010行所需时间:

  • AskTom:60秒
  • 解析函数:100秒

选择第9,000,000到9,000,010行所需时间:

  • AskTom:130秒
  • 解析函数:150秒

干得好。你试过使用“between”而不是“>=”和“<=”的简短替代方法吗? - Mathieu Longtin
5
"BETWEEN" 只是 ">= AND <=" 的简写形式。(https://dev59.com/dm445IYBdhLWcg3wia6W) - wweicker
1
zeldi - 这是哪个版本?Oracle在11.1和11.2中进行了分析性能的改进。 - Leigh Riffel
@Leigh Riffel,它是10.2.0.5版本;有一天我可能会花时间检查11i版本。 - zeldi
7
我进行了一些快速测试,对于12c获得了类似的结果。新的“offset”语法与分析方法相比,在计划和性能方面都相同。 - Jon Heller
显示剩余2条评论

59

只有一个嵌套查询的分析解决方案:

SELECT * FROM
(
   SELECT t.*, Row_Number() OVER (ORDER BY name) MyRow FROM sometable t
) 
WHERE MyRow BETWEEN 10 AND 20;

Rank()可以替换Row_Number(),但如果存在名称重复的情况,可能会返回比您预期更多的记录。


3
我喜欢分析。你可能需要澄清Rank()和Row_Number()之间的行为差异。 - Dave Costa
确实,不确定为什么我没有考虑到重复项。所以,在这种情况下,如果有重复的姓名值,RANK 可能会给出比预期更多的记录,因此你应该使用 Row_Number。 - Leigh Riffel
如果提到 rank(),值得注意的是 dense_rank(),后者在输出控制方面可能更有用,因为它不会“跳过”数字,而 rank() 可能会。无论如何,在这个问题中,row_number() 最合适。另一个需要注意的是,这种技术适用于支持所提到的函数的任何数据库。 - Paul Maxwell

50

SQL标准

自12c版本以来,Oracle支持SQL:2008标准,该标准提供以下语法来限制SQL结果集:

SELECT
    title
FROM
    post
ORDER BY
    id DESC
FETCH FIRST 50 ROWS ONLY

Oracle 11g及更早版本

在12c版本之前,要获取Top-N记录,您必须使用派生表和ROWNUM伪列:

SELECT *
FROM (
    SELECT
        title
    FROM
        post
    ORDER BY
        id DESC
)
WHERE ROWNUM <= 50

我很好奇,Oracle中从来没有支持使用类似于“select TOP N * from {TableName}”这样的语法吗? - Ak777
@Ak777 不是。那只是SQL Server。 - Vlad Mihalcea

31

对于 Oracle 12c(请参见SQL 参考文档中的行限制子句):

SELECT * 
FROM sometable
ORDER BY name
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;

63
当然,他们必须使用与迄今为止其他人完全不同的语法。 - Mathieu Longtin
9
很明显,在与其他供应商一起商讨 SQL:2008 中的“LIMIT”时,他们不得不效仿微软的方式并打破标准。 - beldaz
1
有趣的是,最近我听说最新的标准包括这种语法,所以也许在实现之前Oracle先推动了它。可以说,它比“LIMIT ... OFFSET”更灵活。 - beldaz
3
@Derek:是的,不遵循标准是令人遗憾的。但12cR1中引入的新功能比仅有的“LIMIT n, m”更加强大(请参见我的答案)。然而,Oracle应该将“LIMIT n, m”实现为语法糖,因为它与“OFFSET n ROWS FETCH NEXT m ROWS ONLY”等效。 - sampathsris
10
@Derek: 实际上,我刚刚在PostgreSQL手册中注意到了这个备注http://www.postgresql.org/docs/9.0/static/sql-select.html#AEN69535,“LIMIT和OFFSET子句是PostgreSQL特定的语法,也被MySQL使用。 SQL:2008标准引入了OFFSET ... FETCH {FIRST | NEXT} ...子句,具有相同的功能。”因此,LIMIT从未成为标准的一部分。 - beldaz
显示剩余4条评论

22

使用带排序的分页查询在Oracle中确实很棘手。

Oracle提供了一个ROWNUM伪列,返回一个数字,指示数据库从表或一组连接视图中选择行的顺序。

ROWNUM是一个伪列,经常会让很多人陷入麻烦。ROWNUM值不会永久地分配给一行(这是一个常见的误解)。当实际分配ROWNUM值时可能会令人困惑。 ROWNUM值是在查询的过滤谓词通过之后,但在查询聚合或排序之前被分配给行的。

此外,只有在分配后才会递增ROWNUM值。

这就是为什么下面的查询语句不返回任何行:

 select * 
 from (select *
       from some_table
       order by some_column)
 where ROWNUM <= 4 and ROWNUM > 1; 

查询结果的第一行不满足 ROWNUM > 1 的条件,因此 ROWNUM 不会增加到 2。因此没有任何一个 ROWNUM 值大于 1,因此查询返回零行。

正确定义的查询应该像这样:

select *
from (select *, ROWNUM rnum
      from (select *
            from skijump_results
            order by points)
      where ROWNUM <= 4)
where rnum > 1; 

了解更多关于分页查询的内容,请访问Vertabelo博客中的文章:


5
查询结果的第一行未通过 ROWNUM > 1 条件(...)- 请点赞以获取解释。 - Piotr Dobrogost
令人难以置信!迄今为止,Oracle还没有为此提供高效解决方案。例如,在这之间,速度非常慢! - Rafael Pizao

13
作为被接受的答案的延伸,Oracle在内部使用ROW_NUMBER/RANK函数。 OFFSET FETCH语法只是一种语法糖。
可以通过使用DBMS_UTILITY.EXPAND_SQL_TEXT过程来观察:
准备样本:
CREATE TABLE rownum_order_test (
  val  NUMBER
);

INSERT ALL
  INTO rownum_order_test
SELECT level
FROM   dual
CONNECT BY level <= 10;
COMMIT;

查询:

SELECT val
FROM   rownum_order_test
ORDER BY val DESC
FETCH FIRST 5 ROWS ONLY;

定期:

SELECT "A1"."VAL" "VAL" 
FROM  (SELECT "A2"."VAL" "VAL","A2"."VAL" "rowlimit_$_0",
               ROW_NUMBER() OVER ( ORDER BY "A2"."VAL" DESC ) "rowlimit_$$_rownumber" 
      FROM "ROWNUM_ORDER_TEST" "A2") "A1" 
WHERE "A1"."rowlimit_$$_rownumber"<=5 ORDER BY "A1"."rowlimit_$_0" DESC;

db<>fiddle演示

获取展开的SQL文本:

declare
  x VARCHAR2(1000);
begin
 dbms_utility.expand_sql_text(
        input_sql_text => '
          SELECT val
          FROM   rownum_order_test
          ORDER BY val DESC
          FETCH FIRST 5 ROWS ONLY',
        output_sql_text => x);

  dbms_output.put_line(x);
end;
/

WITH TIES 相当于用 RANK 函数展开:

declare
  x VARCHAR2(1000);
begin
 dbms_utility.expand_sql_text(
        input_sql_text => '
          SELECT val
          FROM   rownum_order_test
          ORDER BY val DESC
          FETCH FIRST 5 ROWS WITH TIES',
        output_sql_text => x);

  dbms_output.put_line(x);
end;
/

SELECT "A1"."VAL" "VAL" 
FROM  (SELECT "A2"."VAL" "VAL","A2"."VAL" "rowlimit_$_0",
              RANK() OVER ( ORDER BY "A2"."VAL" DESC ) "rowlimit_$$_rank" 
       FROM "ROWNUM_ORDER_TEST" "A2") "A1" 
WHERE "A1"."rowlimit_$$_rank"<=5 ORDER BY "A1"."rowlimit_$_0" DESC

和偏移量:

declare
  x VARCHAR2(1000);
begin
 dbms_utility.expand_sql_text(
        input_sql_text => '
          SELECT val
FROM   rownum_order_test
ORDER BY val
OFFSET 4 ROWS FETCH NEXT 4 ROWS ONLY',
        output_sql_text => x);

  dbms_output.put_line(x);
end;
/


SELECT "A1"."VAL" "VAL" 
FROM  (SELECT "A2"."VAL" "VAL","A2"."VAL" "rowlimit_$_0",
             ROW_NUMBER() OVER ( ORDER BY "A2"."VAL") "rowlimit_$$_rownumber" 
       FROM "ROWNUM_ORDER_TEST" "A2") "A1" 
       WHERE "A1"."rowlimit_$$_rownumber"<=CASE  WHEN (4>=0) THEN FLOOR(TO_NUMBER(4)) 
             ELSE 0 END +4 AND "A1"."rowlimit_$$_rownumber">4 
ORDER BY "A1"."rowlimit_$_0"

9

使用21c版本,您可以像下面这样简单地应用限制:

select * from course where ROWNUM <=10;

3
请阅读我的问题的第二部分。这个方法不起作用,而且它已经存在了很长时间,远超过21世纪。 - Mathieu Longtin

6

减少SELECT语句,也会降低性能消耗。感谢:anibal@upf.br

SELECT *
    FROM   (SELECT t.*,
                   rownum AS rn
            FROM   shhospede t) a
    WHERE  a.rn >= in_first
    AND    a.rn <= in_first;

2
此外,这是完全不正确的答案。问题是关于在排序之后进行限制,因此rownum应该在子查询之外。 - BitLord

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接