Oracle как работает rownum

Обновлено: 04.07.2024

мне трудно конвертировать хранимые процедуры из SQL Server в Oracle, чтобы наш продукт был совместим с ним.

у меня есть запросы, которые возвращают самую последнюю запись некоторых таблиц, на основе метки времени:

SQL Server:

=> это вернет мне самую последнюю запись

но Oracle:

=> это вернет мне самую старую запись (возможно, в зависимости от индекс), независимо от ORDER BY заявление!

Я инкапсулировал запрос Oracle таким образом, чтобы соответствовать моим требованиям:

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

каков наилучший способ достичь этого ?

The where заявление выполняется до the order by . Итак, ваш желаемый запрос говорит"возьмите первую строку, а затем закажите ее по t_stamp desc". И это не то, что вы хотите.

метод подзапроса является правильным методом для этого в Oracle.

если вы хотите версию, которая работает на обоих серверах, вы можете использовать:

внешний * вернет "1" в последнем столбце. Вы нужно будет перечислить столбцы по отдельности, чтобы избежать этого.

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

альтернативой, которую я бы предложил в этом случае использования, является использование MAX(t_stamp) для получения последней строки . например,

мой предпочтительный шаблон кодирования (возможно) - надежный, обычно выполняет или лучше, чем попытка выбрать 1 - ю строку из отсортированного списка-также намерение более явно читается.
Надеюсь, это поможет .

SQLer

документированы несколько проблем с дизайном с этим в комментарии выше. Короче говоря, в Oracle вам нужно ограничить результаты вручную, когда у вас есть большие таблицы и/или таблицы с одинаковыми именами столбцов (и вы не хотите явно вводить их все и переименовывать их все). Простое решение-определить точку останова и ограничить ее в запросе. Или вы также можете сделать это во внутреннем запросе, если у вас нет ограничения конфликтующих имен столбцов. Е. Г.

будут резать вниз результаты существенно. Затем вы можете заказать или даже сделать внешний запрос для ограничения строк.

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

Администрирование Oracle. Программирование на PL\SQL. А также все что касается лидера разработки корпоративного ПО.

понедельник, апреля 18, 2011

ROWNUM в разных ситуациях

А знаете ли вы что. функция ROWNUM в разных ситуациях ведет себя по-разному. Так вот, не так давно наткнулись на интересный момент, о котором расскажу ниже.

Все, кто хоть раз пытался использовать ROWNUM вместе с сортировкой выборки, прекрасно себе представляет, какая каша выдается в итоге. Большинство прекрасно понимает, а Oracle об этом прямо заявляет, что указанная функция не гарантирует верную нумерацию при проведении сортировки.
На всякий случай показываю, как оно бывает:

В данном случае можно увидеть по плану, что ROWNUM считается до сортировки (да, COUNT - оно самое), в итоге получаем ту самую кашу.
А теперь эксперимент, сделаем "ложную" деревянную выборку одного уровня:
Забавно, но имея деревянный запрос, дающий такую же выборку (дерево никогда не получит второго уровня по условию 1=2), при ORDER SIBLINGS BY мы получаем корректные значения ROWNUM. Кроме того, в случае деревянной выборки сортировка производится как бы без сортировки (где-то в CONNECT BY (WITHOUT FILTERING) или еще где, надо трейсы смотреть), а ROWNUM (все тот же COUNT) выполняется последним перед выдачей клиенту. В результате мы получаем правильную нумерацию.

Обращаю внимание на то, что проход все также один и стоимость запроса остается неизменной.

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

P.S: По факту правильней использовать функцию из аналитики: ROW_NUMBER() OVER (ORDER BY column_name), которая гарантированно даст нужный порядок.

7 комментариев:

И всё равно я с тобой не согласен. Rownum это следствие сортировки.
то есть сначала делает деревяшка с сортировкой, потом уже выдается результат. учитывая что Rownum - это номер записи выбранный из базы до сортировки, в данном случае мы будем всегда правильно его получать. Причем на мой взгляд важен тот момент что неважно имя столбца сортировки. Для каждого столбца сортировки будет выдаватся абсолютно правильный свой Rownum.

Причем я об этом написал "Кроме того, в случае деревянной выборки сортировка производится как бы без сортировки (где-то в CONNECT BY (WITHOUT FILTERING) или еще где, надо трейсы смотреть), а ROWNUM (все тот же COUNT) выполняется последним перед выдачей клиенту." Rownum - не следствие сортировки, а вот правильность результата - следствие порядка операций, а именно того, что в деревянной выборке нумерация идет после сортировки, а в обычной - до.

Совсем не понимаю данного поста. В первом случае приведено корректное поведение БД(я уже молчу что в нормальных базах данных нет вообще всяких rownum). Во втором случае описывается "правильность результата" в конструкции запроса не описанной стандартами SQL, которую Oracle может поменять от версии к версии.
Пользуйтесь нормальным SQL синтаксисом.

Серега, тут есть 2 момента:
1. Основное. Я хотел показать, что ROWNUM может себя вести по-разному в зависимости от условий. Это из разряда экспериментов, которыми стоит заниматься для разминки мозгов, но которые не стоит применять к продакшну.
2. Я придумал как сделать в Oracle аналог мсскульного TOP N без подзапроса :)

Уговорил. Пост полезный. Сам часто отвечаю на вопрос коллег: почему при . where rownum<50 order by.
они не видят нужных записей :)

Дерево объектов

Чтобы понять рекурсию, сначала надо понять рекурсию. Возможно, поэтому рекурсивные запросы применяют так редко. Наверняка вы представляете что такое SQL-запрос, я расскажу, чем рекурсивные запросы отличаются от обычных. Тема получилась объемная, приготовьтесь к долгому чтению. В основном речь пойдет об Oracle, но упоминаются и другие СУБД.

Суть проблемы

Большинство современных СУБД (Система Управления Базами Данных) — реляционные, т.е. представляют данные в виде двумерной таблицы, в которой есть строки (записи) и столбцы (поля записей). Но на практике мы часто сталкиваемся с иной организацией данных, а именно иерархической.

Взгляните на список файлов на вашем компьютере: все они организованы в виде дерева. Аналогично можно представить книги в библиотеке: Библиотека->Зал->Шкаф->Полка->Книга. То же самое и статьи на сайте: Сайт->Раздел->Подраздел->Статья. Примеры можно приводить долго. Впрочем, тут еще можно разделить все на отдельные таблицы: таблица для хранения списка библиотек, другая таблица для списка залов, третья для шкафов и т.д. Но если заранее не известна глубина вложенности или эта вложенность может меняться, тут уж от иерархии никак не отмашешься.

Проблема в том, что данные, имеющие иерархическую структуру, очень плохо представляются в реляционной модели. В стандарте SQL-92 нет средств для их обработки.

Зато такие средства появились в стандарте SQL-1999. Правда к тому времени в Oracle уже был собственный оператор CONNECT BY. Несмотря на это, в SQL-1999 синтаксис рекурсивных запросов совершенно не похож на синтаксис CONNECT BY в Oracle и использует ключевое слово WITH. Реализация же рекурсивных запросов в других СУБД несколько запоздала, так в MS SQL Server она появилась лишь в версии 2005.

Так же как и в синтаксисе, есть отличия и в терминологии. В Oracle обычно обсуждаемые запросы называются “иерархические”, у всех остальных “рекурсивные”. Суть от этого не меняется, я буду использовать и то и другое.

От слов к делу!

Для демонстрации будем использовать структуру каталогов, нам потребуется тестовая таблица, состоящая из 3-х полей:
id – идентификатор,
pid – идентификатор родителя (ссылается на id другой записи в той же таблице),
title – название каталога (вместо него может быть что угодно, даже несколько полей или ссылок к другим таблицам).

create table test_table (
id int ,
pid int ,
title varchar (256)
);

Здесь я использовал синтаксис mySQL, в других СУБД он несколько отличается. Так, в Oracle используются другие типы данных: вместо int — number, а вместо varchar — varchar2.

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

Думаю, остальные строки заполнить не составит сложностей. Получается почти как на картинке, только не в том порядке. Я специально заполнял не по-порядку, чтобы простым SELECT * FROM test_table получалось не иерархическая структура:

Тестовые данные готовы. Приступим к выборкам.

mySQL

Вынужден огорчить пользователей mySQL – в этой СУБД придется рекурсию организовывать внешними по отношению к СУБД средствами, например на php (код приводить не буду, его публиковали не раз, например здесь, достаточно поискать по запросу “mySQL tree” и т.п.).

Впрочем, есть иной подход, заключающийся в создании левой и правой границы для каждого узла, предложенный Джо Селко. Тогда можно будет обойтись обычными, не рекурсивными запросами.

Как-то я выводил дерево объектов в действующем проекте на php. База данных была на mySQL. Поплевавшись на отсутствие удобных операторов, я решил тогда не отображать все дерево целиком, а показать пользователю только первый уровень (схлопнутое дерево). При клике по плюсику в узле дерева отображались дочерние узлы для выбранного объекта, при этом они подгружались через AJAX. Выборка дочерних узлов по известному pid происходит быстро, поэтому интерфейс получился вполне шустрым. Возможно, это не лучшее решение, но оно имеет право на жизнь.

SQL-1999

В отличие от предыдущего стандарта SQL-92, в названии следующего решили отобразить номер тысячелетия, чтобы он не страдал от проблемы двухтысячного года. Помимо этого :-), появились новые типы данных (LOB), новые предикаты SIMILAR и DISTINCT, точки сохранения транзакций, объекты и их методы, и многое другое. Среди нововведений появились также рекурсивные запросы, о которых мы сейчас и поговорим.

Для получения иерархических данных используется временное представление, которое описывается оператором WITH. После этого из нее выбираются данные простым селектом. В общем виде синтаксис примерно такой:

В MS SQL нет ключевого слова RECURSIVE, его следует опустить. Но в остальном все то же самое. Такой синтаксис поддерживается в DB2, Sybase iAnywhere, MS SQL, начиная с версии 2005, и во всех базах данных, которые поддерживают стандарт SQL 1999.

Тогда для получения нашего дерева запрос получится такой:

WITH RECURSIVE
Rec (id, pid, title)
AS (
SELECT id, pid, title FROM test_table
UNION ALL
SELECT Rec.id, Rec.pid, Rec.title
FROM Rec, test_table
WHERE Rec.id = test_table.pid
)
SELECT * FROM Rec
WHERE pid is null ;

Здесь используется рекурсивная таблица Rec, которую мы сами придумали, построив ее по исходной таблице test_table. В описании Rec указано правило, каким образом соединять: WHERE Rec.id = test_table.pid. А в главном запросе отметили, что начинать надо с записи, у которой pid является пустым, т.е. с корневой записи.

Честно говоря, я никогда не работал в MS SQL Server 2005 или другой СУБД, использующей такой синтаксис. Поэтому написал этот запрос чисто из теоретических соображений. Для общности картины, чтобы было с чем сравнить.

В MS SQL 2008 можно применить более новое средство hierarchyid. Спасибо XaocCPS за его описание.

Oracle

Описание синтаксиса в документации напоминает бусы: на единую нить запроса нанизываются нужные операторы. Никому не приходило в голову сделать украшение для гички?

Тут видно, что единственно важное условие для построения иерархического запроса – это оператор CONNECT BY, остальное “нанизывается” по мере надобности.

Необязательный оператор START WITH говорит Ораклу с чего начинать цикл, т.е. какая строка (или строки) будет корневой. Условие может быть практически любым, можно даже использовать функции или внутренние запросы: pid is null, или или даже substr(title, 1, 1) = ‘Р’.

Условие после CONNECT BY нужно указать обязательно. Тут надо сказать Ораклу, как долго продолжать цикл. Что-то в духе while в обычных языках программирования. Например, мы можем попросить достать нам 10 строк: rownum<=10 – он и нафигачит нам в цикле ровно 10 одинаковых строк. Почему одинаковых? Да потому что мы указали какую строку выбрать первой, а как найти следующую нет – вот он и выдает 1-ую строку нужное количество раз. Кстати сказать, rownum это псевдостолбец, в котором нумеруются строки, начиная от 1 в порядке их выдачи. Его можно использовать не только в иерархических запросах. Но это уже другая история.

Как же получить нормальную иерархию? Нужно использовать специальный оператор, который называется PRIOR. Это обычный унарный оператор, точно такой же как + или -. “Позвоните родителям” – говорит он, заставляя Оракл обратиться к предыдущей записи. С его помощью можно написать правило pid = PRIOR id (или PRIOR как говорится, от перестановки мест…).

Что получается? Оракл находит первую запись, удовлетворяющую условию в START WITH, и принимается искать следующую. При этом к той первой записи можно обратиться через PRIOR. Если мы все сделали правильно, то Оракл будет искать записи, в которых в поле для хранения информации о родителе (pid) будет содержаться значение, равное идентификатору id нашей первой записи. Таким образом будут найдены все потомки корневой записи. А так как процесс рекурсивный, аналогичный поиск будет продолжаться с каждой найденной строкой, пока не отыщутся все потомки.

Теперь у нас есть все необходимое, чтобы написать иерархический запрос в Oracle. Но прежде чем мы его напишем, расскажу еще об одной штуке. Порядок строк это хорошо, но нам было бы трудно понять, две строки рядом это родитель и его потомок или два брата-потомка одного родителя. Пришлось бы сверять id и pid. К счастью, Oracle предлагает в помощь дополнительный псевдостолбец LEVEL. Как легко догадаться, в нем записывается уровень записи по отношению к корневой. Так, 1-ая запись будет иметь уровень 1, ее потомки уровень 2, потомки потомков — 3 и т.д.

SELECT level , id, pid, title
FROM test_table
START WITH pid is null
CONNECT BY PRIOR >

Неплохо. Все дочерние строки оказываются под своими родителями. Сортировку бы еще добавить, чтобы записи одного уровня выводились не абы-как, а по алфавиту. Ну чтож, сортировка это просто: добавим в конец запроса конструкцию ORDER BY title.

SELECT level , id, pid, title
FROM test_table
START WITH pid is null
CONNECT BY PRIOR > ORDER BY title;

О, нет! Вся иерархия поломалась. Что же получилось? Оракл честно выбрал нужные строки в порядке иерархии (об этом говорит правильная расстановка level), а затем пересортировал их согласно правилу ORDER BY. Чтобы указать Ораклу, что сортировать надо только в пределах одного уровня иерархии, нам поможет маленькая добавка в виде оператора SIBLINGS. Достаточно изменить условие сортировки на ORDER SIBLINGS BY title – и все встанет на свои места.

Кстати, возможно все еще не понятно, почему этот порядок строк является деревом. Можно убрать все “лишние” поля и добавить отступы, станет более наглядно:

SELECT lpad( ' ' , 3* level )||title as Tree
FROM test_table
START WITH pid is null
CONNECT BY PRIOR > ORDER SIBLINGS BY title;

Ну вот, теперь все в точности, как на картинке в самом начале статьи.

Помните, файловые менеджеры обычно пишут путь к каталогу, в котором вы находитесь: /home/maovrn/documents/ и т.п.? Неплохо было бы и нам сделать так же. А сделать это можно абсолютно не напрягаясь: специалисты из Oracle все уже сделали за нас. Просто берем и используем функцию SYS_CONNECT_BY_PATH(). Она принимает два параметра через запятую: название колонки и строку с символом-разделителем. Будем не оригинальны, напишем так: SYS_CONNECT_BY_PATH(title, ‘/’).

Заодно ограничим вывод, выбрав только одну строку. Для этого, как всегда, нужно добавить условие WHERE. Даже в иерархическом запросе ограничивающее условие применяется ко всем строкам. Вставить его надо до иерархической конструкции, сразу после FROM. Для примера определим путь до “Сервер 1”, который у нас записан с >

SELECT SYS_CONNECT_BY_PATH(title, '/' ) as Path
FROM test_table
WHERE > START WITH pid is null
CONNECT BY PRIOR >

Еще может быть полезен псевдостолбец CONNECT_BY_ISLEAF. Его можно использовать так же, как LEVEL. В этом псевдостолбце напротив каждой строки проставляется 0 или 1. Если есть потомки – проставится 0. Если потомков нет, такой узел в дереве называется “листом”, тогда и значение в поле CONNECT_BY_ISLEAF будет равно 1.

Устали? Осталось немного, самое страшное уже позади. Раньше мы использовали оператор PRIOR, который ссылался к родительской записи. Помимо него есть другой унарный оператор CONNECT_BY_ROOT, который ссылается (ни за что не догадаетесь!) на корневую запись, т.е. на самую первую в выборке.

SELECT id, pid, title, level ,
CONNECT_BY_ISLEAF as IsLeaf,
PRIOR title as Parent,
CONNECT_BY_ROOT title as Root
FROM test_table
START WITH pid is null
CONNECT BY PRIOR > ORDER SIBLINGS BY title;

Стоит отметить, что если в результате выполнения запроса обнаружится петля, Oracle выдаст ошибку. К счастью, ее можно обойти, хотя если в данных содержатся петли – это явно ошибка, в деревьях не бывает петель. На картинке с “бусами” запроса был нарисован оператор NOCYCLE после CONNECT BY – его мы и будем применять. Теперь запрос не будет вылетать. А чтобы определить “больной” участок, воспользуемся псевдостолбцом CONNECT_BY_ISCYCLE – в нем во всех хороших строках будет записано 0, а в тех, которые приводят к петлям, волшебным образом окажется 1.

Чтобы проиллюстрировать это, придется немного подпортить данные. ЛискиПресс ссылается у нас на город Лиски; изменим запись Лиски, чтобы она ссылалась на ЛискиПресс (не забудьте про commit – я вечно забываю):

Если мы запустим какой-нибудь из предыдущих запросов, увидим, что и Лиски, и ЛискиПресс выпали из выборки, будто их нет совсем. Бегая в цикле, Оракл просто перестал на них натыкаться, т.к. нет пути от записи Россия к городу Лиски. Изменим условия START WITH, чтобы начинать с города Лиски – появится ошибка. Умный Оракл видит что запись уже выбиралась ранее и отказывается бегать в бесконечном цикле. Исправляем ошибку:

SELECT CONNECT_BY_ISCYCLE as cycl, id, pid, title
FROM test_table
START WITH > CONNECT BY NOCYCLE PRIOR >

Практические примеры

Иерархические запросы можно применять не только там, где есть явная иерархия.

Например, рассмотрим задачу получения списка пропущенных номеров из последовательности. Это бывает нужно, когда в некоей таблице id генерируется автоматически путем увеличения на 1, но часть записей были удалены. Нужно получить список удаленных номеров. По хорошей традиции, это следует сделать одним селектом.

Подготовим тестовые данные. Удалим из нашей таблицы пару записей:

С чего начнем? Во-первых, неплохо было бы получить список номеров подряд от 1 до максимального значения в нашей таблице, чтобы было с чем сравнивать. Выяснить максимальное значение id из таблицы, думаю, не составит никаких трудностей:

А вот чтобы сгенерировать последовательность от 1 до max как раз и понадобится рекурсивный запрос. Ведь как здорово просто взять и получить нужное количество строк! Достаточно будет их пронумеровать – и вот список готов.

SELECT rownum as rn FROM dual
CONNECT BY level <= ( SELECT max (id) FROM test_table);

Конструкция “SELECT … FROM dual” используется, когда надо вычислить значение функции, не производя при этом выборки данных. Dual – это системная таблица, состоящая из одного столбца и одной строки. Запрос из нее всегда возвращает одну строку со значением ‘X’. Благодаря такой умопомрачительной стабильности, эту таблицу удобно использовать в качестве источника строк.

Обычно таблицу, которую нагло используют для получения нужного количества строк, не выбирая сами данные, называют pivot. В качестве такой таблицы может выступать любая большая таблица, в том числе системная. Но использование dual в Oracle является более разумным решением.

Теперь, когда список номеров подряд уже есть, достаточно пройтись по нему и сравнить, есть ли такой номер в проверяемой таблице:

SELECT sq.rn
FROM ( SELECT rownum as rn FROM dual
CONNECT BY level <= ( SELECT max (id) FROM test_table)) sq
WHERE sq.rn not in ( SELECT id FROM test_table)
ORDER BY rn;

Всё. Ведро начищено до блеска, лошадь вооружена биноклем для остроты зрения. Да не коснется вас ROLLBACK. COMMIT!

К псевдостолбцам можно относиться как к обычным колонкам в таблице, за тем лишь исключением, что данные, которые они представляют, в таблице не хранятся.

Некоторые псевдостолбцы доступны только в определенном контексте, например, лишь при использовании рекурсивных запросов.

Мы рассмотрим не все псевдостолбцы, доступны в Oracle, а лишь самые основные и часто используемые. Полный их список и описание можно почитать в докумениации.

Мы будем использовать таблицу dishes , которая создается в части про операторы сравнения.

ROWNUM

Данный псевдостолбец возвращает порядковый номер, под которым Oracle выбирает строку из таблицы. Для первой строки значение ROWNUM будет равно 1, для второй - 2, и т.д.

Один из классических примеров использования ROWNUM - ограничение количества получаемых строк из таблицы:


Если в запросе используется сортировка, то она может изменить порядок строк. Т.е. строка из таблицы могла получаться первой, и ей мог быть присвоен rownum = 1, но после того, как все строки были получены, они были отсортированы в другом порядке:


Что же делать, если мы хотим пронумеровать наши строки начиная от 1 таким образом, чтобы у самого дешевого блюда был номер 1, у более дорогого - 2 и т.п.?

Для этого можно использовать подзапросы:


Теперь все будет работать, т.к. сортировка данных была произведена в подзапросе еще *до того, как данные будут получаться внешним запросом*, а значит и до того, как каждой строке будет присваиваться значение ROWNUM .

Следует отметить, что использование оператора ">" с ROWNUM не имеет смысла. Рассмотрим это на примере:

Этот запрос ничего не выведет, несмотря на то, что строк в таблице больше трех. Все потому, что rownum хранит в себе номер строки, под которым Oracle получает ее из таблицы или соединения. В примере выше у первой строки(какой бы она не была, она все равно будет первой) значение rownum будет равно 1. Это значит, что условие rownum > 3 будет ложным, и строка не будет добавлена в выборку. Следующая строка опять будет иметь rownum = 1 , что опять приведет значение условия в False, и так будет для всех строк из таблицы dishes .

Top-N query

Получим топ-3 блюда по рейтингу с помощью rownum :


Подобного рода запросы относятся к так называемым "top-N queries", т.е. они получают часть данных, основываясь на каком-либо критерии сортировки ( в данном случае это рейтинг блюд).

ROWID

ROWID содержит в себе адрес строки в таблице. На практике он используется не часто, но иногда его значение может понадобиться сторонним библиотекам.

Сам rowid уникально идентифицирует определенную строку в таблице, но это не означает, что rowid уникален в пределах всей базы данных.

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

Для примера просто получим все строки из таблицы dishes с их rowid :


LEVEL

Данный псевдостолбец доступен только в рекурсивных запросах. Подробнее про него можно почитать в части про рекурсивные запросы.

Читайте также: