How to convert column into rows in oracle 10g - sql

Suppose I have the result of an Oracle sql query:
Month Date
----- -----
Jan 10
Jan 15
Jan 20
Feb 11
Feb 16
Feb 25
I want to display this data in the following format:
Jan Jan Jan Feb Feb Feb
10 15 20 11 16 25
How to write the query?

Using PIVOT:
SQL> WITH sample_data AS(
2 SELECT 'Jan' mnth, 10 dt FROM dual UNION ALL
3 SELECT 'Jan' mnth, 15 dt FROM dual UNION ALL
4 SELECT 'Jan' mnth, 20 dt FROM dual UNION ALL
5 SELECT 'Feb' mnth, 11 dt FROM dual UNION ALL
6 SELECT 'Feb' mnth, 16 dt FROM dual UNION ALL
7 SELECT 'Feb' mnth, 25 dt FROM dual
8 )
9 -- end of smaple_data mimicking real table
10 SELECT *
11 FROM
12 (SELECT dt, row_number() OVER(ORDER BY NULL) rn FROM sample_data
13 ) PIVOT (MAX(dt) FOR (rn)
14 IN (1 AS Jan_1, 2 AS jan_2, 3 AS Jan_3, 4 AS Feb_1, 5 Feb_2, 6 Feb_3));
JAN_1 JAN_2 JAN_3 FEB_1 FEB_2 FEB_3
---------- ---------- ---------- ---------- ---------- ----------
10 15 20 11 16 25
Under the hood PIVOT is same MAX + CASE. You can check it in 12c where Oracle added EXPAND_SQL_TEXT procedure to DBMS_UTILITY package.
SQL> VARIABLE c CLOB
SQL> BEGIN
2 dbms_utility.expand_sql_text(Q'[WITH sample_data AS(
3 SELECT 'Jan' mnth, 10 dt FROM dual UNION ALL
4 SELECT 'Jan' mnth, 15 dt FROM dual UNION ALL
5 SELECT 'Jan' mnth, 20 dt FROM dual UNION ALL
6 SELECT 'Feb' mnth, 11 dt FROM dual UNION ALL
7 SELECT 'Feb' mnth, 16 dt FROM dual UNION ALL
8 SELECT 'Feb' mnth, 25 dt FROM dual
9 )
10 -- end of smaple_data mimicking real table
11 SELECT *
12 FROM
13 (SELECT dt, row_number() OVER(ORDER BY NULL) rn FROM sample_data
14 ) PIVOT (MAX(dt) FOR (rn)
15 IN (1 AS Jan_1, 2 AS jan_2, 3 AS Jan_3, 4 AS Feb_1, 5 Feb_2, 6 Feb_3))]',:c);
16 END;
17 /
PL/SQL procedure successfully completed.
Now let's see what Oracle actually does internally:
SQL> set long 100000
SQL> print c
C
--------------------------------------------------------------------------------
SELECT "A1"."JAN_1" "JAN_1",
"A1"."JAN_2" "JAN_2",
"A1"."JAN_3" "JAN_3",
"A1"."FEB_1" "FEB_1",
"A1"."FEB_2" "FEB_2",
"A1"."FEB_3" "FEB_3"
FROM
(SELECT MAX(
CASE WHE N ("A2"."RN"=1)
THEN "A2"."DT"
END ) "JAN_1",
MAX(
CASE
WHEN ("A2"."RN"=2)
THEN " A2"."DT"
END ) "JAN_2",
MAX(
CASE
WHEN ("A2"."RN"=3)
THEN "A2"."DT"
END ) "JAN_3" ,
MAX(
CASE
WHEN ("A2"."RN"=4)
THEN "A2"."DT"
END ) "FEB_1",
MAX(
CASE
WHEN ("A2". "RN"=5)
THEN "A2"."DT"
END ) "FEB_2",
MAX(
CASE
WHEN ("A2"."RN"=6)
THEN "A2"."DT"
END ) "FEB_3"
FROM
(SELECT "A3"."DT" "DT",
ROW_NUMBER() OVER ( ORDER BY NULL) " RN"
FROM (
(SELECT 'Jan' "MNTH",10 "DT" FROM "SYS"."DUAL" "A10"
)
UNION ALL (SE LECT 'Jan' "MNTH",15 "DT" FROM "SYS"."DUAL" "A9")
UNION ALL
(SELECT 'Jan' "MNTH",20 "DT" FROM "SYS"."DUAL" "A8"
)
UNION ALL
(SELECT 'Feb' "MNTH",11 "DT" FROM " SYS"."DUAL" "A7"
)
UNION ALL
(SELECT 'Feb' "MNTH",16 "DT" FROM "SYS"."DUAL" "A6"
)
UNION ALL
(SELECT 'Feb' "MNTH",25 "DT" FROM "SYS"."DUAL" "A5"
)) "A3"
) "A2"
) " A1"

WITH dates( month, day ) AS (
SELECT 'Jan', 10 FROM DUAL UNION ALL
SELECT 'Jan', 15 FROM DUAL UNION ALL
SELECT 'Jan', 20 FROM DUAL UNION ALL
SELECT 'Feb', 11 FROM DUAL UNION ALL
SELECT 'Feb', 16 FROM DUAL UNION ALL
SELECT 'Feb', 25 FROM DUAL
),
ordered_dates( month, day, seq_no ) AS (
SELECT month,
day,
ROW_NUMBER() OVER ( PARTITION BY month ORDER BY day )
FROM dates
)
SELECT MAX( CASE WHEN month = 'Jan' AND seq_no = 1 THEN day END ) AS "Jan",
MAX( CASE WHEN month = 'Jan' AND seq_no = 2 THEN day END ) AS "Jan",
MAX( CASE WHEN month = 'Jan' AND seq_no = 3 THEN day END ) AS "Jan",
MAX( CASE WHEN month = 'Feb' AND seq_no = 1 THEN day END ) AS "Feb",
MAX( CASE WHEN month = 'Feb' AND seq_no = 2 THEN day END ) AS "Feb",
MAX( CASE WHEN month = 'Feb' AND seq_no = 3 THEN day END ) AS "Feb"
FROM ordered_dates;
Output:
Jan Jan Jan Feb Feb Feb
---------- ---------- ---------- ---------- ---------- ----------
10 15 20 11 16 25

Related

How to separate range of year on oracle

I am working on a db oracle and I need to create a query where it return a range of date. For example:
Supose that I had a field of like this:
I need to get this dates and apply a range of years to return someting like:
|'0-5'|'6-10'|'11-15'|...
| 10 | 35 | 20 |...
where each range contains a number of people in this range of years old.
I tried to use SELECT CASE...
SELECT CASE
WHEN DATE_BORN <= DATE_BORN + 5 THEN '0 - 5
WHEN DATE_BORN >= DATE_BORN + 6 AND DATE_BORN <= 10 THEN '6 - 10'
END AS AGE_RANGE,
COUNT(*)
FROM MY_TABLE
GROUP BY 1
So I saw that this way change only days not year.
How can I write this query?
That's conditional aggregation:
SQL> with test (date_born) as
2 (select date '2000-05-12' from dual union all
3 select date '2001-05-12' from dual union all
4 select date '2012-05-12' from dual union all
5 select date '2013-05-12' from dual union all
6 select date '2004-05-12' from dual union all
7 select date '2008-05-12' from dual union all
8 select date '2009-05-12' from dual union all
9 select date '2001-05-12' from dual union all
10 select date '2012-05-12' from dual union all
11 select date '2001-05-12' from dual union all
12 select date '2004-05-12' from dual union all
13 select date '2005-05-12' from dual
14 )
15 select
16 sum(case when extract (year from date_born) between 2000 and 2005 then 1 else 0 end) as "2000 - 2005",
17 sum(case when extract (year from date_born) between 2006 and 2010 then 1 else 0 end) as "2006 - 2010",
18 sum(case when extract (year from date_born) between 2011 and 2015 then 1 else 0 end) as "2011 - 2015"
19 from test;
2000 - 2005 2006 - 2010 2011 - 2015
----------- ----------- -----------
7 2 3
SQL>
Here is a dynamic way to do this (using the sample table above)
First I think it's easier to have your ranges in rows rather than columns, easier for having a variety of dates that may change.
Second your first grouping is 6 years, so I changed it to just be series of 5 years:
with test (date_born) as
(select date '2000-05-12' from dual union all
select date '2001-05-12' from dual union all
select date '2012-05-12' from dual union all
select date '2013-05-12' from dual union all
select date '2004-05-12' from dual union all
select date '2008-05-12' from dual union all
select date '2009-05-12' from dual union all
select date '2001-05-12' from dual union all
select date '2012-05-12' from dual union all
select date '2001-05-12' from dual union all
select date '2004-05-12' from dual union all
select date '2005-05-12' from dual
)
,mydata AS (
SELECT
(SELECT min(extract(YEAR FROM date_born)) FROM test)+((LEVEL-1)*5)dt1
,(SELECT min(extract(YEAR FROM date_born)) FROM test)+((LEVEL-1)*5)+4 dt2
FROM dual CONNECT BY LEVEL*5 <=
(SELECT max(extract(YEAR FROM date_born))-min(extract(YEAR FROM date_born)) FROM test)+5)
SELECT d.*, count(t.date_born) cnt FROM mydata d
LEFT JOIN test t ON extract(YEAR FROM date_born) BETWEEN d.dt1 AND d.dt2
GROUP BY dt1, dt2
ORDER BY dt1;
You get this for your solution
DT1 DT2 CNT
2000 2004 6
2005 2009 3
2010 2014 3
Solution is basically extracting years from dates, finding min/max of this data set, using connect to get all years in between, and then joining to count your matching records

find gap between months in two consecutive year oracle sql

Need to find record having gap between months in a table if the data is present in two different year.
I have column like id, value,month, year.
Id, value, month,year
1, 123, oct, 2020
1, 128, nov, 2020
1, 127, jan ,2021
2, 121, Dec, 2020
2, 154, jan, 2021
Output I need:
Id 1 as there is a gap in month (Dec is Missing for id=1)
Here's one option. Read comments within code.
SQL> with test (id, value, month, year) as
2 -- sample data; you have that, don't type it
3 (select 1, 123, 'oct', 2020 from dual union all
4 select 1, 128, 'nov', 2020 from dual union all
5 select 1, 127, 'jan', 2021 from dual union all
6 select 2, 121, 'dec', 2020 from dual union all
7 select 2, 154, 'jan', 2021 from dual
8 ),
9 temp as
10 -- "convert" month and year to real date value
11 (select id,
12 value,
13 to_date(month ||' '|| year, 'mon yyyy', 'nls_date_language=english') datum
14 from test
15 ),
16 temp2 as
17 -- select difference in months between DATUM and next month (LEAD!)
18 (select id,
19 months_between
20 (datum,
21 to_date(month ||' '|| year, 'mon yyyy', 'nls_date_language=english') datum
22 ) diff
23 from temp
24 )
25 select distinct id
26 from temp2
27 where abs(diff) > 1;
ID
----------
1
SQL>
It can probably be compressed, but step-by-step CTEs show what's going on.
I would construct a date and use lag():
select t.*
from (select t.*,
lag(dte) over (partition by id order by dte) as prev_dte
from (select t.*,
to_date(year || '-' || month || '-01', 'YYYY-MON-DD') as dte
from t
) t
) t
where prev_dte <> dte - interval '1' month;
Here is a db<>fiddle.
Here is an example using the LAG function and finding rows where where the prior month is not one month behind (or non existent)
WITH
sample_data (Id,
VALUE,
month,
year)
AS
(SELECT 1, 123, 'oct', 2020 FROM DUAL
UNION ALL
SELECT 1, 128, 'nov', 2020 FROM DUAL
UNION ALL
SELECT 1, 127, 'jan', 2021 FROM DUAL
UNION ALL
SELECT 2, 121, 'Dec', 2020 FROM DUAL
UNION ALL
SELECT 2, 154, 'jan', 2021 FROM DUAL)
SELECT DISTINCT id
FROM (SELECT sd.id,
CASE
WHEN ADD_MONTHS (TO_DATE (sd.year || sd.month, 'YYYYMON'), -1) =
TO_DATE (
LAG (sd.year || sd.month)
OVER (
PARTITION BY id
ORDER BY
sd.year, EXTRACT (MONTH FROM TO_DATE (sd.month, 'MON'))),
'YYYYMON')
OR LAG (sd.id)
OVER (
PARTITION BY id
ORDER BY sd.year, EXTRACT (MONTH FROM TO_DATE (sd.month, 'MON')))
IS NULL
THEN
'Y'
ELSE
'N'
END AS valid_prev_month
FROM sample_data sd)
WHERE valid_prev_month = 'N';

SQL: Create multiple rows for a record based on months between two dates

My table has records as below for different Id's and different start and end dates
ID, Startdate, Enddate
1, 2017-02-14, 2018-11-05
I want to write an SQL without using date dimension table that gives below output: Basically one record for each month between start and end date.
1, 2017, 02
1, 2017, 03
1, 2017, 04
1, 2017, 05
1, 2017, 06
1, 2017, 07
1, 2017, 08
1, 2017, 09
1, 2017, 10
1, 2017, 11
1, 2017, 12
1, 2018, 01
1, 2018, 02
1, 2018, 03
1, 2018, 04
1, 2018, 05
1, 2018, 06
1, 2018, 07
1, 2018, 09
1, 2018, 10
1, 2018, 11
Please use below query example:
set #start_date = '2017-02-14';
set #end_date = LAST_DAY('2018-11-05');
WITH RECURSIVE date_range AS
(
select MONTH(#start_date) as month_, YEAR(#start_date) as year_, DATE_ADD(#start_date, INTERVAL 1 MONTH) as next_month_date
UNION
SELECT MONTH(dr.next_month_date) as month_, YEAR(dr.next_month_date) as year_, DATE_ADD(dr.next_month_date, INTERVAL 1 MONTH) as next_month_date
FROM date_range dr
where next_month_date <= #end_date
)
select month_, year_ from date_range
order by next_month_date desc
This is what I did and it worked like a charm:
-- sample data
WITH table_data
AS (
SELECT 1 AS id
,cast('2017-08-14' AS DATE) AS start_dt
,cast('2018-12-16' AS DATE) AS end_dt
UNION ALL
SELECT 2 AS id
,cast('2017-09-14' AS DATE) AS start_dt
,cast('2019-01-16' AS DATE) AS end_dt
)
-- find minimum date from the data
,starting_date (start_date)
AS (
SELECT min(start_dt)
FROM TABLE_DATA
)
--get all months between min and max dates
,all_dates
AS (
SELECT last_day(add_months(date_trunc('month', start_date), idx * 1)) month_date
FROM starting_date
CROSS JOIN _v_vector_idx
WHERE month_date <= add_months(start_date, abs(months_between((
SELECT min(start_dt) FROM TABLE_DATA), (SELECT max(end_dt) FROM TABLE_DATA))) + 1)
ORDER BY month_date
)
SELECT id
,extract(year FROM month_date)
,extract(month FROM month_date)
,td.start_dt
,td.end_dt
FROM table_data td
INNER JOIN all_dates ad
ON ad.month_date > td.start_dt
AND ad.month_date <= last_day(td.end_dt)
ORDER BY 1
,2
You have to generate date and from that have to pick year and month
select distinct year(date),month( date) from
(select * from (
select
date_add('2017-02-14 00:00:00.000', INTERVAL n5.num*10000+n4.num*1000+n3.num*100+n2.num*10+n1.num DAY ) as date
from
(select 0 as num
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9) n1,
(select 0 as num
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9) n2,
(select 0 as num
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9) n3,
(select 0 as num
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9) n4,
(select 0 as num
union all select 1
union all select 2
union all select 3
union all select 4
union all select 5
union all select 6
union all select 7
union all select 8
union all select 9) n5
) a
where date >'2017-02-14 00:00:00.000' and date < '2018-11-05'
) as t

How to get data for previous 7 days based on set of dates in groups in sql

I was going through this forum for my query to get data for previous 7 days,but most of them give it for current date.Below is my requirement:
I have a Table 1 as below:
These are start dates of week which is monday
from_date
2016-01-04
2016-01-11
2016-01-18
Table 2
I have all days of week here starting from monday.Ex: jan 04 - monday to jan 10 - sunday and so on for other weeks also.
get_date flag value
2016-01-04 N 4
2016-01-05 N 9
2016-01-06 Y 2
2016-01-07 Y 13
2016-01-08 Y 7
2016-01-09 Y 8
2016-01-10 Y 8
2016-01-11 Y 1
2016-01-12 Y 9
2016-01-13 N 8
2016-01-14 N 24
2016-01-15 N 8
2016-01-16 Y 4
2016-01-17 Y 5
2016-01-18 Y 9
2016-01-19 Y 2
2016-01-20 Y 8
2016-01-21 Y 4
2016-01-22 N 9
2016-01-23 N 87
2016-01-24 Y 3
Expected Result
here wk is the unique number for each start-end dates respectively
avg value is the avg of the values for the dates in that week.
last 2 days of the week are weekend days.
say 2016-01-09 and 2016-01-10 are weekends
from_date get_date Wk Total_days Total_weekdays_flag_Y Total_weekenddays_flag_Y Avg_value
2016-01-04 2016-01-10 1 7 3 2 6.714285714
2016-01-11 2016-01-17 2 7 2 2 8.428571429
2016-01-18 2016-01-24 3 7 4 1 17.42857143
Could anyone help me with this as I am not good at sql.
Thanks
select
from_date
, Wk
, count(case when day_of_week <=5 and flag = 'Y' then 1 end) as Total_weekdays_flag_Y
, count(case when day_of_week > 5 and flag = 'Y' then 1 end) as Total_weekenddays_flag_Y
, avg(value) as Avg_value
from (
select trunc(get_date,'IW') as from_date
, (trunc(get_date,'IW')- trunc(date'2016-01-04','IW'))/7 + 1 as Wk
, flag
, value
, get_date - trunc(get_date,'IW') as day_of_week
from Table_2)
group by from_date, Wk
order by from_date, Wk;
EDIT:
/*generate some test_data for table 2*/
with table_2 (get_date, flag, value) as (
select date'2016-01-03' + level,
DECODE(mod(level,3),0,'Y','N'),
round(dbms_random.value(0,10))
from dual connect by level < 101
),
/*generate some test_weeks for table 1*/
table_1 (FROM_date) as (select date'2016-01-04' + (level-1)*7 from dual connect by level < 101 )
/*main query */
select
from_date
, Wk
, count(day_of_week) as total
, count(case when day_of_week <=5 and flag = 'Y' then 1 end) as Total_weekdays_flag_Y
, count(case when day_of_week > 5 and flag = 'Y' then 1 end) as Total_weekenddays_flag_Y
, avg(value) as Avg_value
from (
select last_value(from_date ignore nulls) over (order by get_date) as from_date
,last_value(Wk ignore nulls) over (order by get_date) as Wk
, flag
, value
, get_date - trunc(get_date,'IW') as day_of_week
from Table_2 t2
full join (select row_number() over (order by from_date) as wk,from_date from table_1) t1 on t2.get_date = t1.from_date
)
group by from_date, Wk
having count(day_of_week) > 0
order by from_date, Wk
In the query below, I create the test data right within the query; in final form, you would delete the subqueries table_1 and table_2 and use the rest.
The syntax will work from Oracle 11.2 on. In Oracle 11.1, you need to move the column names in factored subqueries to the select... from dual part. Or, since you really only have one subquery (prep) and an outer query, you can write prep as an actual, in-line subquery.
Your arithmetic seems off on the average for the first week.
In your sample output you use get_date for the last day of the week. That is odd, since in table_2 that name has a different meaning. I used to_date in my output. I also do not show total_days - that is always 7, so why include it at all? (If it is not always 7, then there is something you didn't tell us; anyway, a count(...), if that is what it should be, is easy to add).
with
-- begin test data, can be removed in final solution
table_1 ( from_date ) as (
select date '2016-01-04' from dual union all
select date '2016-01-11' from dual union all
select date '2016-01-18' from dual
)
,
table_2 ( get_date, flag, value ) as (
select date '2016-01-04', 'N', 4 from dual union all
select date '2016-01-05', 'N', 9 from dual union all
select date '2016-01-06', 'Y', 2 from dual union all
select date '2016-01-07', 'Y', 13 from dual union all
select date '2016-01-08', 'Y', 7 from dual union all
select date '2016-01-09', 'Y', 8 from dual union all
select date '2016-01-10', 'Y', 8 from dual union all
select date '2016-01-11', 'Y', 1 from dual union all
select date '2016-01-12', 'Y', 9 from dual union all
select date '2016-01-13', 'N', 8 from dual union all
select date '2016-01-14', 'N', 24 from dual union all
select date '2016-01-15', 'N', 8 from dual union all
select date '2016-01-16', 'Y', 4 from dual union all
select date '2016-01-17', 'Y', 5 from dual union all
select date '2016-01-18', 'Y', 9 from dual union all
select date '2016-01-19', 'Y', 2 from dual union all
select date '2016-01-20', 'Y', 8 from dual union all
select date '2016-01-21', 'Y', 4 from dual union all
select date '2016-01-22', 'N', 9 from dual union all
select date '2016-01-23', 'N', 87 from dual union all
select date '2016-01-24', 'Y', 3 from dual
),
-- end test data, continue actual query
prep ( get_date, flag, value, from_date, wd_flag ) as (
select t2.get_date, t2.flag, t2.value, t1.from_date,
case when t2.get_date - t1.from_date <= 4 then 'wd' else 'we' end
from table_1 t1 inner join table_2 t2
on t2.get_date between t1.from_date and t1.from_date + 6
)
select from_date,
from_date + 6 as to_date,
row_number() over (order by from_date) as wk,
count(case when flag = 'Y' and wd_flag = 'wd' then 1 end)
as total_weekday_Y,
count(case when flag = 'Y' and wd_flag = 'we' then 1 end)
as total_weekend_Y,
round(avg(value), 6) as avg_value
from prep
group by from_date;
Output:
FROM_DATE TO_DATE WK TOTAL_WEEKDAY_Y TOTAL_WEEKEND_Y AVG_VALUE
---------- ---------- ---- --------------- --------------- ----------
2016-01-04 2016-01-10 1 3 2 7.285714
2016-01-11 2016-01-17 2 2 2 8.428571
2016-01-18 2016-01-24 3 4 1 17.428571

SQL - Get Min, Max date for a given group with break in dates

I'm trying to find min and max process date for following data for a given value with break in process date (note that rows are not processed on weekends, i don't want to break them into two different sets if they have same value)
SELECT 1, 'A',to_date('10/01/2012','dd/mm/yyyy'), 10, to_date('11/01/2012','dd/mm/yyyy') FROm DUAL
UNION ALL SELECT 1, 'A',to_date('11/01/2012','dd/mm/yyyy'), 10, to_date('12/01/2012','dd/mm/yyyy') FROm DUAL
UNION ALL SELECT 1, 'A',to_date('12/01/2012','dd/mm/yyyy'), 9, to_date('13/01/2012','dd/mm/yyyy') FROm DUAL
UNION ALL SELECT 1, 'A',to_date('13/01/2012','dd/mm/yyyy'), 9, to_date('14/01/2012','dd/mm/yyyy') FROm DUAL
UNION ALL SELECT 1, 'A',to_date('16/01/2012','dd/mm/yyyy'), 9, to_date('17/01/2012','dd/mm/yyyy') FROm DUAL
UNION ALL SELECT 1, 'A',to_date('17/01/2012','dd/mm/yyyy'), 10, to_date('18/01/2012','dd/mm/yyyy') FROm DUAL
UNION ALL SELECT 1, 'A',to_date('18/01/2012','dd/mm/yyyy'), 10, to_date('19/01/2012','dd/mm/yyyy') FROm DUAL;
My attempt (which i know is wrong)
SELECT id, cd, value, min(p_dt) min_dt, max(p_dt) max_dt FROM T
group by id, cd, value;
This returns
ID CD VALUE MIN_DT MAX_DT
----------------------------------------------------------------------------------
1 A 9 January, 12 2012 00:00:00+0000 January, 16 2012 00:00:00+0000
1 A 10 January, 10 2012 00:00:00+0000 January, 18 2012 00:00:00+0000
What i want to return is
ID CD VALUE MIN_DT MAX_DT
----------------------------------------------------------------------------------
1 A 9 January, 12 2012 00:00:00+0000 January, 16 2012 00:00:00+0000
1 A 10 January, 10 2012 00:00:00+0000 January, 11 2012 00:00:00+0000
1 A 10 January, 17 2012 00:00:00+0000 January, 18 2012 00:00:00+0000
I tried different ways to query this but i couldn't come with a working query.
SQL FIDDLE
Not sure what you want... You do not have correct data to partition by dates. Your dates are unique, unless you meant that your i_dt must be equal p_dt. Even if you partition by dates instead of values you will get all rows in return as in simple select.
In my example I partition by value. There could be only one max and one min date within unique value. Examine the output:
SELECT id, cd, i_dt, p_dt, value
, To_Char(MIN(p_dt) OVER (PARTITION BY value), 'Mon, DD YYYY HH24:MI:SS') min_dt
, To_Char(MAX(p_dt) OVER (PARTITION BY value), 'Mon, DD YYYY HH24:MI:SS') max_dt
FROM t
/
ID CD I_DT P_DT VALUE MIN_DT MAX_DT
---------------------------------------------------------------------------------------
1 A 1/14/2012 1/13/2012 9 Jan, 12 2012 00:00:00 Jan, 16 2012 00:00:00
1 A 1/17/2012 1/16/2012 9 Jan, 12 2012 00:00:00 Jan, 16 2012 00:00:00
1 A 1/13/2012 1/12/2012 9 Jan, 12 2012 00:00:00 Jan, 16 2012 00:00:00
1 A 1/19/2012 1/18/2012 10 Jan, 10 2012 00:00:00 Jan, 18 2012 00:00:00
1 A 1/18/2012 1/17/2012 10 Jan, 10 2012 00:00:00 Jan, 18 2012 00:00:00
1 A 1/12/2012 1/11/2012 10 Jan, 10 2012 00:00:00 Jan, 18 2012 00:00:00
1 A 1/11/2012 1/10/2012 10 Jan, 10 2012 00:00:00 Jan, 18 2012 00:00:00
The are a number of other questions on this site looking to solve the same problem. Examples are here and here, and those are just questions that I have provided answers for.
This question is a little more complicated because of the requirement to ignore weekends. The seems to be relatively simple to solve as I will explain soon.
You question doesn't include column names for all of the columns within your table. I have assumed that the first date is the process date and the other date is not important for this query. This might be the wrong assumption.
From the question, it looks like a group will exist if, for a weekday (Mon-Thurs), there is a matching row on the next day. For a Friday, there needs to be a matching row on the following Monday. I handle this by adding 3 days if it is a Friday or one day in every other case.
An example query is shown below and a SQLFiddle is also available.
Hopefully this solves your problem.
with test_data as (
SELECT 1 as id, 'A' as cd,to_date('10/01/2012','dd/mm/yyyy') as p_date, 10 as value, to_date('11/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL UNION ALL
SELECT 1 as id, 'A' as cd,to_date('11/01/2012','dd/mm/yyyy') as p_date, 10 as value, to_date('12/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL UNION ALL
SELECT 1 as id, 'A' as cd,to_date('12/01/2012','dd/mm/yyyy') as p_date, 9 as value, to_date('13/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL UNION ALL
SELECT 1 as id, 'A' as cd,to_date('13/01/2012','dd/mm/yyyy') as p_date, 9 as value, to_date('14/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL UNION ALL
SELECT 1 as id, 'A' as cd,to_date('16/01/2012','dd/mm/yyyy') as p_date, 9 as value, to_date('17/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL UNION ALL
SELECT 1 as id, 'A' as cd,to_date('17/01/2012','dd/mm/yyyy') as p_date, 10 as value, to_date('18/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL UNION ALL
SELECT 1 as id, 'A' as cd,to_date('18/01/2012','dd/mm/yyyy') as p_date, 10 as value, to_date('19/01/2012','dd/mm/yyyy') as some_other_date FROm DUAL
)
select
id,
cd,
value,
block_num,
min(p_date) as process_start_date,
max(p_date) as process_end_date
from (
select
id,
cd,
value,
p_date,
sum(is_block_start) over (partition by id, cd, value order by p_date) as block_num
from (
select
id,
cd,
value,
p_date,
-- get end date of previous block
case when lag(case when to_char(p_date, 'DY') = 'FRI' then p_date+3 else p_date+1 end)
over (partition by id, cd, value order by p_date) = p_date then 0 else 1 end as is_block_start
from test_data
-- Make sure that the data definitely doesn't include Sat or Sun because this could just confuse things
where to_char(p_date, 'DY') not in ('SAT', 'SUN')
)
)
group by id, cd, value, block_num
order by id, cd, value, block_num
Here is the answer using analytic functions.
With your sample data...
WITH
tbl (ID, CD, P_DATE, A_VALUE, I_DATE) AS
(
SELECT 1, 'A',to_date('10/01/2012','dd/mm/yyyy'), 10, to_date('11/01/2012','dd/mm/yyyy') FROm DUAL UNION ALL
SELECT 1, 'A',to_date('11/01/2012','dd/mm/yyyy'), 10, to_date('12/01/2012','dd/mm/yyyy') FROm DUAL UNION ALL
SELECT 1, 'A',to_date('12/01/2012','dd/mm/yyyy'), 9, to_date('13/01/2012','dd/mm/yyyy') FROm DUAL UNION ALL
SELECT 1, 'A',to_date('13/01/2012','dd/mm/yyyy'), 9, to_date('14/01/2012','dd/mm/yyyy') FROm DUAL UNION ALL
SELECT 1, 'A',to_date('16/01/2012','dd/mm/yyyy'), 9, to_date('17/01/2012','dd/mm/yyyy') FROm DUAL UNION ALL
SELECT 1, 'A',to_date('17/01/2012','dd/mm/yyyy'), 10, to_date('18/01/2012','dd/mm/yyyy') FROm DUAL UNION ALL
SELECT 1, 'A',to_date('18/01/2012','dd/mm/yyyy'), 10, to_date('19/01/2012','dd/mm/yyyy') FROm DUAL
),
... create cte (grid) with columns (PREV_DAY_DIFF and NEXT_DAY_DIFF) to handle continuity (taking care of weekends) and to, later, help grouping the rows based on continuity...
grid AS
( SELECT ID, CD, A_VALUE, P_DATE, To_Char(P_DATE, 'DY') "P_DAY", I_DATE,
--
CASE WHEN To_Char(LAG(P_DATE, 1) OVER(Partition By ID, CD, A_VALUE Order By ID, CD, A_VALUE, P_DATE), 'DY') = 'FRI' THEN 1
WHEN To_Char(LAG(P_DATE, 1) OVER(Partition By ID, CD, A_VALUE Order By ID, CD, A_VALUE, P_DATE), 'DY') Is Null THEN 0
ELSE P_DATE - LAG(P_DATE, 1) OVER(Partition By ID, CD, A_VALUE Order By ID, CD, A_VALUE, P_DATE)
END "PREV_DAY_DIFF",
--
CASE WHEN To_Char(LEAD(P_DATE, 1) OVER(Partition By ID, CD, A_VALUE Order By ID, CD, A_VALUE, P_DATE), 'DY') = 'MON' THEN 1
WHEN To_Char(LEAD(P_DATE, 1) OVER(Partition By ID, CD, A_VALUE Order By ID, CD, A_VALUE, P_DATE), 'DY') Is Null THEN 0
ELSE LEAD(P_DATE, 1) OVER(Partition By ID, CD, A_VALUE Order By ID, CD, A_VALUE, P_DATE) - P_DATE
END "NEXT_DAY_DIFF"
FROM tbl
ORDER BY ID, CD, A_VALUE, P_DATE
)
Main SQL - takes cte data (the inner join query) and joins them with your sample data calculating and selecting distinct groups with min and max dates as asked
SELECT DISTINCT
t.ID, t.CD, t.A_VALUE,
Nvl(g.MIN_P_DATE, LAG(g.MIN_P_DATE) OVER(Partition By t.ID, t.CD, t.A_VALUE Order By t.ID, t.CD, t.A_VALUE, t.P_DATE)) "MIN_P_DATE",
Nvl(g.MAX_P_DATE, LEAD(g.MAX_P_DATE) OVER(Partition By t.ID, t.CD, t.A_VALUE Order By t.ID, t.CD, t.A_VALUE, t.P_DATE)) "MAX_P_DATE"
FROM tbl t
INNER JOIN
( SELECT ID, CD, A_VALUE, NEXT_DAY_DIFF, PREV_DAY_DIFF,
MIN( CASE WHEN (PREV_DAY_DIFF > 1 And NEXT_DAY_DIFF = 1) THEN P_DATE
WHEN (PREV_DAY_DIFF = 0 And NEXT_DAY_DIFF = 1) THEN P_DATE
END ) OVER( Partition By ID, CD, A_VALUE, PREV_DAY_DIFF Order By ID, CD, A_VALUE, P_DATE ) "MIN_P_DATE",
MAX( CASE WHEN (NEXT_DAY_DIFF > 1 And PREV_DAY_DIFF = 1) THEN P_DATE
WHEN (NEXT_DAY_DIFF = 0 And PREV_DAY_DIFF = 1) THEN P_DATE
WHEN (PREV_DAY_DIFF > 1 And NEXT_DAY_DIFF = 1) THEN P_DATE + 1
END ) OVER( Partition By ID, CD, A_VALUE, NEXT_DAY_DIFF Order By ID, CD, A_VALUE, P_DATE ) "MAX_P_DATE"
FROM grid
WHERE NEXT_DAY_DIFF - PREV_DAY_DIFF != 0
) g ON (t.ID = g.ID And t.CD = g.CD And t.A_VALUE = g.A_VALUE And t.P_DATE = g.MIN_P_DATE OR t.P_DATE = g.MAX_P_DATE)
ORDER BY t.ID, t.CD, t.A_VALUE
This results as:
ID
CD
A_VALUE
MIN_P_DATE
MAX_P_DATE
1
A
9
12-JAN-12
16-JAN-12
1
A
10
10-JAN-12
11-JAN-12
1
A
10
17-JAN-12
18-JAN-12