Oracle Magazine - Русское издание (Май Июнь 2007)

Том Кайт

Том Кайт: о запросах первых n строк и запросах с разбивкой на страницы
(On Top-n and Pagination Queries,
By Tom Kyte)

Источник: журнал Oracle Magazine, January-February 2007
(http://www.oracle.com/technology/oramag/oracle/07-jan/o17asktom.html).

Наш эксперт получает больше результатов, используя функции ROW_NUMBER, RANK и DENSE_RANK.  

В недавнем выпуске колонки Ask Tom (September/October 2006 – русский перевод: "Том Кайт: о столбце ROWNUM и ограничении размеров результирующих наборов" публикуется в этом же выпуске журнала) я писал об использовании столбца ROWNUM для выполнения запросов первых n строк упорядоченного результирующего набора (top-n queries) и запросов с разбивкой на страницы (pagination queries), которые позволяют извлекать из результирующего набора строки в диапазоне от n от m.

В том выпуске колонки я показывал, как использовать столбец ROWNUM для извлечения первых 10 строк "вашего запроса":

select *
  from
(ваш_запрос)
 where rownum <= 10;
А также, как использовать столбец ROWNUM для извлечения строк "вашего запроса" в диапазоне от n до m:
select *
  from
( select rownum rnum, a.*
    from (ваш_запрос) a
   where rownum <= :M )
where rnum >= :N;

Как всегда, в сервере Oracle Database есть несколько способов выполнения чего-либо, и эти запросы первых n строк и запросы с разбивкой на страницы – не исключение. В этом выпуске колонки я рассмотрю альтернативные способы использования этих функциональных возможностей с помощью аналитических функций, а также представлю еще одну разновидность запросов первых n строк: извлечение первых n строк с группированием по некоторому набору атрибутов, такому как извлечение набора строк таблицы EMP, представляющих троих самых высокооплачиваемых служащих каждого отдела (группирование по столбцу DEPTNO).

Запросы первых N строк

Другой способ написания запроса первых n строк – использование аналитической функции ROW_NUMBER (это похоже на ограничение числа строк, возвращаемых запросом, с помощью предложения LIMIT в СУБД MySQL или оператора "set rowcount" в СУБД SQL Server). Эта функция позволяет получить такие же результаты, которые позволяет получить псевдостолбец ROWNUM, но она более гибкая и имеет больше возможностей. В частности, я буду использовать ее в следующем разделе для извлечения первых n строк с группированием по некоторому набору атрибутов.

Давайте посмотрим, что функция ROW_NUMBER может делать. В следующем примере запрос использует эту функцию для присвоения возрастающих номеров строкам таблицы EMP после ее сортировки в убывающем порядке по столбцу SAL (оклад):

SQL> select ename, sal,
  2      row_number()
  3       over (order by sal desc) rn
  4  from emp
  5  order by sal desc
  6  /

ENAME    SAL    RN
-----   ----    --
 KING   5000     1
 FORD   3000     2
SCOTT   3000     3
JONES   2975     4
 .
 .
 .
 JAMES    950   13
 SMITH    800   14

14 rows selected.

Итак, функция ROW_NUMBER позволяет мне легко присваивать эти возрастающие номера после сортировки таблицы, но, на первый взгляд, кажется, что она не поддерживает извлечение только первых n строк:

SQL> select ename, sal,
  2      row_number()
  3       over (order by sal desc) rn
  4  from emp
  5  where
  6   row_number()
  7    over (order by sal desc) <= 3
  8  order by sal desc
  9  /
 row_number()
 *
ERROR at line 6:
ORA-30483: window  functions are not allowed here

Здесь проблема заключается в том, что аналитические функции вычисляются после обработки предложения WHERE, поэтому их нельзя использовать в предложении WHERE. На самом деле, это никакая не проблема, я же могу использовать вложенные представления – применить предикат после присвоения номеров. Например:

SQL> select *
  2  from (
  3  select ename, sal,
  4  row_number()
  5   over (order by sal desc) rn
  6  from emp
  7  )
  8  where rn <= 3
  9  order by sal desc
 10  /

ENAME    SAL    RN
-----   ----    --
 KING   5000     1
SCOTT   3000     2
 FORD   3000     3

3 rows selected.

Таким образом, это показывает, как с помощью функции ROW_NUMBER выполнять запросы первых n строк, а также указывает общую проблему запросов первых n строк. Посмотрите на результат, в нем две строки со значением 3000. Что если в таблице EMP оклад 3000 имеют не два человека, а три? Результат этого запроса – неоднозначный, поскольку я могу извлекать три записи, но они могут оказаться случайными. Например:

SQL> update emp
  2  set sal = 3000
  3  where ename = 'SMITH';
1 row updated.

SQL> select *
  2  from (
  3  select ename, sal,
  4      row_number()
  5       over (order by sal desc) rn
  6  from emp
  7  )
  8  where rn <= 3
  9  order by sal desc
 10  /

ENAME    SAL    RN
-----   ----    --
KING    5000     1
SMITH   3000     2
SCOTT   3000     3

3 rows selected.

Как видите, строка служащего FORD исчезла из результирующего набора. Если же вы сделаете это обновление и запросите ваши данные, эта строка может не исчезнуть, а исчезнет строка служащего SCOTT или SMITH – порядок исчезновения строк будет произвольным. Это – общая проблема запросов первых n строк: если вы сортируете по какому-либо неуникальному столбцу, возвращаемые наборы первых n строк могут быть разными, даже при одинаковых входных данных. Например:

SQL> create table t
  2  ( x number,
  3    y number
  4  );
Table created.

SQL> insert into t
  2  values ( 1, 100 );
SQL> insert into t
  2  values ( 1, 200 );
SQL> insert into t
  2  values ( 1, 300 );
SQL> insert into t
  2  values ( 1, 400 );
SQL> select *
  2  from
  3  (select x,y,
  4    row_number()
  5    over (order by x) rn
  6  from t)
  7  where rn <= 3;

         X     Y   RN
       ---   ---   --
         1   100    1
         1   200    2
         1   300    3

3 rows selected.

Теперь, попробуем повторно вставить эти же данные, но в другом порядке:

SQL> truncate table t;
Table truncated.

SQL> insert into t
  2  values ( 1, 400 );
SQL> insert into t
  2  values ( 1, 200 );
SQL> insert into t
  2  values ( 1, 300 );
SQL> insert into t
  2  values ( 1, 100 );

SQL> select *
  2  from
  3  (select x,y,
  4    row_number()
  5    over (order by x) rn
  6  from t)
  7  where rn <= 3;

        X     Y    RN
       ---   ---   --
         1   400    1
         1   200    2
         1   300    3

3 rows selected.

Неожиданно я получаю совсем другой ответ. Это то, что вы должны осознавать при выполнении запросов первых n строк и сортировке по неуникальному столбцу.

Имейте в виду, для устранения неоднозначности можно использовать другие аналитические функции. Но при их использовании можно получить более n строк. По моему мнению, когда я использую сортировку по неуникальному столбцу, я хочу получить не только первые n произвольных строк, а все релевантные строки. С этой целью я могу использовать аналитические функции RANK – вычисление ранга и DENSE_RANK – вычисление плотного ранга. Посмотрим, что они делают:

SQL> select ename,sal,
  2   row_number()
  3     over (order by sal desc)rn,
  4   rank()
  5     over (order by sal desc)rnk,
  6   dense_rank()
  7     over (order by sal desc)drnk
  8   from emp
  9  order by sal desc
 10  /

ENAME    SAL   RN   RNK   DRNK
-----   ----   --   ---   ----
 KING   5000    1     1      1
 FORD   3000    2     2      2
SCOTT   3000    3     2      2
JONES   2975    4     4      3
BLAKE   2850    5     5      4
CLARK   2450    6     6      5
 .
 .
 .
14 rows selected.

Здесь главное отметить следующее:

  • функция ROW_NUMBER присваивает строкам результирующего набора непрерывные, уникальные номера от 1 до N;
  • функция RANK не присваивает уникальные номера – служащие FORD и SCOTT делят второе место. Она не присваивает и непрерывные номера – никакой записи не присвоено значение 3, поскольку два человека делят второе место и, в соответствии с определением функции RANK, никто не может занять третье место;
  • функция DENSE_RANK, как и функция RANK, не присваивает уникальные номера, но она присваивает непрерывные номера. Хотя две записи делят второе место, имеется также и третье место.

Вы можете использовать функции RANK and DENSE_RANK так же как вы использовали функцию ROW_NUMBER для ограничения числа возвращаемых строк, но, очевидно, вы получите несколько другие результаты. Например:

SQL> select *
  2  from (
  3  select ename,sal,
  4   dense_rank()
  5     over (order by sal desc)drnk
  6   from emp
  7  ) where drnk <= 3
  8  order by sal desc
  9  /

ENAME    SAL   DRNK
-----   ----   ----
 KING   5000      1
SCOTT   3000      2
 FORD   3000      2
JONES   2975      3

4 rows selected.

Этот запрос возвращает "набор строк, представляющих служащих, которые имеют три самых высоких оклада"; это похоже на искомый результат. Если использовать функцию ROW_NUMBER, результат может быть произвольным, потому что столбец SAL – неуникальный столбец (результат зависит от порядка вставки строк). Однако при использовании функции DENSE_RANK я получаю не точно три строки, а повторяемый (детерминированный) результирующий набор. И я полагаю, что я нашел то, что на самом деле хотел найти конечный пользователь – людей с тремя самыми высокими окладами.

Запросы первых n строк с группированием

Аналитические функции более предпочтительно (по сравнению с псевдостолбцом ROWNUM) использовать при необходимости обработки более сложных запросов. Например, предположим, вы хотите в таблице EMP найти набор строк, представляющих n самых высокооплачиваемых служащих каждого отдела (группирование по столбцу DEPTNO). Для этого мне нужно отсортировать результирующий набор по столбцу DEPTNO, затем отсортировать получившиеся группы по столбцу SAL в убывающем порядке, а затем каждой строке присвоить результаты аналитических функций. К счастью, аналитика разработана для выполнения именно такой работы. Например:

SQL> break on deptno skip 1
SQL> select deptno, ename,sal,
  2   row_number()
  3     over (partition by deptno
  4           order by sal desc)rn,
  5   rank()
  6     over (partition by deptno
  7           order by sal desc)rnk,
  8   dense_rank()
  9     over (partition by deptno
 10           order by sal desc)drnk
 11   from emp
 12  order by deptno, sal desc
 13  /

  DEPTNO   ENAME    SAL   RN  RNK  DRNK
--------   -----   ----   --  ---  ----
      10    KING   5000    1    1     1
           CLARK   2450    2    2     2
          MILLER   1300    3    3     3

      20   SCOTT   3000    1    1     1
            FORD   3000    2    1     1
           JONES   2975    3    3     2
           ADAMS   1100    4    4     3
           SMITH    800    5    5     4

      30   BLAKE   2850    1    1     1
           ALLEN   1600    2    2     2
          TURNER   1500    3    3     3
          MARTIN   1250    4    4     4
            WARD   1250    5    4     4
           JAMES    950    6    6     5

14 rows selected.

Здесь я при вызове аналитических функций использую предложение секционирования PARTITION. Оно виртуально разбивает мой результирующий набор на некоторое количество групп, а затем по очереди применяет аналитические функции к каждой группе.

Это позволяет мне присвоить возрастающие номера, используя, например, функцию ROW_NUMBER, строкам каждого отдела (со сбросом номера строки в 1 при переходе от группы к группе). Как видите, теперь очень легко получить набор строк, представляющих трех самых высокооплачиваемых служащих каждого отдела; для этого используется вложенное представление и применяется предикат WHERE RN <= 3 или RNK <= 3 или DRNK <= 3 – в зависимости от того, какой метод нумерации возвращает ответ, который вы считаете наиболее подходящим. Например:

SQL> break on deptno skip 1
SQL> select *
  2  from (
  3  select deptno, ename, sal,
  4   dense_rank()
  5     over (partition by deptno
  6           order by sal desc)drnk
  7   from emp
  8  ) where drnk <= 3
  9  order by deptno, sal desc
 10  /

     DEPTNO   ENAME    SAL   DRNK
     ------   -----   ----   ----
         10    KING   5000      1
              CLARK   2450      2
             MILLER   1300      3

         20   SCOTT   3000      1
               FORD   3000      1
              JONES   2975      2
              ADAMS   1100      3

         30   BLAKE   2850      1
              ALLEN   1600      2
             TURNER   1500      3

10 rows selected.

Разбивка на страницы с получением строк в диапазоне от N до M

Последнее, что я хочу показать, это использование аналитики для разбивки результатов запросов на страницы. Такая разбивка обычно применяется в веб-приложениях: пользователь вводит набор критериев, по которым выполняется запрос, а затем этот пользователь может просматривать результирующий набор, нажимая на кнопки Previous (предыдущая) и Next (следующая). Для этого приложение должно уметь извлекать из запроса определенный набор строк. В уже упомянутом выпуске колонки "О столбце ROWNUM и ограничении размеров результирующих наборов" а также в представленном выше разделе "Запросы первых N строк" я уже показывал, как выполнять такую разбивку, но такой подход требует два уровня вложенных представлений и его можно считать чересчур громоздким. Синтаксически гораздо проще написать следующий запрос:

select *
 from (
select /*+ first_rows(25) */
  ваши_столбцы,
  row_number()
  over (order by что-то уникальное)rn
 from ваши_таблицы)
where rn between :n and :m
order by rn;

Например, предположим, вы хотите разбить на страницы результаты запроса к представлению ALL_OBJECTS, по 25 строк на странице, и нужно показать страницу 5:

SQL> variable n number
SQL> variable m number
SQL> exec :n := 101; :m := 125
PL/SQL procedure successfully completed.

SQL> select *
  2  from (
  3  select /*+ first_rows(25) */
  4   object_id,object_name,
  5   row_number() over
  6     (order by object_id) rn
  7  from all_objects)
  8  where rn between :n and :m
  9  order by rn;

  OBJECT_ID    OBJECT_NAME    RN
  ---------    -----------   ---
        102         I_SEQ1   101
        103     I_OBJAUTH1   102
        .
        .
        .
        124      I_ACCESS1   123
        125      I_TRIGGER1  124
        126      I_TRIGGER2  125

25 rows selected.

Следующие шаги

СПРАШИВАЙТЕ Тома
Том Кайт, вице-президент корпорации Oracle, отвечает на наиболее трудные вопросы, связанные с технологией баз данных Oracle. Наиболее яркие материалы этого форума публикуются в данной колонке.

ЧИТАЙТЕ еще у Тома
Expert Oracle Database Architecture: 9i and 10g Programming Techniques and Solutions
Effective Oracle by Design
"On ROWNUM and Limiting Results"
Tom Kyte Blog

ЗАГРУЖАЙТЕ Oracle Database 10g Express Edition(Oracle Database XE)

ЧИТАЙТЕ более подробно
о аналитических функциях
о предложении ORDER BY

В подсказке FIRST_ROWS я указываю число 25 – размер моей страницы, я хочу как можно быстрее получить мою первую страницу. После сортировки по столбцу OBJECT_ID я присваиваю номер ROW_NUMBER каждой строке получившегося результирующего набора. Затем для извлечения нужных мне строк (в данном случае от 101 до 125) я использую простое предложение BETWEEN.

Чтобы при каждом выполнении такого запроса этот номер ROW_NUMBER присваивался детерминировано, нужно сортировать по уникальным ключам. В противном случае, вы столкнетесь с проблемой, похожей на ту, которую я указал при рассмотрении запросов первых n строк, представляющих самых высокооплачиваемых служащих. Более подробно о детерминированных запросах см. oracle.com/technology/oramag/oracle/06-sep/o56asktom.html. Кроме того, полная информация об использовании аналитических функций содержится в download.oracle.com/docs/cd/B19306_01/server.102/b14223/analysis.htm#i1007779 (глава 21 Руководства по хранилищам данных). По моему мнению, аналитические функции – самая крутая вещь, появившаяся в языке SQL после ключевого слова SELECT.

Выполняет ли предложение GROUP BY сортировку?

Гарантирует ли предложение GROUP BY, что результирующий набор запроса будет отсортирован по столбцам предложения GROUP BY, даже если в запросе нет предложения ORDER BY?

До тех пор пока в запросе не появится предложение ORDER BY, нельзя предполагать какой-либо порядок возвращаемых строк. Без предложения ORDER BY данные могут возвращаться в любом порядке, который заблагорассудится выбрать серверу баз данных. Это всегда было так и всегда будет так. На самом деле, в сервере Oracle Database 10g Release 2 можно обнаружить, что предложение GROUP BY возвращает данные в более произвольном порядке, чем это было ранее:

SQL> set autotrace on explain
SQL> select job, count(*)
  2    from emp
  3   group by job
  4  /

JOB          COUNT(*)
---------    --------
CLERK               4
SALESMAN            4
PRESIDENT           1
MANAGER             3
ANALYST             2

-----------------------------------
|  Id  | Operation         | Name |
-----------------------------------
|   0  | SELECT STATEMENT  |      |
|   1  | HASH GROUP BY     |      |
|   2  | TABLE ACCESS FULL | EMP  |
-----------------------------------

В этом плане выполнения появился новый шаг HASH GROUP BY, который в общем более быстрый и эффективный шага выполнения операции GROUP BY, он точно не сортирует данные.

Если в вашей системе получаются другие результаты, это не должно вас вводить в заблуждение. Если в вашей системе получились отсортированные результаты, это не имеет никакого значения – потребуется выполнить только один контрпример, и все встанет на свои места. Только использование предложения ORDER BY позволяет сделать предположения о порядке сортировки данных. Многие вещи – от параллельных запросов до секционирования, до индексов по инвертированным ключам, до хеш-секционированных индексов, до шагов HASH GROUP BY, до наличия (или отсутствия) индексов со структурой B-дерева – могут и будут возвращать данные не в отсортированном порядке.

Если вам нужны отсортированные данные, вы должны использовать предложение ORDER BY; нет никакой возможности обойтись без него. Более подробное обсуждение предложения ORDER BY см. на сайте asktom.oracle.com/tkyte/OrderBy.html.  


 

Ведущий данной колонки Том Кайт (Tom Kyte, thomas.kyte@oracle.com) работает в корпорации Oracle с 1993 года. Кайт – вице-президент Oracle, возглавляющий группу Oracle Public Sector; он автор таких книг, как "Expert Oracle: 9i and 10g Programming Techniques and Solutions" (Apress, 2005), "Effective Oracle by Design" (Oracle Press, 2003), а также ряда других.

E-mail this page