views:

59

answers:

4

I am running a query that returns me a collection of date objects for months between a certain date range. The query works fine, but is very slow (~2 seconds on my local machine, ~30 in our corporate development environment). Here it is:

SELECT ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rownum) AS MONTH
FROM all_objects
WHERE ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rownum) <= TO_DATE('200805', 'YYYYMM')

Currently, it will only return one month, but if you extend the second date string, it returns more.

I have two questions. First, why does this run so slow? I know Oracle functions really slow down a query, but this takes about 30 seconds on a development machine at my work.

The second, and more puzzling question: why does the runtime shorten to a fraction of a second when you extend the range to, say, '201805'? I would think that a greater range would take longer. It seems to be the opposite effect.

+4  A: 

Use this instead,

SELECT ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rn) AS MONTH
FROM (select level rn from dual connect by level < 4000)
WHERE ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rn) <= TO_DATE('200805', 'YYYYMM')
;

This avoids all_objects which is likely to be different between your two environments.

all_objects is a complex view so will not be as performant as the inline view used above. If you do not want to use the "connect by" syntax then create a table of integers and use that.

Janek Bogucki
+1 for the quick and comprehensive response
Mark Baker
Thanks for the explanation. Would you be able to hazard an answer to my question #2?
Ishmael Smyrnow
@Ishmael Sorry, I cannot help with your second question. Maybe looking at the query plan in SQL Developer will help you understand this.
Janek Bogucki
@Janek Thanks, I'll do that.
Ishmael Smyrnow
+3  A: 

Slight variant of Janek's function that gets rid of the arbitrary 4000 month limit by using the MONTHS_BETWEEN() function

SELECT ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rn) AS MONTH 
  FROM ( select level rn 
           from dual 
           connect by level < abs(months_between(TO_DATE('200804', 'YYYYMM'),TO_DATE('201805', 'YYYYMM')))+2
       ) 
 WHERE ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rn) <= TO_DATE('201805', 'YYYYMM') 
; 
Mark Baker
I like this answer, and I'll be using this in my code. I think the arbitrary limit would throw future coders off. This code explains itself a bit better.
Ishmael Smyrnow
+2  A: 

There is no need to use inline views and I see too much date functions being used. If you skip all that, this remains:

SQL> var START_YM varchar2(6)
SQL> var END_YM varchar2(6)
SQL> exec :START_YM := '200804'; :END_YM := '201805'

PL/SQL procedure successfully completed.

SQL>  select add_months(to_date(:START_YM,'yyyymm'),level-1) m
  2     from dual
  3  connect by level <= months_between(to_date(:END_YM,'yyyymm'),to_date(:START_YM,'yyyymm'))+1
  4  /

M
-------------------
01-04-2008 00:00:00
01-05-2008 00:00:00
01-06-2008 00:00:00
<... 116 rows skipped ...>
01-03-2018 00:00:00
01-04-2018 00:00:00
01-05-2018 00:00:00

122 rows selected.

Which looks even easier ...

Regards, Rob.

Rob van Wijk
Very nice. This is much simpler as well.
Ishmael Smyrnow
A: 

Part of the difficulty here is that it needs to evaluate ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rownum) for every row in the ALL_OBJECTS view. If you rewrite the where clause, it will then use a different plan with COUNT STOPKEY instead of COUNT.

Try the query below, instead. This ran quite a lot faster on mine.

SELECT ADD_MONTHS(TO_DATE('200804', 'YYYYMM'), -1+rownum) AS MONTH
FROM all_objects
where 
  months_between(date '2008-05-01, date '2008-04-01') >= rownum

The comment that you made about using 201805 to make the query run faster is actually wrong. The query does not run faster, it will just bring the first rows back faster, so appears to be faster.

With the end date set at 2008-05-01, it needs to run right through the whole of the ALL_OBJECTS table before returning any rows but with the longer time period, it will return rows to you when the buffer is full. Each query will run to completion in the same amount of time.

MikeyByCrikey
I'm not sure I agree with you. Using Oracle SQL Developer, the reported query runtime is much shorter when using a larger date range, even when I can see all the results for both queries.
Ishmael Smyrnow
Just tried it with SQL Developer and it looks like you're right... BUT the time taken appears to be the time taken to bring the first rows back rather than all the rows. If you run it using 'create table as select' or set the 'array fetch size' to 200, this will bring back all the rows and tell you the actual time for it..... which is the same for both queries.
MikeyByCrikey