Search code examples
sqloracle-databasegaps-and-islands

Oracle: select missing dates


I have a table with (among other things) dates in a field.

I need to get a list of all dates that are more recent than the oldest date, older than the most recent date, and are completely missing from the table.

So, if the table contained:

2012-01-02
2012-01-02
2012-01-03
2012-01-05
2012-01-05
2012-01-07
2012-01-08

I want a query that returns:

2012-01-04
2012-01-06

Solution

  • Something like this (assuming your table is named your_table and the date column is named the_date):

    with date_range as (
          select min(the_date) as oldest, 
                 max(the_date) as recent, 
                 max(the_date) - min(the_date) as total_days
          from your_table
    ),
    all_dates as (
       select oldest + level - 1 as a_date
       from date_range
       connect by level <= (select total_days from date_range)
    )
    select ad.a_date
    from all_dates ad
      left join your_table yt on ad.a_date = yt.the_date
    where yt.the_date is null
    order by ad.a_date;  
    

    Edit:
    the WITH clause is called a "common table expression" and is equivalent to a derived table ("inline view").

    It's similar to

    select * 
    from ( 
         ..... 
    ) all_dates
    join your_table ...
    

    The second CTE simply creates a list of dates "on-the-fly" using a undocumented feature of Oracle's connect by implementation.

    Re-using a select (like I did with calculating the first and last date) is a bit easier (and IMHO more readable) than using derived tables.

    Edit 2:

    This can be done with a recursive CTE as well:

    with date_range as (
          select min(the_date) as oldest, 
                 max(the_date) as recent, 
                 max(the_date) - min(the_date) as total_days
          from your_table
    ),
    all_dates (a_date, lvl) as (
       select oldest as a_date, 1 as lvl
       from date_range 
       union all
       select (select oldest from date_range) + lvl, lvl + 1
       from all_dates 
       where lvl < (select total_days from date_range)
    )
    select ad.a_date, lvl
    from all_dates ad    
      left join your_table yt on ad.a_date = yt.the_date
    where yt.the_date is null
    order by ad.a_date;  
    

    Which should work in all DBMS supporting recursive CTEs (PostgreSQL and Firebird - being more standard compliant - do need the recursive keyword though).

    Note the hack select (select oldest from date_range) + lvl, lvl + 1 in the recursive part. This should not be necessary, but Oracle still has some bugs with regards to DATEs in a recursive CTE. In PostgreSQL the following works without problems:

    ....
    all_dates (a_date, lvl) as (
       select oldest as a_date, 0 as lvl
       from date_range 
       union all
       select a_date + 1, lvl + 1
       from all_dates 
       where lvl < (select total_days from date_range)
    )
    ....