简体   繁体   中英

Remove Nulls from multiple rows; create [Start] and [End] columns

I'm trying to create a table that has start and end columns by week that dont overlap month transitions. Using January 2016 as an example, I want the results to look like:

 Start      End
 1/1/2016   1/2/2016
 1/3/2016   1/9/2016
 1/10/2016  1/16/2016
 1/17/2016  1/23/2016
 1/24/2016  1/30/2016
 1/31/2016  1/31/2016  

What I'm currently getting with the query is (I want the records in the 2nd and 3rd columns to line up accordingly):

 DATES          Wk_START_END    MONTH_START_END
 1/1/2016                       1/1/2016
 1/2/2016       1/2/2016    
 1/3/2016       1/3/2016    
 1/4/2016       
 1/5/2016       
 1/6/2016       
 1/7/2016       
 1/8/2016       
 1/9/2016       1/9/2016    
 1/10/2016      1/10/2016   
 1/11/2016      
 1/12/2016      
 1/13/2016      
 1/14/2016      
 1/15/2016      
 1/16/2016      1/16/2016   
 1/17/2016      1/17/2016   
 1/18/2016      
 1/19/2016      
 1/20/2016      
 1/21/2016      
 1/22/2016      
 1/23/2016      1/23/2016   
 1/24/2016      1/24/2016   
 1/25/2016      
 1/26/2016      
 1/27/2016      
 1/28/2016      
 1/29/2016      
 1/30/2016      1/30/2016   
 1/31/2016      1/31/2016       1/31/2016

Here's the query at the moment:

    SELECT trunc
      (sysdate, 'YEAR')+rownum-1 DATES
      --,to_char(trunc(sysdate,'YEAR') + rownum -1 ,'D') Day_Of_Wk

    , CASE
         WHEN to_char
             (trunc
             (sysdate, 'YEAR')+rownum-1, 'D') = '1' THEN trunc
                                                (sysdate, 'YEAR')+rownum-1
         WHEN to_char
             (trunc
             (sysdate, 'YEAR')+rownum-1, 'D') = '7' THEN trunc
                                                (sysdate, 'YEAR')+rownum-1
         ELSE NULL
      END Wk_Start_End
    , CASE
         WHEN trunc
             (sysdate, 'YEAR')+rownum-1 = TRUNC
             (trunc
             (sysdate, 'YEAR')+rownum-1, 'MONTH') THEN trunc
                                               (sysdate, 'YEAR')+rownum-1
         WHEN trunc
             (sysdate, 'YEAR')+rownum-1 = Add_months
             (TRUNC
             (trunc
             (sysdate, 'YEAR')+rownum-1, 'MONTH'), 1)-1 THEN trunc
                                                    (sysdate, 'YEAR')+rownum-1
      END Month_Start_end
FROM   all_objects
WHERE  trunc
     (sysdate, 'YEAR')+rownum <= Add_months
     (trunc
     (sysdate, 'YEAR'), 12)-1;

Any help is appreciated. Thanks!

Hmmm. You seem to want odd-numbered rows with values, along with the next non-null value. I'm thinking:

select dte as start_date, next_dte and end_date
from (select ao.*, rownum as seqnum, lead(dte) over (order by dates) as next_dte
      from ((select dates, Wk_START_END as dte from all_objects ao) union all
            (select dates, MONTH_START_END as dte from all_objects ao)
           ) t
      where dte is not null
     ) t
where mod(seqnum, 2) = 1;

Here is a query that will do it for all 2016 dates. You can extrapolate it for your purposes, I hope.

The basic approach is to get all the dates in the range of interest (say, all of 2016), then group them by month/week. Then get the min() and max() date in each group.

WITH dtes AS
       (SELECT TO_DATE ('01-JAN-2016') + ROWNUM - 1 dte
        FROM   DUAL
        CONNECT BY ROWNUM <= 365),
     dtes_grouped_by_month_week AS
       (SELECT dte,
               DENSE_RANK () OVER (PARTITION BY NULL ORDER BY TO_CHAR (dte, 'IYYY-MM'), TO_CHAR (dte, 'IW')) month_week
        FROM   dtes)
SELECT MIN (dte) start_date,
       MAX (dte) end_date
FROM   dtes_grouped_by_month_week
GROUP BY month_week
ORDER BY month_week;

UPDATE re: first day of week

Looking at your example, you seem to want Sunday to be the 1st day of the week instead of Monday. That's easily adjusted for in the query -- just add +1 to the date in the TO_CHAR functions in the DENSE_RANK .

The query below starts from scratch - it doesn't use any of your code (or its output). The year and month are hard-coded in the first CTE (subfactored query in the WITH clause at the top); more likely in your application you will exclude the first CTE, named inputs , and you will make y and m into bind variables in the definition of first_date (also in the WITH clause).

I used your convention: the week starts on "day 1 of the week" (which in the US is Sunday) and ends on "day 7 of the week." This can be adjusted through NLS parameters if needed.

with 
     inputs ( y, m ) as (
       select 2016, 1 from dual
     ),
     first_date ( f_dt ) as (
       select to_date(to_char(y, '0009') || '-' || to_char(m, '09'), 'yyyy-mm') 
       from   inputs
     ),
     mth_dates ( dt ) as (
       select f_dt + level - 1 from first_date
       connect by level <= last_day(f_dt) - f_dt + 1
     ),
     start_dates ( dt, rn ) as (
       select dt, row_number() over (order by dt)
       from   ( select dt from mth_dates where to_char(dt, 'd') = '1'
                union
                select min(dt) from mth_dates )
     ),
     end_dates ( dt, rn ) as (
       select dt, row_number() over (order by dt)
       from   ( select dt from mth_dates where to_char(dt, 'd') = '7'
                union
                select max(dt) from mth_dates )
     )
select s.rn as week_nbr, s.dt as start_date, e.dt as end_date
from   start_dates s inner join end_dates e   on s.rn = e.rn;

  WEEK_NBR START_DATE  END_DATE
---------- ---------- ----------
         1 2016-01-01 2016-01-02
         2 2016-01-03 2016-01-09
         3 2016-01-10 2016-01-16
         4 2016-01-17 2016-01-23
         5 2016-01-24 2016-01-30
         6 2016-01-31 2016-01-31

ADDED at OP's request:

To generate the start and end dates for the entire year one can use the query below.

with 
     inputs ( y ) as (
       select 2016 from dual
     ),
     first_date ( f_dt ) as (
       select to_date(to_char(y, '0009') || '-01-01', 'yyyy-mm-dd') 
       from   inputs
     ),
     year_dates ( dt ) as (
       select f_dt + level - 1 from first_date
       connect by level <= add_months(f_dt, 12) - f_dt
     ),
     start_dates ( dt, rn ) as (
       select dt, row_number() over (order by dt)
       from   ( select dt from year_dates where to_char(dt, 'd') = '1'
                                             or extract(day from dt) = 1 )
     ),
     end_dates ( dt, rn ) as (
       select dt, row_number() over (order by dt)
       from   ( select dt from year_dates where to_char(dt, 'd') = '7'
                                             or extract(day from dt + 1) = 1 )
     )
select s.dt as start_date, e.dt as end_date
from   start_dates s inner join end_dates e   on s.rn = e.rn;

Further comment : I actually like Matthew's answer better than mine. His solution simply groups the days into the proper "set intersections" of months and weeks and uses max() and min() over those groups, avoiding the need for a join. It's a better solution than mine.

For completeness, I reproduce Matthew's solution below, with a few minor changes.

  • First, to match the requirement (and as Matthew suggested), I add 1 to dte in forming the groups by week, so weeks begin on Sundays and end on Saturdays.

  • Second, as I suggested in a comment to Matthew's Answer, I use "month" and "week" directly to form the groups; there is no need for dense_rank() .

  • Third, to conform with good coding practices, I added an explicit date format model to to_date() in the first CTE.

Credit : @Matthew McPeak

with dtes ( dte ) as (
         select to_date ('01-Jan-2016', 'dd-Mon-yyyy') + rownum - 1
         from   dual
         connect by rownum <= 366   -- 2016 is a leap year
     ),
     dtes_grouped_by_month_week ( dte, mth, wk ) as (
         select dte, to_char(dte, 'mm'), to_char(dte+1, 'iw')
         from   dtes
     )
select   min(dte) start_date, max(dte) end_date
from     dtes_grouped_by_month_week
group by mth, wk
order by start_date;

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM