SQL query to get the first date, depending on the current group (~control break) - sql

I'm trying to create a sql query which returns the first date of the current group.
Let's assume (just as an example) that on the first of every month a row is saved in a table (EmployeeInfo) with the employee ID and the current department.
EmployeeID, Department, Date (Format: DD.MM.YYYY)
100, IT, 01.07.2014
100, IT, 01.08.2014
100, IT, 01.09.2014
100, HR, 01.10.2014
100, HR, 01.11.2014
100, CC, 01.12.2014
100, IT, 01.01.2015
100, IT, 01.02.2015
100, IT, 01.03.2015
100, IT, 01.04.2015
The query should return the date since an employee is working in the current department.
The current department of the employee with ID 100 is IT, therefore the value should be 01.01.2015 (not 01.07.2014).
Any ideas how this could be implemented?

UPDATE New answer based on OP's comments.
You could use ANALYTIC function ROW_NUMBER and LAG.Something like, start of group method:
SQL> WITH DATA AS(
2 SELECT 100 EmployeeID, 'IT' Department, to_date('01.07.2014','DD.MM.YYYY') dt FROM dual UNION ALL
3 SELECT 100, 'IT', to_date('01.08.2014','DD.MM.YYYY') dt from dual union all
4 select 100, 'IT', to_date('01.09.2014','DD.MM.YYYY') dt from dual union all
5 SELECT 100, 'HR', to_date('01.10.2014','DD.MM.YYYY') dt from dual union all
6 select 100, 'HR', to_date('01.11.2014','DD.MM.YYYY') dt from dual union all
7 SELECT 100, 'CC', to_date('01.12.2014','DD.MM.YYYY') dt from dual union all
8 select 100, 'IT', to_date('01.01.2015','DD.MM.YYYY') dt from dual union all
9 select 100, 'IT', to_date('01.02.2015','DD.MM.YYYY') dt from dual
10 )
11 SELECT EmployeeID,
12 Department,
13 DT
14 FROM
15 (SELECT *
16 FROM
17 (SELECT t.*,
18 CASE
19 WHEN Department = lag(Department) over (PARTITION BY EmployeeID ORDER BY dt)
20 THEN 0
21 ELSE 1
22 END gap
23 FROM DATA t
24 ) T
25 WHERE GAP = 1
26 ORDER BY DT DESC
27 )
28 WHERE ROWNUM = 1
29 /
EMPLOYEEID DE DT
---------- -- ---------
100 IT 01-JAN-15
SQL>
OLD answer
For example,
SQL> WITH DATA AS(
2 SELECT 100 EmployeeID, 'IT' Department, to_date('01.07.2014','DD.MM.YYYY') dt FROM dual UNION ALL
3 SELECT 100, 'IT', to_date('01.08.2014','DD.MM.YYYY') dt from dual union all
4 select 100, 'IT', to_date('01.09.2014','DD.MM.YYYY') dt from dual union all
5 SELECT 100, 'HR', to_date('01.10.2014','DD.MM.YYYY') dt from dual union all
6 select 100, 'HR', to_date('01.11.2014','DD.MM.YYYY') dt from dual union all
7 SELECT 100, 'CC', to_date('01.12.2014','DD.MM.YYYY') dt from dual union all
8 select 100, 'IT', to_date('01.01.2015','DD.MM.YYYY') dt from dual union all
9 select 100, 'IT', to_date('01.02.2015','DD.MM.YYYY') dt from dual
10 )
11 SELECT*
12 FROM
13 (SELECT t.*,
14 row_number() OVER(PARTITION BY department ORDER BY dt DESC) rn
15 FROM DATA t
16 )
17 WHERE rn = 1
18 /
EMPLOYEEID DE DT RN
---------- -- --------- ----------
100 CC 01-DEC-14 1
100 HR 01-NOV-14 1
100 IT 01-FEB-15 1
SQL>

Use below query and let me know if want this
WITH table_ (EmployeeID, Department, Dat) AS (
SELECT 100, 'IT', TO_DATE('01.07.2014', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'IT', TO_DATE('01.08.2014', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'IT', TO_DATE('01.09.2014', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'HR', TO_DATE('01.10.2014', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'HR', TO_DATE('01.11.2014', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'CC', TO_DATE('01.12.2014', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'IT', TO_DATE('01.01.2015', 'DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 100, 'IT', TO_DATE('01.02.2015', 'DD.MM.YYYY') FROM DUAL),
----------
--End of Data preparation
----------
table1 AS (SELECT EmployeeID, Department, Dat,
row_number() OVER (PARTITION BY EmployeeID ORDER BY dat DESC) -
row_number() OVER (PARTITION BY EmployeeID, department ORDER BY dat DESC) rk
FROM TABLE_),
table2 AS (SELECT EmployeeID, Department, Dat, RANK() OVER (PARTITION BY employeeid ORDER BY dat) rnk
from table1
WHERE rk = 0 )
SELECT EmployeeID, Department, Dat
FROM table2
WHERE rnk = 1;
output:
EMPLOYEEID DE DAT
---------- -- ---------
100 IT 01-JAN-15

Related

Generate between two dates in different rows SQL

I am working on generating lines depending on the days (a row by day) between two dates. I would have in the first row the first date and in the second row the second date but it all depends on the ID and the money. I consider it is better to show you with an example:
The origin table:
My target:
Could be possible with a loop?
Thank you very much.
You can use a recursive sub-query factoring clause:
WITH dt_range ( id, dt, next_dt, money ) AS (
SELECT id,
dt,
LEAD(dt) OVER (PARTITION BY id ORDER BY dt),
money
FROM table_name
UNION ALL
SELECT id,
dt + INTERVAL '1' DAY,
next_dt,
money
FROM dt_range
WHERE dt + INTERVAL '1' DAY < next_dt
)
SEARCH DEPTH FIRST BY dt SET dt_order
SELECT id, dt, money FROM dt_range;
Which, for your sample data:
CREATE TABLE table_name ( id, dt, money ) AS
SELECT 500, DATE '2017-02-23', 3500 FROM DUAL UNION ALL
SELECT 500, DATE '2017-02-26', 35000 FROM DUAL UNION ALL
SELECT 500, DATE '2017-02-28', 50000 FROM DUAL UNION ALL
SELECT 200, DATE '2020-05-01', 8888 FROM DUAL UNION ALL
SELECT 200, DATE '2020-05-05', 999 FROM DUAL UNION ALL
SELECT 200, DATE '2020-05-09', 1000 FROM DUAL;
Outputs:
ID
DT
MONEY
500
23-FEB-17
3500
500
24-FEB-17
3500
500
25-FEB-17
3500
500
26-FEB-17
35000
500
27-FEB-17
35000
500
28-FEB-17
50000
200
01-MAY-20
8888
200
02-MAY-20
8888
200
03-MAY-20
8888
200
04-MAY-20
8888
200
05-MAY-20
999
200
06-MAY-20
999
200
07-MAY-20
999
200
08-MAY-20
999
200
09-MAY-20
1000
db<>fiddle here
If you are using Oracle 11g then it has bugs iterating over dates; this can be easily fixed by iterating over a number and then adding it to a date (rather than iterating directly on the date):
WITH dt_range ( id, dt, offset, next_dt, money ) AS (
SELECT id,
dt,
0,
LEAD(dt) OVER (PARTITION BY id ORDER BY dt),
money
FROM table_name
UNION ALL
SELECT id,
dt,
offset + 1,
next_dt,
money
FROM dt_range
WHERE dt + offset + 1 < next_dt
)
SEARCH DEPTH FIRST BY dt SET dt_order
SELECT id, dt + offset AS dt, money FROM dt_range;
db<>fiddle here
for example:
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lead (dates,1,dates) over(partition by id order by dates)-1 lead_dts
from simple_data sd
)
select
st1.id,
st1.dates + to_number(t.column_value)-1 as dates,
st1.money
from step1 st1,table(cast(multiset(select level from dual connect by level<= st1.lead_dts-st1.dates+1) as ora_mining_varchar2_nt)) t
order by id desc,dates;
----OR------
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lag (dates,1,dates) over(partition by id order by dates desc) lag_dts
from simple_data sd
)
select
st1.id,
st1.lag_dts - to_number(t.column_value) as dates,
st1.money
from step1 st1,table(cast(multiset(select st1.lag_dts-st1.dates - level +1 from dual connect by level<= st1.lag_dts-st1.dates) as ora_mining_varchar2_nt)) t
order by id desc,dates;
12c and later:
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lead (dates,1,dates) over(partition by id order by dates)-1 lead_dts
from simple_data sd
)
select
st1.id,
st1.dates + t.lvl-1 as dates,
st1.money
from step1 st1,lateral(select level lvl from dual connect by level<= st1.lead_dts-st1.dates+1) t
order by id desc,dates;
---OR-----
with
simple_data( id,dates,money) as
(
select 500, date '2017-02-23', 3500 from dual union all
select 500, date '2017-02-26', 35000 from dual union all
select 500, date '2017-02-28', 50000 from dual union all
select 200, date '2020-05-01', 8888 from dual union all
select 200, date '2020-05-05', 999 from dual union all
select 200, date '2020-05-09', 1000 from dual
)
,step1 as
(
select
sd.id,
sd.money,
sd.dates,
lag (dates,1,dates) over(partition by id order by dates desc) lag_dts
from simple_data sd
)
select
st1.id,
st1.lag_dts - t.lvl as dates,
st1.money
from step1 st1,lateral(select st1.lag_dts-st1.dates - level +1 lvl from dual connect by level<= st1.lag_dts-st1.dates ) t
order by id desc,dates;

Complex query analyzing historical records

I am using Oracle and trying to retrieve the total number of days a person was out of the office during the year. I have 2 tables involved:
Statuses
1 - Active
2 - Out of the Office
3 - Other
ScheduleHistory
RecordID - primary key
PersonID
PreviousStatusID
NextStatusID
DateChanged
I can easily find when the person went on vacation and when they came back, using
SELECT DateChanged FROM ScheduleHistory WHERE PersonID=111 AND NextStatusID = 2
and
SELECT DateChanged FROM ScheduleHistory WHERE PersonID=111 AND PreviousStatusID = 2
But in case a person went on vacation more than once, how can I can I calculate total number of days a person was out of the office. Is it possible to do programmatically, given only PersonID?
Here is some sample data:
RecordID PersonID PreviousStatusID NextStatusID DateChanged
-----------------------------------------------------------------------------
1 111 1 2 03/11/2020
2 111 2 1 03/13/2020
3 111 1 3 04/01/2020
4 111 3 1 04/07/2020
5 111 1 2 06/03/2020
6 111 2 1 06/05/2020
7 111 1 2 09/14/2020
8 111 2 1 09/17/2020
So from the data above, for the year 2020 for PersonID 111 the query should return 7
Try this:
with aux1 AS (
SELECT
a.*,
to_date(datechanged, 'MM/DD/YYYY') - LAG(to_date(datechanged, 'MM/DD/YYYY')) OVER(
PARTITION BY personid
ORDER BY
recordid
) lag_date
FROM
ScheduleHistory a
)
SELECT
personid,
SUM(lag_date) tot_days_ooo
FROM
aux1
WHERE
previousstatusid = 2
GROUP BY
personid;
If you want total days (or weekdays) for each year (and to account for periods when it goes over the year boundary) then:
WITH date_ranges ( personid, status, start_date, end_date ) AS (
SELECT personid,
nextstatusid,
datechanged,
LEAD(datechanged, 1, datechanged) OVER(
PARTITION BY personid
ORDER BY datechanged
)
FROM table_name
),
split_year_ranges ( personid, year, start_date, end_date, max_date ) AS (
SELECT personid,
TRUNC( start_date, 'YY' ),
start_date,
LEAST(
end_date,
ADD_MONTHS( TRUNC( start_date, 'YY' ), 12 )
),
end_date
FROM date_ranges
WHERE status = 2
UNION ALL
SELECT personid,
end_date,
end_date,
LEAST( max_date, ADD_MONTHS( end_date, 12 ) ),
max_date
FROM split_year_ranges
WHERE end_date < max_date
)
SELECT personid,
EXTRACT( YEAR FROM year) AS year,
SUM( end_date - start_date ) AS total_days,
SUM(
( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( end_date - TRUNC( end_date, 'IW' ), 5 )
- LEAST( start_date - TRUNC( start_date, 'IW' ), 5 )
) AS total_weekdays
FROM split_year_ranges
GROUP BY personid, year
ORDER BY personid, year
Which, for the sample data:
CREATE TABLE table_name ( RecordID, PersonID, PreviousStatusID, NextStatusID, DateChanged ) AS
SELECT 1, 111, 1, 2, DATE '2020-03-11' FROM DUAL UNION ALL
SELECT 2, 111, 2, 1, DATE '2020-03-13' FROM DUAL UNION ALL
SELECT 3, 111, 1, 3, DATE '2020-04-01' FROM DUAL UNION ALL
SELECT 4, 111, 3, 1, DATE '2020-04-07' FROM DUAL UNION ALL
SELECT 5, 111, 1, 2, DATE '2020-06-03' FROM DUAL UNION ALL
SELECT 6, 111, 2, 1, DATE '2020-06-05' FROM DUAL UNION ALL
SELECT 7, 111, 1, 2, DATE '2020-09-14' FROM DUAL UNION ALL
SELECT 8, 111, 2, 1, DATE '2020-09-17' FROM DUAL UNION ALL
SELECT 9, 222, 1, 2, DATE '2019-12-31' FROM DUAL UNION ALL
SELECT 10, 222, 2, 2, DATE '2020-12-01' FROM DUAL UNION ALL
SELECT 11, 222, 2, 2, DATE '2021-01-02' FROM DUAL;
Outputs:
PERSONID
YEAR
TOTAL_DAYS
TOTAL_WEEKDAYS
111
2020
7
7
222
2019
1
1
222
2020
366
262
222
2021
1
1
db<>fiddle here
Provided no vacation crosses a year boundary
with grps as (
SELECT sh.*,
row_number() over (partition by PersonID, NextStatusID order by DateChanged) grp
FROM ScheduleHistory sh
WHERE NextStatusID in (1,2) and 3 not in (NextStatusID, PreviousStatusID)
), durations as (
SELECT PersonID, min(DateChanged) DateChanged, max(DateChanged) - min(DateChanged) duration
FROM grps
GROUP BY PersonID, grp
)
SELECT PersonID, sum(duration) days_out
FROM durations
GROUP BY PersonID;
db<>fiddle
year_span is used to split an interval spanning across two years in two different records
H1 adds a row number dependent from PersonID to get the right sequence for each person
H2 gets the periods for each status change and extract 1st day of the year of the interval end
H3 split records that span across two years and calculate the right date_start and date_end for each interval
H calculates days elapsed in each interval for each year
final query sum up the records to get output
EDIT
If you need workdays instead of total days, you should not use total_days/7*5 because it is a bad approximation and in some cases gives weird results.
I have posted a solution to jump on fridays to mondays here
with
statuses (sid, sdescr) as (
select 1, 'Active' from dual union all
select 2, 'Out of the Office' from dual union all
select 3, 'Other' from dual
),
ScheduleHistory(RecordID, PersonID, PreviousStatusID, NextStatusID , DateChanged) as (
select 1, 111, 1, 2, date '2020-03-11' from dual union all
select 2, 111, 2, 1, date '2020-03-13' from dual union all
select 3, 111, 1, 3, date '2020-04-01' from dual union all
select 4, 111, 3, 1, date '2020-04-07' from dual union all
select 5, 111, 1, 2, date '2020-06-03' from dual union all
select 6, 111, 2, 1, date '2020-06-05' from dual union all
select 7, 111, 1, 2, date '2020-09-14' from dual union all
select 8, 111, 2, 1, date '2020-09-17' from dual union all
SELECT 9, 222, 1, 2, date '2019-12-31' from dual UNION ALL
SELECT 10, 222, 2, 2, date '2020-12-01' from dual UNION ALL
SELECT 11, 222, 2, 2, date '2021-01-02' from dual
),
year_span (n) as (
select 1 from dual union all
select 2 from dual
),
H1 AS (
SELECT ROW_NUMBER() OVER (PARTITION BY PersonID ORDER BY RecordID) PID, H.*
FROM ScheduleHistory H
),
H2 as (
SELECT
H1.*, H2.DateChanged DateChanged2,
EXTRACT(YEAR FROM H2.DateChanged) - EXTRACT(YEAR FROM H1.DateChanged) + 1 Y,
trunc(H2.DateChanged,'YEAR') Y2
FROM H1 H1
LEFT JOIN H1 H2 ON H1.PID = H2.PID-1 AND H1.PersonID = H2.PersonID
),
H3 AS (
SELECT Y, N, H2.PID, H2.RecordID, H2.PersonID, H2.NextStatusID,
CASE WHEN Y=1 THEN H2.DateChanged ELSE CASE WHEN N=1 THEN H2.DateChanged ELSE Y2 END END D1,
CASE WHEN Y=1 THEN H2.DateChanged2 ELSE CASE WHEN N=1 THEN Y2 ELSE H2.DateChanged2 END END D2
FROM H2
JOIN year_span N ON N.N <=Y
),
H AS (
SELECT PersonID, NextStatusID, EXTRACT(year FROM d1) Y, d2-d1 D
FROM H3
)
select PersonID, sdescr Status, Y, sum(d) d
from H
join statuses s on NextStatusID = s.sid
group by PersonID, sdescr, Y
order by PersonID, sdescr, Y
output
PersonID Status Y d
111 Active 2020 177
111 Other 2020 6
111 Out of the Office 2020 7
222 Out of the Office 2019 1
222 Out of the Office 2020 366
222 Out of the Office 2021 1
check the fiddle here

Execute a oracle pl/sql query and return the result set based on the run time date value

I have below data
empid date amount
1 12-FEB-2017 10
1 12-FEB-2017 10
1 13-FEB-2017 10
1 14-FEB-2017 10
I need a query to return the total amount for a given id and date i.e, below result set
empid date amount
1 12-FEB-2017 20
1 13-FEB-2017 10
1 14-FEB-2017 10
but the think is, from the UI i will be getting the date as input.. if they pass the date return the result for that date .. if they dont pass the date return the result for most recent date.
below is the query that I wrote .. but it is working partially..
SELECT sum(amount),empid,date
FROM employee emp,
where
((date= :ddd) OR aum_valutn_dt = (select max(date) from emp))
AND emp.id = '1'
group by (empid,date)
Please help..
I think you could do something like this
but it is pretty bad you should try to do it some other way
it is doing extra work to get the most recent date
select amt, empid, date
from
(
select amt, empid, date, rank() over (order by date desc) date_rank
from
(SELECT sum(amount) amt,empid,date
FROM employee emp
where emp.id = '1'
and (date = :ddd or :ddd is null)
group by empid, date)
)
where date = :ddd or (:ddd is null and date_rank=1)
Here's another option; scans TEST table twice so ... mind the performance.
SQL> with test (empid, datum, amount) as
2 (select 1, date '2017-02-12', 10 from dual union all
3 select 1, date '2017-02-12', 10 from dual union all
4 select 1, date '2017-02-13', 10 from dual union all
5 select 1, date '2017-02-14', 10 from dual
6 )
7 select t.empid, t.datum, sum(t.amount) sum_amount
8 from test t
9 where t.datum = (select max(t1.datum)
10 from test t1
11 where t1.empid = t.empid
12 and (t1.datum = to_date('&&par_datum', 'dd.mm.yyyy')
13 or '&&par_datum' is null)
14 )
15 group by t.empid, t.datum;
Enter value for par_datum: 13.02.2017
EMPID DATUM SUM_AMOUNT
---------- ---------- ----------
1 13.02.2017 10
SQL> undefine par_datum
SQL> /
Enter value for par_datum:
EMPID DATUM SUM_AMOUNT
---------- ---------- ----------
1 14.02.2017 10
SQL>
SELECT sum(amount),empid,date
FROM employee emp,
where date =nvl((:ddd ,(select max(date) from emp))
AND emp.id = '1'
group by (empid,date)
My solution is following:
with t (empid, datum, amount) as
(select 1, date '2017-02-12', 10 from dual union all
select 1, date '2017-02-12', 10 from dual union all
select 1, date '2017-02-13', 10 from dual union all
select 1, date '2017-02-14', 10 from dual
)
select empid, datum, s
from (select empid, datum, sum(amount) s, max(datum) over (partition by empid) md
from t
group by empid, datum)
where datum = nvl(to_date(:p, 'yyyy-mm-dd'), md);
Calculate maximal date in the subquery and then, in outer subquery, compare the date with nvl(to_date(:p, 'yyyy-mm-dd'), md). If the paremeter is null, then the date field is compared with maximal date.

sqHow to get sum values weekly based between two dates?

My table values like ...
Date Amt Cash Money Name
15-Jun 100 10 20 GUL
16-Jun 200 20 40 ABC
20-Jun 300 30 60 GUL
25-Jun 400 40 80 BCA
28-Jun 500 50 10 GUL
3-Jul 600 60 120 ABC
19-Jun 700 70 140 BCA
26-Jun 800 80 160 ABC
7-Jul 900 90 180 GUL
9-Jul 1000 100 200 ABC
I need to return weekly based sum of values between two date in oracle .My expected output.
Date Amt Cash Mony
13 to 19 June 1000 100 200
20 to 26 June 1500 150 300
27 to3 July 1100 110 130
4 to 10 July 1900 190 380
you can achieve this by a case statement:
e.g.
-- test data
with data(dat,
val1,
val2) as
(select sysdate - 7, 12, 13
from dual
union all
select sysdate - 6, 32, 1
from dual
union all
select sysdate - 5, 52, 53
from dual
union all
select sysdate - 4, 2, 16
from dual
union all
select sysdate - 3, 72, 154
from dual)
select -- build up your groups
case
when d.dat < to_date('28.09.2016', 'DD.MM.YYYY') then
'<28.09.'
when d.dat > to_date('30.09.2016', 'DD.MM.YYYY') then
'>30.09.'
else
'28.-30.'
end as grp,
sum(val1),
sum(val2)
from data d
group by case
when d.dat < to_date('28.09.2016', 'DD.MM.YYYY') then
'<28.09.'
when d.dat > to_date('30.09.2016', 'DD.MM.YYYY') then
'>30.09.'
else
'28.-30.'
end;
-- output
grp sum(val1) sum(val2)
28.-30. 84 54
<28.09. 12 13
>30.09. 74 170
To group by calendar week use
-- test data
with data(dat,
val1,
val2) as
(select sysdate - 9, 12, 13
from dual
union all
select sysdate - 6, 32, 1
from dual
union all
select sysdate - 5, 52, 53
from dual
union all
select sysdate - 4, 2, 16
from dual
union all
select sysdate + 3, 72, 154
from dual)
select TRUNC(dat, 'iw') ||'-'|| TRUNC(dat+7, 'iw'),
sum(val1),
sum(val2)
from data
group by TRUNC(dat, 'iw') ||'-'|| TRUNC(dat+7, 'iw');
The query below has the input dates (from and to) in the first factored subquery. Those can be made into bind variables, or whatever mechanism you want to use to pass these inputs to the query. Then I have the test data in the second factored subquery; you don't need that in your final solution. I create all the needed weeks in the "weeks" factored subquery and I use a left outer join, so that weeks with no transactions will show 0 sums. Note that in the main query, where I do a join, the "date" column from the base table is not enclosed within any kind of function; this allows the use of an index on that column, which you should have if the table is very large, or if performance may be a concern for any other reason. Note that the output is different from yours (missing the last row) because I input a to-date before the last row in the table. That is intentional, I wanted to make sure the query works correctly. Also: I didn't use "date" or "week" as column names; that is a very poor practice. Reserved Oracle keywords should not be used as column names. I used "dt" and "wk" instead.
with
user_inputs ( from_dt, to_dt ) as (
select to_date('4-Jun-2016', 'dd-Mon-yyyy'), to_date('3-Jul-2016', 'dd-Mon-yyyy') from dual
),
test_data ( dt, amt, cash, money, name ) as (
select to_date('15-Jun-2016', 'dd-Mon-yyyy'), 100, 10, 20, 'GUL' from dual union all
select to_date('16-Jun-2016', 'dd-Mon-yyyy'), 200, 20, 40, 'ABC' from dual union all
select to_date('20-Jun-2016', 'dd-Mon-yyyy'), 300, 30, 60, 'GUL' from dual union all
select to_date('25-Jun-2016', 'dd-Mon-yyyy'), 400, 40, 80, 'BCA' from dual union all
select to_date('28-Jun-2016', 'dd-Mon-yyyy'), 500, 50, 10, 'GUL' from dual union all
select to_date( '3-Jul-2016', 'dd-Mon-yyyy'), 600, 60, 120, 'ABC' from dual union all
select to_date('19-Jun-2016', 'dd-Mon-yyyy'), 700, 70, 140, 'BCA' from dual union all
select to_date('26-Jun-2016', 'dd-Mon-yyyy'), 800, 80, 160, 'ABC' from dual union all
select to_date( '7-Jul-2016', 'dd-Mon-yyyy'), 900, 90, 180, 'GUL' from dual union all
select to_date( '9-Jul-2016', 'dd-Mon-yyyy'), 1000, 100, 200, 'ABC' from dual
),
weeks ( start_dt ) as (
select trunc(from_dt, 'iw') + 7 * (level - 1)
from user_inputs
connect by level <= 1 + (to_dt - trunc(from_dt, 'iw')) / 7
)
select to_char(w.start_dt, 'dd-Mon-yyyy') || ' - ' ||
to_char(w.start_dt + 6, 'dd-Mon-yyyy') as wk,
nvl(sum(t.amt), 0) as tot_amt, nvl(sum(t.cash), 0) as tot_cash,
nvl(sum(t.money), 0) as tot_money
from weeks w left outer join test_data t
on t.dt >= w.start_dt and t.dt < w.start_dt + 7
group by start_dt
order by start_dt
;
Output:
WK TOT_AMT TOT_CASH TOT_MONEY
-------------------------------------------- ---------- ---------- ----------
30-May-2016 - 05-Jun-2016 0 0 0
06-Jun-2016 - 12-Jun-2016 0 0 0
13-Jun-2016 - 19-Jun-2016 1000 100 200
20-Jun-2016 - 26-Jun-2016 1500 150 300
27-Jun-2016 - 03-Jul-2016 1100 110 130
You can try like below, I chose 13-Jun-2016 as a starting date. You can chose it as per your requirement upto any range of dates.
with t as
(select dt,
min(dt) over (partition by week)||' to '|| max(dt) over (partition by week) week
from (
select to_date('13-Jun-2016','dd-Mon-yyyy')+(level-1) dt,
ceil(level/7) week
from dual
connect by level<=52))
select week,
sum(amt),
sum(cash),
sum(money)
from (
select your_table.*,
t.week
from your_table,t
where trunc(to_date(your_table.dt,'dd-Mon-yyyy'))=trunc(t.dt))
group by week;

How to identify positive minimum or negative maximum in a column for a key?

I have the following columns - Person_ID Days. For one person id, multiple days are possible. Something like this:
Person_Id Days
1000 100
1000 200
1000 -50
1000 -10
1001 100
1001 200
1001 50
1001 10
1002 -50
1002 -10
I need to address the following scenarios:
If all values for days column are positive, I need minimum of the days for a person_id. If the days column has both positive and negative, I need minimum of positive. If all negatives, I need maximum of negative.
The output like:
Person_id Days
1000 100
1001 10
1002 -10
I tried using case statement, but I am unable to use a same column in the condition as well as grouping.
Try this (Postgres 9.4+):
select person_id, coalesce(min(days) filter (where days > 0), max(days))
from a_table
group by 1
order by 1;
Oracle Setup:
CREATE TABLE table_name ( Person_Id, Days ) AS
SELECT 1000, 100 FROM DUAL UNION ALL
SELECT 1000, 200 FROM DUAL UNION ALL
SELECT 1000, -50 FROM DUAL UNION ALL
SELECT 1000, -10 FROM DUAL UNION ALL
SELECT 1001, 100 FROM DUAL UNION ALL
SELECT 1001, 200 FROM DUAL UNION ALL
SELECT 1001, 50 FROM DUAL UNION ALL
SELECT 1001, 10 FROM DUAL UNION ALL
SELECT 1002, -50 FROM DUAL UNION ALL
SELECT 1002, -10 FROM DUAL;
Query:
SELECT person_id, days
FROM (
SELECT t.*,
ROW_NUMBER() OVER ( PARTITION BY person_id
ORDER BY SIGN( ABS( days ) ),
SIGN( DAYS ) DESC,
ABS( DAYS )
) AS rn
FROM table_name t
)
WHERE rn = 1;
Output:
PERSON_ID DAYS
---------- ----------
1000 100
1001 10
1002 -10
Oracle solution:
with
input_data ( person_id, days) as (
select 1000, 100 from dual union all
select 1000, 200 from dual union all
select 1000, -50 from dual union all
select 1000, -10 from dual union all
select 1001, 100 from dual union all
select 1001, 200 from dual union all
select 1001, 50 from dual union all
select 1001, 10 from dual union all
select 1002, -50 from dual union all
select 1002, -10 from dual
)
select person_id,
NVL(min(case when days > 0 then days end), max(days)) as days
from input_data
group by person_id;
PERSON_ID DAYS
---------- ----------
1000 100
1001 10
1002 -10
For each person_id, if there is at least one days value that is strictly positive, then the min will be taken over positive days only and will be returned by NVL(). Otherwise the min() will return null, and NVL() will return max() over all days (all of which are, in this case, negative or 0).
select Person_id, min(abs(days)) * days/abs(days) from table_name
group by Person_id
-- + handle zero_divide .. SORRY.. the above works only in MySQL .
Something like this will work anywhere which is equivalent of above query:
select t.Person_id , min(t.days) from table_name t,
(select Person_id, min(abs(days)) as days from table_name group by Person_id) v
where t.Person_id = v.Person_id
and abs(t days) = v.days
group by Person_id;
OR
select id, min(Days) from (
select Person_id, min(abs(Days)) as Days from temp group by Person_id
union
select Person_id, max(Days) as Days from temp group by Person_id
) temp
group by Person_id;
You can do this by using GroupBy clause in sql server. Take a look into below query:-
CREATE TABLE #test(Person_Id INT, [Days] INT)
DECLARE #LargestNumberFromTable INT;
INSERT INTO #test
SELECT 1000 , 100 UNION
SELECT 1000 , 200 UNION
SELECT 1000 , -50 UNION
SELECT 1000 , -10 UNION
SELECT 1001 , 100 UNION
SELECT 1001 , 200 UNION
SELECT 1001 , 50 UNION
SELECT 1001 , 10 UNION
SELECT 1002 , -50 UNION
SELECT 1002 , -10
SELECT #LargestNumberFromTable = ISNULL(MAX([Days]), 0)
FROM #test
SELECT Person_Id
,CASE WHEN SUM(IIF([Days] > 0,[Days] , 0)) = 0 THEN MAX([Days]) -- All Negative
WHEN SUM([Days]) = SUM(IIF([Days] > 0, [Days], 0)) THEN MIN ([Days]) -- ALL Positive
WHEN SUM([Days]) <> SUM(IIF([Days] > 0, [Days], 0)) THEN MIN(IIF([Days] > 0, [Days], #LargestNumberFromTable)) --Mix (Negative And positive)
END AS [Days]
FROM #test
GROUP BY Person_Id
DROP TABLE #test