RESET SUM(AMT_FIELD) OVER(PARTITION BY UNIQUE FIELD ORDER BY ROWNUM) - sql

I just wanted to know, how can I reset summation of number field in below SQL query.
Attached screenshot is the sample of result that I need to get.
Query used:
SUM(UNPAID_MONTHLY) OVER(PARTITION BY SAMPLE_ACCT ORDER BY MONTH_NO DESC) TOTAL_UNPAID_AMT

You need to reset the sum every time the value is zero. You can use cumulative sum to define the group and then another cumulative sum:
select t.*,
sum(unpaid_monthly) over (partition by sample_acct, grp order by month_no desc)
from (select t.*,
sum(case when unpaid_monthly = 0 then 1 else 0 end) over (partition by sample_acct order by month_no) as grp
from t
) t;

You can also use the MATCH_RECOGNIZE clause (if you run Oracle 12 or higher):
WITH t (unpaid_monthly, sample_acct, month_no) AS
(SELECT 1335.67, 22900005, 1 FROM dual UNION ALL
SELECT 1289.36, 22900005, 2 FROM dual UNION ALL
SELECT 1241.95, 22900005, 3 FROM dual UNION ALL
SELECT 1211.32, 22900005, 4 FROM dual UNION ALL
SELECT 1179.33, 22900005, 5 FROM dual UNION ALL
SELECT 0, 22900005, 6 FROM dual UNION ALL
SELECT 5509.8, 22900005, 7 FROM dual UNION ALL
SELECT 3388.59, 22900005, 8 FROM dual UNION ALL
SELECT 1398.41, 22900005, 9 FROM dual UNION ALL
SELECT 0, 22900005, 10 FROM dual UNION ALL
SELECT 1717.97, 22900005, 11 FROM dual UNION ALL
SELECT 0, 22900005, 12 FROM dual UNION ALL
SELECT 5016.4, 22900005, 13 FROM dual)
SELECT unpaid_monthly, sample_acct, month_no,
sum_unpaid + unpaid_monthly AS TOTAL_UNPAID_AMT
FROM t
MATCH_RECOGNIZE (
PARTITION BY sample_acct
ORDER BY month_no
MEASURES
FINAL SUM(unpaid_monthly) - SUM(unpaid_monthly) AS sum_unpaid
ALL ROWS PER MATCH
PATTERN (a+ b?)
DEFINE
a AS unpaid_monthly > 0);
UNPAID_MONTHLY SAMPLE_ACCT MONTH_NO TOTAL_UNPAID_AMT
=============================================================
1335.67 22900005 1 6257.63
1289.36 22900005 2 4921.96
1241.95 22900005 3 3632.6
1211.32 22900005 4 2390.65
1179.33 22900005 5 1179.33
0 22900005 6 0
5509.8 22900005 7 10296.8
3388.59 22900005 8 4787
1398.41 22900005 9 1398.41
0 22900005 10 0
1717.97 22900005 11 1717.97
0 22900005 12 0
5016.4 22900005 13 5016.4

Related

Hot to get a average value on analytic function

I have to get the average value on this BAL column for each account
with cte as (
select distinct t.DATE_ID,
ad.ACCOUNT_ID
from TIMEDATE t
, ACCOUNT_DLY ad)
select cte.date_id,
cte.Account_ID,
NVL(current_bal,lag (ad.current_bal) ignore nulls over (PARTITION by cte.account_id order by cte.date_id )) as bal
from cte left join ACCOUNT_DLY ad
on cte.date_id = ad.SRC_EXTRACT_DT
and cte.ACCOUNT_ID = ad.ACCOUNT_ID
order by 2,1;
I guess I need to use analytic function SUM or AVG again with partition on the NVL
on the top of the picture is my table on the bottom is how it suppose to look like
https://i.stack.imgur.com/H2Dql.png
Analytic vs Aggregate Function
You probably should use aggregate function AVG() ... GROUP BY instead of analytic function AVG() OVER(). The difference is that aggregate function returns one and only one row per columns it is grouped by. Analytic function puts the result in every resulting row and, in the sample as yours, that means use of DISTINCT keyword to eliminate duplicate rows which could be performance costly with bigger datasets. You can have the same result using aggregate or analytic function, though.
This is just to compare one and another - if your data is like this:
WITH
tbl AS
(
Select 1 "ID", To_Date('01-JAN-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
Select 1 "ID", To_Date('02-JAN-2023', 'dd-MON-yyyy') "A_DATE", 7 "A_VALUE" From Dual Union All
Select 1 "ID", To_Date('03-JAN-2023', 'dd-MON-yyyy') "A_DATE", 8 "A_VALUE" From Dual Union All
Select 2 "ID", To_Date('01-JAN-2023', 'dd-MON-yyyy') "A_DATE", 9 "A_VALUE" From Dual Union All
Select 2 "ID", To_Date('02-JAN-2023', 'dd-MON-yyyy') "A_DATE", 9 "A_VALUE" From Dual Union All
Select 2 "ID", To_Date('03-JAN-2023', 'dd-MON-yyyy') "A_DATE", 9 "A_VALUE" From Dual Union All
Select 3 "ID", To_Date('01-JAN-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
Select 3 "ID", To_Date('02-JAN-2023', 'dd-MON-yyyy') "A_DATE", 3 "A_VALUE" From Dual Union All
Select 3 "ID", To_Date('03-JAN-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
--
Select 1 "ID", To_Date('01-FEB-2023', 'dd-MON-yyyy') "A_DATE", 4 "A_VALUE" From Dual Union All
Select 1 "ID", To_Date('02-FEB-2023', 'dd-MON-yyyy') "A_DATE", 3 "A_VALUE" From Dual Union All
Select 1 "ID", To_Date('03-FEB-2023', 'dd-MON-yyyy') "A_DATE", 2 "A_VALUE" From Dual Union All
Select 2 "ID", To_Date('01-FEB-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
Select 2 "ID", To_Date('02-FEB-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
Select 2 "ID", To_Date('03-FEB-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
Select 3 "ID", To_Date('01-FEB-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual Union All
Select 3 "ID", To_Date('02-FEB-2023', 'dd-MON-yyyy') "A_DATE", 9 "A_VALUE" From Dual Union All
Select 3 "ID", To_Date('03-FEB-2023', 'dd-MON-yyyy') "A_DATE", 6 "A_VALUE" From Dual
)
... then use of aggregate function gives you the result you wanted...
-- Aggregate Function AVG() ... GROUP BY
Select DISTINCT
ID, To_Char(A_DATE, 'yyyymm') "MTH", AVG(A_VALUE) "AVG_VALUE"
From
tbl
Group By
ID, To_Char(A_DATE, 'yyyymm')
Order By
To_Char(A_DATE, 'yyyymm'), ID
/*
ID MTH AVG_VALUE
---------- ------ ----------
1 202301 7
2 202301 9
3 202301 5
1 202302 3
2 202302 6
3 202302 7
*/
... on the other hand, using analytic function (without DISTINCT keyword) will result with the same AVG value in all rows with same ID and MONTH...
-- Analytic Function AVG() OVER()
Select --DISTINCT
ID, To_Char(A_DATE, 'yyyymm') "MTH", AVG(A_VALUE) OVER(Partition By ID, To_Char(A_DATE, 'yyyymm') ) "AVG_VALUE"
From
tbl
Order By
To_Char(A_DATE, 'yyyymm'), ID
/*
ID MTH AVG_VALUE
---------- ------ ----------
1 202301 7
1 202301 7
1 202301 7
2 202301 9
2 202301 9
2 202301 9
3 202301 5
3 202301 5
3 202301 5
1 202302 3
1 202302 3
1 202302 3
2 202302 6
2 202302 6
2 202302 6
3 202302 7
3 202302 7
3 202302 7
... and if you put the DISTINCT keyword in your Select statement you will get the same result as with aggregate function above
-- using DISTINCT
/*
ID MTH AVG_VALUE
---------- ------ ----------
1 202301 7
2 202301 9
3 202301 5
1 202302 3
2 202302 6
3 202302 7
*/

Return total months in overlapping date range for each ID in SQL oracle

Sample data:
MBR_ID
MIN_SPANFROM
MAX_SPANFROM
1
202101
202105
1
202101
202108
1
202111
202112
2
202101
202109
2
202103
202108
2
202110
202110
3
202102
202107
3
202104
202110
3
202109
202112
4
202101
202105
4
202101
202105
5
202109
202110
5
202105
202106
date format: yyyymm
Required output:
MBR_ID
TOTAL_MONTHS_2021
1
10
2
10
3
11
4
5
5
4
Example:
For ID 1, date range: Jan to may, Jan to Aug and Nov to Dec then total months is Jan to Aug + Nov to Dec = 8 + 2 = 10
You can use:
WITH as_dates (mbr_id, min_spanfrom, max_spanfrom) AS (
SELECT mbr_id,
TO_DATE(min_spanfrom, 'YYYYMM'),
TO_DATE(max_spanfrom, 'YYYYMM')
FROM table_name
),
group_changes (mbr_id, min_spanfrom, max_spanfrom, group_change) AS (
SELECT d.*,
CASE
WHEN min_spanfrom
<= MAX(max_spanfrom) OVER (
PARTITION BY mbr_id
ORDER BY min_spanfrom, max_spanfrom DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
)
THEN 0
ELSE 1
END
FROM as_dates d
),
groups (mbr_id, min_spanfrom, max_spanfrom, grp) AS (
SELECT mbr_id,
min_spanfrom,
max_spanfrom,
SUM(group_change) OVER (
PARTITION BY mbr_id
ORDER BY min_spanfrom, max_spanfrom DESC
)
FROM group_changes
),
months (mbr_id, months) AS (
SELECT mbr_id,
MONTHS_BETWEEN(MAX(max_spanfrom), MIN(min_spanfrom)) + 1
FROM groups
GROUP BY mbr_id, grp
)
SELECT mbr_id,
SUM(months) AS total_months
FROM months
GROUP BY mbr_id;
Which, for the sample data:
CREATE TABLE table_name (MBR_ID, MIN_SPANFROM, MAX_SPANFROM) AS
SELECT 1, 202101, 202105 FROM DUAL UNION ALL
SELECT 1, 202101, 202108 FROM DUAL UNION ALL
SELECT 1, 202111, 202112 FROM DUAL UNION ALL
SELECT 2, 202101, 202109 FROM DUAL UNION ALL
SELECT 2, 202103, 202108 FROM DUAL UNION ALL
SELECT 2, 202110, 202110 FROM DUAL UNION ALL
SELECT 3, 202102, 202107 FROM DUAL UNION ALL
SELECT 3, 202104, 202110 FROM DUAL UNION ALL
SELECT 3, 202109, 202112 FROM DUAL UNION ALL
SELECT 4, 202101, 202105 FROM DUAL UNION ALL
SELECT 4, 202101, 202105 FROM DUAL UNION ALL
SELECT 5, 202109, 202110 FROM DUAL UNION ALL
SELECT 5, 202105, 202106 FROM DUAL;
Outputs:
MBR_ID
TOTAL_MONTHS
1
10
2
10
3
11
4
5
5
4
db<>fiddle here

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

Return Month wise count if no data for month return 0 as count in oracle sql

I have a table having data for January to March (till current month) and I am able to take the month wise count.But user required is to display zero for rest of the month.Kindly suggest.
For example:
select count(a.emp_id) as cnt ,to_char(a.due_date,'MONTH') as Process_Month from EMP_Request a
where a.due_date is not null
group by to_char(a.due_date,'MONTH')
Output:
cnt Process_month
20 JANUARY
35 FEBUARY
26 March
Desired output:
cnt Process_month
20 JANUARY
35 FEBUARY
26 March
0 APRIL
0 MAY
…….
….
….
0 DECEMBER
Please assist.
use WWV_FLOW_MONTHS_MONTH to get all the month and left join with your query to get the month name from the date column and join with it
with cte
(
SELECT month_display as month FROM WWV_FLOW_MONTHS_MONTH
) , cnt as
(
select count(a.emp_id) as cnt ,
to_char(a.due_date,'MONTH') as Process_Month from EMP_Request a
where a.due_date is not null
group by to_char(a.due_date,'MONTH')
) select coalesce(Process_Month,month), cnt from cte left join cnt on cte.month=cnt.to_char(to_date(Process_Month, 'DD-MM-YYYY'), 'Month')
Right join months generator with your query:
select to_char(to_date(mth_num, 'MM'), 'MONTH') month, nvl(cnt, 0) cnt
from (
select count(emp_id) as cnt, to_char(due_date, 'mm') mth_num
from emp_request where due_date is not null
group by to_char(due_date, 'mm')) e
right join (
select to_char(level, 'fm00') mth_num
from dual connect by level <= 12) m using (mth_num)
order by mth_num
dbfiddle demo
Months generator is a simple hierarchical query which gives us 12 values 01, 02... 12:
select to_char(level, 'fm00') mth_num from dual connect by level <= 12
You can also use system views to get these numbers:
select to_char(rownum, 'fm00') mth_num from all_objects where rownum <= 12
or this syntax:
select to_char(column_value, 'fm00') mth_num
from table(sys.odcivarchar2list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12))
It's better to work on numbers which you can sort properly and convert to month names in the last step. This way you have natural months order.
If you want to be sure that month names are always in english, not dependent from local settings then use to_date with third parameter, like here:
select to_char(sysdate, 'month', 'nls_date_language=english') from dual
This is a general problem which is not really a sql problem. SQL doesn't really know about what months you are interested in. So the solution is to tell it in a sub query.
Here is a solution that doesn't use external tables. You simply select all months of the year and outer join your data.
select TO_CHAR(TO_DATE(available_months.m,'MM'),'MONTH') , NVL(sum(data.cnt),0) from
(select to_number(to_char(sysdate,'MM')) m, 7 cnt from dual) data,
(select 1 m from dual union select 2 from dual union select 3 from dual union select 4 from dual
union select 5 from dual union select 6 from dual union select 7 from dual
union select 8 from dual union select 9 from dual union select 10 from dual
union select 11 from dual union select 12 from dual) available_months
where
data.m (+) = available_months.m
group by available_months.m
order by available_months.m;
Or with your data query included is should look like (not tested):
select TO_CHAR(TO_DATE(available_months.m,'MM'),'MONTH') , NVL(sum(data.cnt),0) from
(select count(a.emp_id) as cnt ,to_char(a.due_date,'MONTH') as Process_Month from EMP_Request a where a.due_date is not null) data
(select 1 m from dual union select 2 from dual union select 3 from dual union select 4 from dual
union select 5 from dual union select 6 from dual union select 7 from dual
union select 8 from dual union select 9 from dual union select 10 from dual
union select 11 from dual union select 12 from dual) available_months
where
data.due_date (+) = available_months.m
group by available_months.m
order by available_months.m;

Oracle: Identifying peak values in a time series

I have following values in a column of table. there are two columns in table. The other column is having distinct dates in descending order.
3
4
3
21
4
4
-1
3
21
-1
4
4
8
3
3
-1
21
-1
4
The graph will be
I need only peaks higlighted in graph with circles in output
4
21
21
8
21
4
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE TEST ( datetime, value ) AS
SELECT DATE '2015-01-01', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-02', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-03', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-04', 21 FROM DUAL
UNION ALL SELECT DATE '2015-01-05', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-06', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-07', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-08', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-09', 21 FROM DUAL
UNION ALL SELECT DATE '2015-01-10', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-11', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-12', 4 FROM DUAL
UNION ALL SELECT DATE '2015-01-13', 8 FROM DUAL
UNION ALL SELECT DATE '2015-01-14', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-15', 3 FROM DUAL
UNION ALL SELECT DATE '2015-01-16', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-17', 21 FROM DUAL
UNION ALL SELECT DATE '2015-01-18', -1 FROM DUAL
UNION ALL SELECT DATE '2015-01-19', 4 FROM DUAL
Query 1:
SELECT datetime, value
FROM (
SELECT datetime,
LAG( value ) OVER ( ORDER BY datetime ) AS prv,
value,
LEAD( value ) OVER ( ORDER BY datetime ) AS nxt
FROM test
)
WHERE ( prv IS NULL OR prv < value )
AND ( nxt IS NULL OR nxt < value )
Results:
| DATETIME | VALUE |
|---------------------------|-------|
| January, 02 2015 00:00:00 | 4 |
| January, 04 2015 00:00:00 | 21 |
| January, 09 2015 00:00:00 | 21 |
| January, 13 2015 00:00:00 | 8 |
| January, 17 2015 00:00:00 | 21 |
| January, 19 2015 00:00:00 | 4 |
So the peak is defined as the previous value and next value being less than the current value, and you can retrieve the previous an next using LAG() and LEAD() functions.
You really need some other column (e.g. my_date) to define the order of the rows, then you can:
select my_date,
value
from (select value,
lag(value ) over (order by my_date) lag_value,
lead(value) over (order by my_date) lead_value
from my_table)
where value > coalesce(lag_value , value - 1) and
value > coalesce(lead_value, value - 1);
This would not allow for a "double-peak" such as:
1,
15,
15,
4
... for which much more complex logic would be needed.
Just for completeness the row pattern matching example:
WITH source_data(datetime, value) AS (
SELECT DATE '2015-01-01', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-02', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-03', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-04', 21 FROM DUAL UNION ALL
SELECT DATE '2015-01-05', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-06', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-07', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-08', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-09', 21 FROM DUAL UNION ALL
SELECT DATE '2015-01-10', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-11', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-12', 4 FROM DUAL UNION ALL
SELECT DATE '2015-01-13', 8 FROM DUAL UNION ALL
SELECT DATE '2015-01-14', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-15', 3 FROM DUAL UNION ALL
SELECT DATE '2015-01-16', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-17', 21 FROM DUAL UNION ALL
SELECT DATE '2015-01-18', -1 FROM DUAL UNION ALL
SELECT DATE '2015-01-19', 4 FROM DUAL
)
SELECT *
FROM
source_data MATCH_RECOGNIZE (
ORDER BY datetime
MEASURES
LAST(UP.datetime) AS datetime,
LAST(UP.value) AS value
ONE ROW PER MATCH
PATTERN ((UP DOWN) | UP$)
DEFINE
DOWN AS DOWN.value < PREV(DOWN.value),
UP AS UP.value > PREV(UP.value)
)
ORDER BY
datetime
There is a much more sophisticated method available in Oracle 12c, which is to use pattern matching SQL.
http://docs.oracle.com/database/121/DWHSG/pattern.htm#DWHSG8966
It would be overkill for a situation like this, but if you needed more complex patterns matched, such as W shaped patterns, then it would be worth investigating.
Using LAG function you can compare values from different rows. I assume the resultset you showed is ordered by another column named position.
select value
from
(select value,
lag(value,-1) over (order by position) prev,
lag(value,1) over (order by position) next
from table)
where value > prev
and value > next