10

This is a never-ending topic for me and I'm wondering if I might be overlooking something. Essentially I use two types of SQL statements in an application:

  1. Regular queries with a "fallback" limit
  2. Sorted and paged queries

Now, we're talking about some queries against tables with several million records, joined to 5 more tables with several million records. Clearly, we hardly want to fetch all of them, that's why we have the above two methods to limit user queries.

Case 1 is really simple. We just add an additional ROWNUM filter:

WHERE ...
  AND ROWNUM < ?

That's quite fast, as Oracle's CBO will take this filter into consideration for its execution plan and probably apply a FIRST_ROWS operation (similar to the one enforced by the /*+FIRST_ROWS*/ hint.

Case 2, however is a bit more tricky with Oracle, as there is no LIMIT ... OFFSET clause as in other RDBMS. So we nest our "business" query in a technical wrapper as such:

SELECT outer.* FROM (
  SELECT * FROM (
    SELECT inner.*, ROWNUM as RNUM, MAX(ROWNUM) OVER(PARTITION BY 1) as TOTAL_ROWS
    FROM (
      [... USER SORTED business query ...]
    ) inner
  ) 
  WHERE ROWNUM < ?
) outer
WHERE outer.RNUM > ?

Note that the TOTAL_ROWS field is calculated to know how many pages we will have even without fetching all data. Now this paging query is usually quite satisfying. But every now and then (as I said, when querying 5M+ records, possibly including non-indexed searches), this runs for 2-3minutes.

EDIT: Please note, that a potential bottleneck is not so easy to circumvent, because of sorting that has to be applied before paging!

I'm wondering, is that state-of-the-art simulation of LIMIT ... OFFSET, including TOTAL_ROWS in Oracle, or is there a better solution that will be faster by design, e.g. by using the ROW_NUMBER() window function instead of the ROWNUM pseudo-column?

skaffman
  • 398,947
  • 96
  • 818
  • 769
Lukas Eder
  • 211,314
  • 129
  • 689
  • 1,509

4 Answers4

6

The main problem with Case 2 is that in many cases the whole query result set has to be obtained and then sorted before the first N rows can be returned - unless the ORDER BY columns are indexed and Oracle can use the index to avoid a sort. For a complex query and a large set of data this can take some time. However there may be some things you can do to improve the speed:

  1. Try to ensure that no functions are called in the inner SQL - these may get called 5 million times just to return the first 20 rows. If you can move these function calls to the outer query they will be called less.
  2. Use a FIRST_ROWS_n hint to nudge Oracle into optimising for the fact that you will never return all the data.

EDIT:

Another thought: you are currently presenting the user with a report that could return thousands or millions of rows, but the user is never realistically going to page through them all. Can you not force them to select a smaller amount of data e.g. by limiting the date range selected to 3 months (or whatever)?

Tony Andrews
  • 129,880
  • 21
  • 220
  • 259
  • Thanks. The rare cases when we have functions, they're usually `DETERMINISTIC` and backed by a functional-based index. Or they're just functions on projections which are relevant only after limiting the outermost query... As far as the `FIRST_ROWS` hint is concerned, unfortunately you're wrong :-) That was the source of all doom for Oracle when we still had it, because that hint will force `NESTED LOOPS` on a complex sorted query, which resulted in several full table scans on the huge tables... :-/ – Lukas Eder May 17 '11 at 15:52
  • OK, well FIRST_ROWS may not always be right, but it can't _always_ be wrong either - the whole point of FIRST_ROWS is to optimise for returning the first rows from a query rather than all rows. YMMV! – Tony Andrews May 17 '11 at 16:18
  • Yeah, it really depends on the Oracle version. We ran into major trouble with that hint in Oracle 11g. Before, this hint was working great... – Lukas Eder May 17 '11 at 16:23
  • In 11g you should be using FIRST_ROWS_n (with a number) as FIRST_ROWS is deprecated. It might not fix your problem though. – Gary Myers May 18 '11 at 00:27
  • @Tony, about your edit: Of course, that's what we try to do. Those screens where so much data *could* be requested, we require at least 1-2 very selective filters from the user. This question is more about better understanding the technique of paging and sorting in Oracle – Lukas Eder May 18 '11 at 06:32
  • @Gary, that's true. Believe me though, I've tried quite a few hints... :-) – Lukas Eder May 18 '11 at 07:03
  • **ACCEPTED ANSWER**: After a couple of answers, it seems that our solution is about as good as it gets. This answer suggests some workarounds and ideas that we use as well to keep our application from running "too dangerous" queries. Thanks Tony – Lukas Eder May 18 '11 at 09:45
3

You might want to trace the query that takes a lot of time and look at its explain plan. Most likely the performance bottleneck comes from the TOTAL_ROWS calculation. Oracle has to read all the data, even if you only fetch one row, this is a common problem that all RDBMS face with this type of query. No implementation of TOTAL_ROWS will get around that.

The radical way to speed up this type of query is to forego the TOTAL_ROWS calculation. Just display that there are additional pages. Do your users really need to know that they can page through 52486 pages? An estimation may be sufficient. That's another solution, implemented by google search for example: estimate the number of pages instead of actually counting them.

Designing an accurate and efficient estimation algorithm might not be trivial.

Vincent Malgrat
  • 66,725
  • 9
  • 119
  • 171
  • That's not entirely true. Oracle has to read a lot of data anyway to perform the sorting (which unfortunately, I missed in the example. I'll update immediately). Paging without sorting doesn't make sense in my case. On the other hand, you're right, we could just ignore the maximum number of pages and at least omit the `TOTAL_ROWS` column, which isn't really really necessary. But with user sorting, I doubt that the effect will be tremendous – Lukas Eder May 17 '11 at 15:54
  • @Lukas: you're right, most of the time Oracle will need to read the entire dataset in order to sort it, especially with user sorting where you can't possibly index every column combination. – Vincent Malgrat May 18 '11 at 07:43
  • exactly. But I've been thinking again about your inputs. Maybe replacing `MAX(ROWNUM)` by `count(*)` might accelerate things a bit... I don't know how the `ROWNUM` pseudo-column is evaluated, when put in the `MAX` aggregate function... Maybe Oracle is smart enough for a transformation. – Lukas Eder May 18 '11 at 08:23
  • @Lukas: the `COUNT(*)` might be more efficient but the performance difference will probably be unnoticeable. – Vincent Malgrat May 18 '11 at 08:57
  • You're right. It's only a 200ms improvement on a 57s query. Which is still an improvement... – Lukas Eder May 18 '11 at 09:37
3

A "LIMIT ... OFFSET" is pretty much syntactic sugar. It might make the query look prettier, but if you still need to read the whole of a data set and sort it and get rows "50-60", then that's the work that has to be done.

If you have an index in the right order, then that can help.

Gary Myers
  • 34,963
  • 3
  • 49
  • 74
  • So you're saying, my solution is about as good as it gets (apart of course from the not really necessary window function to get `TOTAL_ROWS? ) – Lukas Eder May 18 '11 at 06:31
1

It may perform better to run two queries instead of trying to count() and return the results in the same query. Oracle may be able to answer the count() without any sorting or joining to all the tables (join table elimination based on declared foreign key constraints). This is what we generally do in our application. For performance important statements, we write a separate query that we know will return the correct count as we can sometimes do better than Oracle.

Alternatively, you can make a tradeoff between performance and recency of the data. Bringing back the first 5 pages is going to be nearly as quick as bringing back the first page. So you could consider storing the results from 5 pages in a temporary table along with an expiry date for the information. Take the result from the temporary table if valid. Put a background task in to delete the expired data periodically.

WW.
  • 23,793
  • 13
  • 94
  • 121
  • thanks for your input. As discussed in other answers, the `count(*) over (partition by 1)` or `max(rownum) over (partition by 1)` functions are not the big part of the effort. If sorting is a must, then this is only a minor additional effort for the database. Besides, I doubt that we can really do it better than Oracle. Those window (or analytic) functions are quite advanced. I'm not sure about temporary tables. With 2000 concurrent sessions, that's going to be a lot of tables to maintain...? – Lukas Eder May 18 '11 at 09:34
  • You would only need 1 table for each query screen/page that could hold results for many sessions. – WW. May 18 '11 at 22:43
  • since the queries usually include full text searches, that makes a lot of tables... :-) – Lukas Eder May 19 '11 at 06:58
  • I think we're talking at cross-purposes here. Are the columns that come back from a particular query page always the same (regardless of search criteria/ordering)? If so, you make a table with those columns plus an extra one for the user session id/expiry time. – WW. May 19 '11 at 23:11