4

This question is now moot

I've recomputed statistics on the table, added new indices, and re-analyzed the existing indices. This has completely changed my results and invalidates most of my findings below. At this point I've found a new query which is sufficiently performant and doesn't need any ROWNUM trickery.

Also, I think it's worth pointing out that the query below, as written, is not guaranteed to give the results I wanted. Adding DISTINCT to the mid-query can potentially destroy the ordering I attempted to apply in the innermost query. In practice this was not occurring, but I can't rely on that.


Original question

I've written a query which seems to perform dramatically better when I put in a fake ROWNUM check:

  SELECT * FROM
  (
    SELECT DISTINCT * FROM 
    ( 
      SELECT TransactionID FROM WOWDev.QueryLog WHERE UPPER(UserName)=UPPER('xyz') AND TransactionID IS NOT NULL ORDER BY TransactionID DESC
    ) 
    WHERE ROWNUM<=1e100 -- fake ROWNUM check! this gets us on the fast path
  )
  WHERE ROWNUM<=50 

Here's the optimizer plan.

SELECT STATEMENT, GOAL = ALL_ROWS           38025   50  650
 COUNT STOPKEY                  
  VIEW  JSTILES     38025   801 10413
   SORT UNIQUE NOSORT           38025   801 3204
    COUNT STOPKEY                   
     VIEW   JSTILES     38024   801 3204
      TABLE ACCESS BY INDEX ROWID   WOWDEV  QUERYLOG    38024   545694  9276798
       INDEX FULL SCAN DESCENDING   WOWDEV  IX_QUERYLOG_TID 1263    212704  

If I comment out the fake ROWNUM check, suddenly the query falls apart and becomes much slower (as well as having a 5x cost).

SELECT STATEMENT, GOAL = ALL_ROWS           204497  50  650
 COUNT STOPKEY                  
  VIEW  JSTILES     204497  34865   453245
   SORT GROUP BY STOPKEY            204497  34865   592705
    INDEX FAST FULL SCAN    WOWDEV  IX_QUERYLOG_USER_TID    204462  545694  9276798

Obviously it's hitting a completely different index as well (one which apparently doesn't suit it).

If I simplify the query naturally and remove all the redundancy, we get a similar execution plan to the poor version, and similarly poor performance as well.

SELECT * FROM 
(
  SELECT DISTINCT TransactionID
          FROM WOWDev.QueryLog
         WHERE UPPER(UserName) = UPPER('xyz')
           AND TransactionID IS NOT NULL
         ORDER BY TransactionID DESC
)
WHERE ROWNUM <= 50

Explains like so:

SELECT STATEMENT, GOAL = ALL_ROWS           207527  50  650
 COUNT STOPKEY                  
  VIEW  JSTILES     207527  34865   453245
   SORT UNIQUE STOPKEY          207491  34865   592705
    INDEX FAST FULL SCAN    WOWDEV  IX_QUERYLOG_USER_TID    204462  545694  9276798

As suggested below, I tried replacing my ROWNUM<=1e100 with ROWNUM>0 and this also hit the fast path, but with a slightly different plan:

SELECT STATEMENT, GOAL = ALL_ROWS           38025   50  650
 COUNT STOPKEY                  
  VIEW  JSTILES     38025   801 10413
   SORT UNIQUE NOSORT           38025   801 3204
    COUNT                   
     FILTER                 
      VIEW  JSTILES     38024   801 3204
       TABLE ACCESS BY INDEX ROWID  WOWDEV  QUERYLOG    38024   545694  9276798
        INDEX FULL SCAN DESCENDING  WOWDEV  IX_QUERYLOG_TID 1263    212704  

Can anyone explain this behavior? Is there a cleaner, less hacky way to get Oracle on the fast path?

StilesCrisis
  • 15,972
  • 4
  • 39
  • 62
  • What is the execution plan for `SELECT * FROM ( SELECT DISTINCT TransactionID FROM WOWDev.QueryLog WHERE UPPER(UserName)=UPPER('xyz') AND TransactionID IS NOT NULL ORDER BY TransactionID DESC ) WHERE ROWNUM<=50` (This should force using `INDEX FULL SCAN` with `COUNT STOPKEY` right away)? – potashin Jun 10 '15 at 03:23
  • I'll check that out tomorrow morning and get back to you. – StilesCrisis Jun 10 '15 at 03:40
  • I've posted the plan for that. It doesn't change anything significant from the slow version. – StilesCrisis Jun 10 '15 at 21:49

3 Answers3

2

This is not necessarily the correct answer but....

I have used ROWNUM > 0 in the past to force 'materialisation' of the data.

This in turn allows the query processor to get the cardinality correct.

For example if the query planner thinks a particular predicate will only return one row it will often use a cartesian join on it. If the data is in fact not one row but lots, the cartesian joins results in lots of rows and lots of incorrect processing. Adding ROWNUM > 0 forces it to evaluate rownum for every row before it can evaluate ROWNUM > 0, effectively forcing a materialisation of data

It doesn't look like this is actually your problem though.

Perhaps because of the spread of the data it's quicker to materialise then search the table rather than trying to check the index first.

Is there a way in that plan dump to work out where the predicates are being applied?

It's a shame you have to UPPER the field because it makes it non sargeable and it won't use an index on that field.

Nick.Mc
  • 18,304
  • 6
  • 61
  • 91
  • We actually have a functional index on `UPPER(UserName), TransactionID` but it's almost impossible to get Oracle to use it for anything. It avoids it even in cases where it seems like it ought to work. I wish there were better ways to do case-insensitive searches. That's really a weak point in Oracle. – StilesCrisis Jun 10 '15 at 03:29
  • 1
    Totally off topic but this is certainly something that suprised me when going from SQL Server to Oracle.... that and blank strings get turned into NULLs – Nick.Mc Jun 10 '15 at 07:00
  • 1
    Don't get me started on the NULL blank strings! There's no excuse for that. It's very frustrating that there's no way to represent the empty string naturally. – StilesCrisis Jun 10 '15 at 07:01
  • I did find that using `ROWNUM>0` gave the same results as `ROWNUM<=1e100`, but with a slightly different plan. Interesting experiment! – StilesCrisis Jun 10 '15 at 21:51
  • So the two different versions of rownum give a different plan but both of these use the same indexes, basically implying that the rownum is forcing to use the correct indexes. I'm still curious about the cardinality estimates in various parts of the plan – Nick.Mc Jun 10 '15 at 22:56
  • I'm not sure about ROWNUM > 0 forcing materialisation of the data. Have you got a demonstration of it? – David Aldridge Jun 11 '15 at 22:59
  • You might try an index on (case when transactionid is not null then UPPER(UserName) end, transactionid ), then "select transactionid from WOWDev.QueryLog where case when transactionid is not null then UPPER(UserName) end = Upper('xyz') – David Aldridge Jun 11 '15 at 23:23
  • With regards to `Rownum`. It's been a while. All I recall is I had issues with incorrect cardinality estimates, and adding ROWNUM>0 fixed it. When I looked at the query plan there was an operator that looked like it was materialising (can't remember what it was), and the cardinality estimates were corrected. Oracle isn't my prime area at the moment and I don't have time to whip up a demo. There's plenty of googleness like this https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:32812348052#2030256639041 that appears to indicate that it "might' materialize. – Nick.Mc Jun 12 '15 at 01:50
0

As I recall, one of the effects of placing a round predicate in a subquery is that it prevents predicate pushing, and certain types of view merging.

Although they are both generally beneficial for query performance, there are some cases where they have undesirable side effects. This used to be a trick that could be used instead of an optimiser hint to prevent that query transformation from being considered.

David Aldridge
  • 51,479
  • 8
  • 68
  • 96
-1

You have reduced the possible results adding the "fake" romwnum WHERE ROWNUM<=1e100

This instructs the database that ROWNUM must be at most 1e100.


The reason why the first query is faster is that the outher select needs to process a small subset.

Maybe you can achieve the "faster" result by SELECT * FROM ( SELECT DISTINCT * FROM ( SELECT TransactionID FROM WOWDev.QueryLog WHERE UPPER(UserName)=UPPER('xyz') AND TransactionID IS NOT NULL ORDER BY TransactionID DESC ) WHERE ROWNUM<=50 -- move inside to get small result set )

Vhortex
  • 381
  • 1
  • 7
  • 1
    You understand that 1e100 means 10^100, right? That's an absurdly large number. Oracle cannot process that much data regardless. – StilesCrisis Jun 10 '15 at 02:14
  • Also, no, moving the ROWNUM inside doesn't help. – StilesCrisis Jun 10 '15 at 02:24
  • Oracle picks the indexes base on what you are asking it. You asked it to compare "ROWNUM" and will pick what it believes as best. Any other reason why distinct is separated as another outer query? – Vhortex Jun 10 '15 at 02:24
  • SELECT DISTINCT TransactionID FROM WOWDev.QueryLog WHERE UPPER(UserName)=UPPER('xyz') AND TransactionID IS NOT NULL ORDER BY TransactionID DESC – Vhortex Jun 10 '15 at 02:30
  • Well now you've gotten rid of the ROWNUM check completely. That's not even kind of equivalent. – StilesCrisis Jun 10 '15 at 02:31
  • I just collapsed your inefficient extra nesting. the one with DISTINCT. What's wrong with that? If you don't want an answer then why bother asking here – Vhortex Jun 10 '15 at 02:34
  • As far as I read it "If I comment out the fake ROWNUM check, suddenly the query falls apart and becomes much slower ".. you said it yourself that the extra ROWNUM check is not needed since it is fake and you are complaining I removed it? – Vhortex Jun 10 '15 at 02:35