Programmer's Cookbook

Recipes for the practical programmer

Monday, February 13, 2006

 

Using ROWNUM Properly for Pagination

I learned a very interesting thing about Oracle's ROWNUM variable behaves and how it affects queries that have on ORDER BY clause.

My goal was to create a Java Servlet that showed query results in a list format, with links to more pages of results and PREV and NEXT links - much like a search engine. For example, if there were 100 results, and 10 results per page, there would be PREV and NEXT links around links for the individual pages ( 1 2 3 4 5 6 7 8 9 10). Something we've all seen before.

Not seeing any problem with Oracle's ROWNUM, my basic thinking was to pass a START and END to the Servlet, which would dynamically create my queries and they would basically look like this:

SELECT * from table where rownum between <START> and <END>


For example, the first page results query would be:

SELECT * from table where rownum between 1 and 5


The next page would be:

SELECT * from table where rownum between 6 and 10


and so on.

What I didn't realize is that ROWNUM cannot be used this way because ROWNUM doesn't behave as you think it does, and it cannot be used reliably with any ORDER BY clause. See below

Let's start by getting a known set of results, in this case there are 10 results for employees with last name "smith". Note the value of ROWNUM


SQL> select first_name, last_name, rownum
from employee
where last_name = 'SMITH';

FIRST_NAME LAST_NAME ROWNUM
--------------- ------------------------------ ----------
CLARA SMITH 1
DEBORAH SMITH 2
GERTRUDE SMITH 3
JEFF SMITH 4
KEITH SMITH 5
KENN SMITH 6
MARK SMITH 7
MARYLYNN SMITH 8
MATTHEW SMITH 9
NICOLE SMITH 10

10 rows selected.


Great! That looks fine ... 1 through 10, they're all in order. Let's try getting just the first 5 results.


SQL> select first_name, last_name, rownum
from employee
where rownum between 1 and 5 and
last_name = 'SMITH';

FIRST_NAME LAST_NAME ROWNUM
--------------- ------------------------------ ----------
CLARA SMITH 1
DEBORAH SMITH 2
GERTRUDE SMITH 3
JEFF SMITH 4
KEITH SMITH 5

5 rows selected.


Still looks good. In reality though, i want to be able to change how the results are sorted, so i create a dynamic ORDER BY clause. In this example, let me use LOCATION:


SQL> select first_name, last_name, location, rownum
from employee
where last_name = 'SMITH'
order by location;

FIRST_NAME LAST_NAME LOC ROWNUM
--------------- ---------------------------- ----- ----------
CLARA SMITH 211 1
JEFF SMITH 329 6
GERTRUDE SMITH 335 4
MARYLYNN SMITH 335 8
KEITH SMITH 348 5
MARK SMITH 349 7
MATTHEW SMITH 450 9
NICOLE SMITH 450 10
DEBORAH SMITH 813 3
KENN SMITH 847 2

10 rows selected.


Holy cow! Why is my ROWNUM out of order? Anyway, let me see if my ROWNUM filtering clause still works for results 1 - 5. I should expect to see CLARA, JEFF, GERTRUDE, MARYLYNN, and KEITH as my results, in order.

SQL> select first_name, last_name, location, rownum 
from employee
where rownum between 1 and 5 and
last_name = 'SMITH'
order by location;

FIRST_NAME LAST_NAME LOC ROWNUM
--------------- ------------------------------ ----- ----------
CLARA SMITH 211 1
GERTRUDE SMITH 335 4
KEITH SMITH 348 5
DEBORAH SMITH 813 3
KENN SMITH 847 2

5 rows selected.


OH NO! This isn't going to work! From my research, ROWNUM is a value that is inherent to that row of data, and *not* a value that is computed on the fly. WHAT A BUMMER!

From some more research, I found a trick that is 100% effective and does not have any performance impact.

What you have to do is embed your original query with NO USE OF ROWNUM in 2 embedded SQL calls


SQL> select * from (
SELECT x.*, rownum as r FROM (
select first_name, last_name, location, rownum
from employee
where last_name = 'SMITH'
order by location
) x
)
where r between 1 AND 5;

FIRST_NAME LAST_NAME LOC ROWNUM R
--------------- ------------------------------ ----- ---------- ----------
CLARA SMITH 211 1 1
JEFF SMITH 329 6 2
GERTRUDE SMITH 335 4 3
MARYLYNN SMITH 335 8 4
KEITH SMITH 348 5 5

5 rows selected.


These are the results I expected in the first place when re-ordering by location.

And as an aside, MYSQL offers the LIMIT BY clause which works with the behavior you'd expect, without having to jump through hoops to get the subset you want.

Example:

select first_name, last_name, location, rownum 
from employee
where
rownum between 1 and 5 and
last_name = 'SMITH'
LIMIT BY 1, 5
order by location;


This would return me exactly what I want. Makes you wonder why ORACLE is so expensive and doesn't offer this seemingly intuitive functionality.

Comments:
Great help !!
This knowledge base resolved my issue
 
Very good trick. Excellent for paging mechanism
 
Great thanks, man. That solves problem of parsing the tables without any 'id' column...
 
Simply amazing.... excellent trick !!! Solved my problem...
 
Great description of the cause of the problem, and a great solution. thanks!
 
This was a perfect soultion to the problem I was having! Thanks a lot for posting it.
 
Solutionistic :). Thanks a ton ! Great explanation.
 
The only problem with this solution is that It does not offer any performance benefit bcos first of all the query without rowid restrictions is executed and then the resultset acts as a table for selecting the rows based upon the rowid values.And assuming that the query returns 1000 records it will a burden to use this techinique bcos this query will be executed every time a page is requested.so for getting a page of records 1000 records will be selected again and again.
Correct me if my understanding is incorrect.
 
The following is the same as your code, just a little more concise.

SELECT * FROM (
select first_name, last_name, location
from employee
where last_name = 'SMITH'
order by location)
where ROWNUM between 1 AND 5
 
THANK YOU is not enough....
 
dude. u r my savior! this was the prob i was facing and i just solved it using ur method! thanks a lot!!!
 
very good trick . very very thanks
 
@Chris: Your code won't work but just for the first page. You need ROWNUM to exist for the first value wHich is 1, then you can SELECT from the set between whatever low and top indexes you want. That's why the author also gives an alias to ROWNUM and then makes another SELECT anidation.

Why? Well Oracle always add functions and new features implemented on what would give more reliability (I hope) and also gives developers more options to use a single feature to solve many problems, not just one, in the case of LIMIT on MySQL it solves less problems (maybe just one). The disadvantage of course: experienced developers, efforts, time, etc. needed; added costs to what you are already paying for licenses etc.
 
Simply awesome. This solved numerous challenges I was experiencing paging results in a web app against Oracle.
 
@Gauss: thanks, yes, you're right. Sorry.
 
This is NOT the way you would actually want to do it in Oracle:
select *
from (select result.*, rownum rnum
from ( ) result )
where rnum between :min_row_to_fetch and :max_row_to_fetch;

The proper way would be:
select *
from (select result.*, rownum rnum
from ( ) result
where rownum<=:max_row_to_fetch)
where rnum >= :min_row_to_fetch;

The difference may not seem to be significant, BUT with cost-based optimization the last method be able to use the STOPKEY properly. The reason for this comes from the fact that oracle can not transfer a prodicate (rownum<:max_row) into a inline-view containing rownum.
Because of this the Oracle optimizer will loose the oportunity to stop early. Oracle will build the resultset, and then use the "where rownum between ..."
This means that using this other approach we can get the first 25 rows, then the 25 next rows, and the next 25 rows (and so on) alot faster. With the cost-based optimizer you will probably also want to use /*+ first_rows */ hint (if not set for the instance).
 
create or replace function testfunc (start_row in varchar2,end_row in varchar2) return sys_refcursor
as
c_test sys_refcursor;
begin
open c_test for SELECT MOBILE_NO,HIS_NUMBER
FROM
(SELECT rownum rnum, MOBILE_NO,HIS_NUMBER
FROM
(SELECT MOBILE_NO,HIS_NUMBER
FROM calls
ORDER BY MOBILE_NO)
WHERE rownum <= end_row
)
WHERE rnum > start_row ;
return c_test;
end;
/
USE THIS FOR PAGING IT WILL WORK IN ORACLE......
THANKS
SANDIP
 
great post, thanks much!
solved my problem!
 
I've found that Rajesh's solution of
SELECT * FROM (
select first_name, last_name, location
from employee
where last_name = 'SMITH'
order by location)
where ROWNUM between 1 AND 5

Scores the lowest cost with Oracle 11G cost optimizer, with COUNT STOPKEY fully leveraged
 
This comment has been removed by the author.
 
Great help. Thank you.
 
Post a Comment



<< Home

This page is powered by Blogger. Isn't yours?