Oracle Query to rollup QTY by Year - sql

I have a requirement to find out MAX VALUE from SUM of Quantities Divided by YEAR (Need to write a Oracle Query).
For Example
ITEM_ID ORG_ID YEAR QTY
100 121 2015 10
100 121 2016 5
100 121 2017 8
101 146 2014 10
101 146 2015 11
101 146 2016 12
101 146 2017 13
My Output should be like this :-
for Item_id 100,121 the max_avg should be max(10+5+8/3, 5+10/2, 10/1)... max (7.6, 7.5, 8) = 8
for Item_id 101,146 the max_avg should be max(10+11+12+13/4, 11+12+13/3, 12+13/2, 13/1)... max(11.5, 12, 12.5, 13) = 13
ITEM_ID ORG_ID YEAR QTY MAX_AVG
100 121 2015 10 8
100 121 2016 5 8
100 121 2017 8 8
101 146 2014 10 13
101 146 2015 11 13
101 146 2016 12 13
101 146 2017 13 13
Any help would be greatly appreciated.

You need two layers of analytic functions: You need analytic MAX (of something) because you want to return all rows from the original table; and within the MAX you need analytic (rolling) average. Analytic functions can't be nested, so you need a subquery and an outer query. Something like this:
with inputs ( item_id, org_id, yr, qty ) as (
select 100, 121, 2015, 10 from dual
union all select 100, 121, 2016, 5 from dual
union all select 100, 121, 2017, 8 from dual
union all select 101, 146, 2014, 10 from dual
union all select 101, 146, 2015, 11 from dual
union all select 101, 146, 2016, 12 from dual
union all select 101, 146, 2017, 13 from dual
)
-- End of simulated inputs (for testing only, not part of the solution).
-- SQL query begins BELOW THIS LINE. Use your actual table and column names.
select item_id, org_id, yr, qty,
max(forward_avg) over ( partition by item_id, org_id ) as max_avg
from ( select item_id, org_id, yr, qty,
avg(qty) over ( partition by item_id, org_id
order by yr desc ) as forward_avg
from inputs i
) b
order by item_id, org_id, yr -- If needed
;
ITEM_ID ORG_ID YR QTY MAX_AVG
---------- ---------- ---------- ---------- ----------
100 121 2015 10 8
100 121 2016 5 8
100 121 2017 8 8
101 146 2014 10 13
101 146 2015 11 13
101 146 2016 12 13
101 146 2017 13 13

Related

Generate rows to fill in gaps between years, carry over a value from previous year

I have a table of road condition ratings (roads are rated from 1-20; 20 being good).
with road_inspections
(road_id, year, cond) as (
select 1, 2009, 17 from dual union all
select 1, 2011, 16 from dual union all
select 1, 2015, 14 from dual union all
select 1, 2016, 18.3 from dual union all
select 1, 2019, 18.1 from dual union all
select 2, 2013, 17.5 from dual union all
select 2, 2016, 18 from dual union all
select 2, 2019, 18 from dual union all
select 2, 2022, 18 from dual union all
select 3, 2022, 20 from dual)
select * from road_inspections
ROAD_ID YEAR COND
---------- ---------- ----------
1 2009 17
1 2011 16
1 2015 14
1 2016 18.3
1 2019 18.1
2 2013 17.5
2 2016 18
2 2019 18
2 2022 18
3 2022 20
db<>fiddle
In a query, for each road, I want to generate rows to fill in the gaps between the years.
For a given road, starting at the first row (the earliest inspection), there should be consecutive rows for each year all the way to the current year (the sysdate year; currently 2022).
For the filler rows, I want carry over the condition rating from the last known inspection.
The result would look like this:
ROAD_ID YEAR COND
---------- ---------- ----------
1 2009 17
1 2010 17 *
1 2011 16
1 2012 16 *
1 2013 16 *
1 2014 16 *
1 2015 14
1 2016 18.3
1 2017 18.3 *
1 2018 18.3 *
1 2019 18.1
1 2020 18.1 *
1 2021 18.1 *
1 2022 18.1 *
2 2013 17.5
2 2014 17.5 *
2 2015 17.5 *
2 2016 18
2 2017 18 *
2 2018 18 *
2 2019 18
2 2020 18 *
2 2021 18 *
2 2022 18
3 2022 20
*=filler row
Question:
How can I create those filler rows using an Oracle SQL query?
(My priorities are: simplicity first, performance second.)
You can use the LEAD analytic function with a LATERAL joined hierarchical query to generate the missing rows from each row until the next row:
SELECT r.road_id,
y.year,
r.cond
FROM ( SELECT r.*,
LEAD(year, 1, EXTRACT(YEAR FROM SYSDATE) + 1)
OVER (PARTITION BY road_id ORDER BY year) AS next_year
FROM road_inspections r
) r
CROSS JOIN LATERAL (
SELECT r.year + LEVEL - 1 AS year
FROM DUAL
CONNECT BY r.year + LEVEL - 1 < r.next_year
) y
Which, for the sample data:
CREATE TABLE road_inspections (road_id, year, cond) as
select 1, 2009, 17 from dual union all
select 1, 2011, 16 from dual union all
select 1, 2015, 14 from dual union all
select 1, 2016, 18.3 from dual union all
select 1, 2019, 18.1 from dual union all
select 2, 2013, 17.5 from dual union all
select 2, 2016, 18 from dual union all
select 2, 2019, 18 from dual union all
select 2, 2022, 18 from dual union all
select 3, 2022, 20 from dual;
Outputs:
ROAD_ID
YEAR
COND
1
2009
17
1
2010
17
1
2011
16
1
2012
16
1
2013
16
1
2014
16
1
2015
14
1
2016
18.3
1
2017
18.3
1
2018
18.3
1
2019
18.1
1
2020
18.1
1
2021
18.1
1
2022
18.1
2
2013
17.5
2
2014
17.5
2
2015
17.5
2
2016
18
2
2017
18
2
2018
18
2
2019
18
2
2020
18
2
2021
18
2
2022
18
3
2022
20
db<>fiddle here
with
road_inspections (road_id, year_, cond) as (
select 1, 2009, 17 from dual union all
select 1, 2011, 16 from dual union all
select 1, 2015, 14 from dual union all
select 1, 2016, 18.3 from dual union all
select 1, 2019, 18.1 from dual union all
select 2, 2013, 17.5 from dual union all
select 2, 2016, 18 from dual union all
select 2, 2019, 18 from dual union all
select 2, 2022, 18 from dual union all
select 3, 2022, 20 from dual
)
, prep (road_id, first_year) as (
select road_id, min(year_)
from road_inspections
group by road_id
)
, all_years (road_id, year_) as (
select p.road_id, l.year_
from prep p cross join lateral (
select first_year + level - 1 as year_
from dual
connect by level <= 2022 - first_year + 1
) l
)
select road_id, year_,
last_value(ri.cond ignore nulls) over
(partition by road_id order by year_) as cond
from all_years ay left outer join road_inspections ri using (road_id, year_)
;
The first subquery, prep, finds the first year for each road id. This is used in the all_years subquery to generate all the years relevant for each road id.
Then left-outer-join to the original cata, copy the cond wherever it is available, and use the analytic function last_value with the ignore nulls option to fill in the gaps.
Note that I changed the column name year to year_ (with a trailing underscore); year is an Oracle keyword, not a good choice for a column name.
Output:
ROAD_ID YEAR_ COND
---------- ---------- ----------
1 2009 17
1 2010 17
1 2011 16
1 2012 16
1 2013 16
1 2014 16
1 2015 14
1 2016 18.3
1 2017 18.3
1 2018 18.3
1 2019 18.1
1 2020 18.1
1 2021 18.1
1 2022 18.1
2 2013 17.5
2 2014 17.5
2 2015 17.5
2 2016 18
2 2017 18
2 2018 18
2 2019 18
2 2020 18
2 2021 18
2 2022 18
3 2022 20
Using LEAD function and connect by LEVEL row generator we can achieve the same. The DB FIDDLE here
with r as (
select
*
from
road_inspections
union
select
road_id,
2022,
cond
from
road_inspections
where
(road_id, year) in(
select
road_id,
max(year) over (partition by road_id)
from
road_inspections a
where
not exists (
select
1
from
road_inspections b
where
a.road_id = b.road_id
and b.year = 2022
)
)
),
data as(
SELECT
r.*,
nvl(
lead(year, 1) over (
partition by road_id
order by
year
)- year,
0
) gaps
FROM
r
)
select
road_id,
year + level -1 year,
cond
from
(
select
a.road_id,
year,
cond,
rownum rn,
gaps
from
data a
) connect by level <= gaps
and prior rn = rn
and prior dbms_random.value != 1
order by
road_id,
year + level -1;

How to group sales by month, quarter and year in the same row using case?

I'm trying to return the total number of sales for every month, every quarter, for the year 2016. I want to display annual sales on the first month row, and not on the other rows. Plus, I want to display the quarter sales on the first month of each quarter, and not on the others.
To further explain this, here's what I want to achieve:
MONTH MONTH_SALES QUARTER_SALES YEAR_SALES
1 2183 5917 12505
2 1712 - -
3 1972 - -
4 2230 6588 -
5 2250 - -
6 2108 - -
Here's my SQL query so far:
SELECT
Time.month,
SUM(Sales.sales) AS MONTH_SALES, -- display monthly sales.
CASE
WHEN MOD(Time.month, 3) = 1 THEN ( -- first month of quarter
SELECT
SUM(Sales.sales)
FROM
Sales,
Time
WHERE
Sales.Time_id = Time.Time_id
AND Time.year = 2016
GROUP BY
Time.quarter
FETCH FIRST 1 ROW ONLY
)
END AS QUARTER_SALES,
CASE
WHEN Time.month = 1 THEN ( -- display annual sales.
SELECT
SUM(Sales.sales)
FROM
Sales,
Time
WHERE
Sales.Time_id = Time.Time_id
AND Time.year = 2016
GROUP BY
Time.year
)
END AS YEAR_SALES
FROM
Sales,
Time
WHERE
Sales.Time_id = Time.Time_id
AND Time.year = 2016
GROUP BY
Time.month
ORDER BY
Time.month
I'm almost getting the desired output, but I'm getting the same duplicated 6588 value in quarter sales for the first and fourth month (because I'm fetching the first row that comes from first quarter).
MONTH MONTH_SALES QUARTER_SALES YEAR_SALES
1 2183 6588 12505
2 1712 - -
3 1972 - -
4 2230 6588 -
5 2250 - -
6 2108 - -
I even tried to put WHERE Time.quarter = ((Time.month * 4) / 12) but the month value from the outer query doesn't get passed in the subquery.
Unfortunately I don't have enough experience with CASE WHEN expressions to know how to pass the month row. Any tips would be awesome.
How about this?
Sample data:
SQL> with
2 time (time_id, month, quarter, year) as
3 (select 1, 1, 1, 2016 from dual union all
4 select 2, 2, 1, 2016 from dual union all
5 select 3, 3, 1, 2016 from dual union all
6 select 4, 5, 2, 2016 from dual union all
7 select 5, 7, 3, 2016 from dual union all
8 select 6, 8, 3, 2016 from dual union all
9 select 7, 9, 3, 2016 from dual union all
10 select 8, 10, 4, 2016 from dual union all
11 select 9, 11, 4, 2016 from dual
12 ),
13 sales (time_id, sales) as
14 (select 1, 100 from dual union all
15 select 1, 100 from dual union all
16 select 2, 200 from dual union all
17 select 3, 300 from dual union all
18 select 4, 400 from dual union all
19 select 5, 500 from dual union all
20 select 6, 600 from dual union all
21 select 7, 700 from dual union all
22 select 8, 800 from dual union all
23 select 9, 900 from dual
24 ),
Query begins here; it uses sum aggregate in its analytic form; partition by clause says what to compute. row_number, similarly, sorts rows in each quarter/year - it is later used in CASE expression to decide whether to show quarterly/yearly total or not.
25 temp as
26 (select t.month, t.quarter, t.year, sum(s.sales) month_sales
27 from time t join sales s on s.time_id = t.time_id
28 where t.year = 2016
29 group by t.month, t.quarter, t.year
30 ),
31 temp2 as
32 (select month, quarter, month_sales,
33 sum(month_sales) over (partition by quarter) quarter_sales,
34 sum(month_sales) over (partition by year ) year_sales,
35 row_number() over (partition by quarter order by quarter) rnq,
36 row_number() over (partition by year order by null) rny
37 from temp
38 )
39 select month,
40 month_sales
41 case when rnq = 1 then quarter_sales end month_sales,
42 case when rny = 1 then year_sales end year_sales
43 from temp2
44 order by month;
MONTH MONTH_SALES QUARTER_SALES YEAR_SALES
---------- ---------- ----------- ----------
1 200 700 4600
2 200
3 300
4 400 1500
5 500
6 600
7 700 2400
8 800
9 900
9 rows selected.
SQL>

SQL: How to deal with NULL and PARTITION BY?

I've got a question, if you don't mind terribly.
So suppose I have this kind of a table here – Products (amount sold by quarter in 2000, only there are multiple entries for the same product and quarter (with different dates)):
product
quarter
amount sold
Jeans
1
20
Jeans
2
40
Jeans
3
60
Jeans
4
5
Skirt
1
10
Skirt
2
5
Skirt
3
30
Blouse
1
15
Blouse
2
40
Blouse
3
60
Blouse
4
15
I want to reintroduce it as follows:
product
quarter1
quarter2
quarter3
quarter4
Jeans
20
40
60
5
Skirt
10
5
30
Null
Blouse
15
40
60
15
I decided to do it with partition (cause it's not exactly that simple, there are different rows with the same quarter for the same product, but different amount sold, that's why it's sum(amount_sold), but you get the idea, I hope):
WITH quater_sales as(
SELECT DISTINCT pro.product, pro.quarter, to_char (sum(pro.amount_sold) OVER (PARTITION BY pro.product, pro.quarter)) AS quater
FROM products pro
ORDER BY pro.pro.product)
SELECT quater_sales.prod_product, quater_sales.quater AS "Q1", qu2.quater AS "Q2", qu3.quater AS "Q3", qu4.quater AS "Q4"
FROM quater_sales
JOIN quater_sales qu2 ON quater_sales.prod_subcategory=qu2.prod_subcategory
JOIN quater_sales qu3 ON quater_sales.prod_subcategory=qu3.prod_subcategory
JOIN quater_sales qu4 ON quater_sales.prod_subcategory=qu4.prod_subcategory
WHERE quater_sales.calendar_quarter_number=1 and qu2.calendar_quarter_number=2 and qu3.calendar_quarter_number=3 and qu4.calendar_quarter_number=4
The problem is with partition (or maybe it's the condition of select) that the product that was not sold in all the 4 quarters is just discarded. What I basically get in the end is this:
product
quarter1
quarter2
quarter3
quarter4
Jeans
20
40
60
5
Blouse
15
40
60
15
So how do I make "skirts" appear there too? I am a bit stuck with this.
Have you considered using a PIVOT statement?
WITH
quarter_sales (product, quarter, amount_sold)
AS
(SELECT 'Jeans', 1, 20 FROM DUAL
UNION ALL
SELECT 'Jeans', 2, 40 FROM DUAL
UNION ALL
SELECT 'Jeans', 3, 60 FROM DUAL
UNION ALL
SELECT 'Jeans', 4, 5 FROM DUAL
UNION ALL
SELECT 'Skirt', 1, 10 FROM DUAL
UNION ALL
SELECT 'Skirt', 2, 5 FROM DUAL
UNION ALL
SELECT 'Skirt', 3, 30 FROM DUAL
UNION ALL
SELECT 'Blouse', 1, 15 FROM DUAL
UNION ALL
SELECT 'Blouse', 2, 40 FROM DUAL
UNION ALL
SELECT 'Blouse', 3, 60 FROM DUAL
UNION ALL
SELECT 'Blouse', 4, 15 FROM DUAL)
SELECT *
FROM (SELECT *
FROM quarter_sales qs)
PIVOT (SUM (amount_sold)
FOR quarter
IN (1 AS quarter1, 2 AS quarter2, 3 AS quarter3, 4 AS quarter4));
PRODUCT QUARTER1 QUARTER2 QUARTER3 QUARTER4
__________ ___________ ___________ ___________ ___________
Blouse 15 40 60 15
Jeans 20 40 60 5
Skirt 10 5 30
try pivot. this is how you would pivot in tsql
declare #tmp as table(product varchar(20),quarter int,[amount sold] int);
insert into #tmp values
('Jeans', 1, 20)
,('Jeans', 2, 40)
,('Jeans', 3, 60)
,('Jeans', 4, 5)
,('Skirt', 1, 10)
,('Skirt', 2, 5)
,('Skirt', 3, 30)
,('Blouse', 1, 15)
,('Blouse', 2, 40)
,('Blouse', 3, 60)
,('Blouse', 4, 15)
select product, [1] as quarter1,[2] as quarter2,[3] as quarter3,[4] as quarter4
from
(
select product,quarter,[amount sold] from #tmp)p
pivot
(
sum([amount sold])
for quarter in([1],[2],[3],[4])
) as pvt
output:
product quarter1 quarter2 quarter3 quarter4
Blouse 15 40 60 15
Jeans 20 40 60 5
Skirt 10 5 30 NULL

SQL - How to remove from the total what has already been counted

I'm asking ur help
here this is my set
ID date_answered
---------- --------------
1 16/09/19
2 16/09/19
3 16/09/19
4 16/09/19
5 16/09/19
6 16/09/19
7 16/09/19
8 16/09/19
9 16/09/19
10 17/09/19
11 17/09/19
12 17/09/19
13 18/09/19
14 18/09/19
15 18/09/19
16 18/09/19
17 19/09/19
18 19/09/19
19 19/09/19
20 19/09/19
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
as you can see :
16/09/2019 there are 9 people who answered
17/09/2019 there are 7 people who answered
18/09/2019 there are 4 people who answered
19/09/2019 there are 4 people who answered
there are still 20 people who didnt answer
to calculate how many people answered per day, i have done :
nb_answered = count(id) over (partition by date_answered order by date_answered)
now my problem is there, i'm trying to get that :
date_answered nb_answered nb_left
--------------- -------------- --------
16/09/2019 9 40
17/09/2019 7 31(40-9)
18/09/2019 4 24(31-7)
19/09/2019 4 20(24-4)
i have tried :
count(id) over (order by date_complete rows between UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) which give me 40 (total person).
it's cool for the first date, but when i move to the second date i dont know how to have 31.
How can I do that: every day I remove from the total, the number that has already answered
Do you have any suggestion ?
Another option might be correlated subquery in SELECT statement.
Example is a little bit simplified (didn't feel like typing that much).
SQL> with test (id, da) as
2 (select 1, 16092019 from dual union all
3 select 2, 16092019 from dual union all
4 select 3, 16092019 from dual union all
5 select 4, 16092019 from dual union all
6 select 5, 16092019 from dual union all
7 --
8 select 6, 17092019 from dual union all
9 select 7, 17092019 from dual union all
10 select 8, 17092019 from dual union all
11 --
12 select 9, 19092019 from dual union all
13 --
14 select 10, null from dual union all
15 select 11, null from dual union all
16 select 12, null from dual union all
17 select 13, null from dual
18 )
19 select a.da date_answered,
20 count(a.id) nb_answered,
21 (select count(*) from test b
22 where b.da >= a.da
23 or b.da is null
24 ) nb_left
25 from test a
26 group by a.da
27 order by a.da;
DATE_ANSWERED NB_ANSWERED NB_LEFT
------------- ----------- ----------
16092019 5 13
17092019 3 8
19092019 1 5
4 4
SQL>
You want to subtract the overall count from the cumulative count:
select date_answered, count(*) as answered_on_date,
( count(*) over () -
sum(count(*)) over (order by date_answered nulls last)
) as remaining
from t
group by date_answered
order by date_answered;
If you don't want to include the current date, then subtract that as well:
select date_answered, count(*) as answered_on_date,
( count(*) over () -
sum(count(*)) over (order by date_answered nulls last) -
count(*)
) as remaining
from t
group by date_answered
order by date_answered;

How can update a column based on the value of another column in SQL?

Basically I have Product table like this:
date price
--------- -----
02-SEP-14 50
03-SEP-14 60
04-SEP-14 60
05-SEP-14 60
07-SEP-14 71
08-SEP-14 45
09-SEP-14 45
10-SEP-14 24
11-SEP-14 60
I need to update the table in this form
date price id
--------- ----- --
02-SEP-14 50 1
03-SEP-14 60 2
04-SEP-14 60 2
05-SEP-14 60 2
07-SEP-14 71 3
08-SEP-14 45 4
09-SEP-14 45 4
10-SEP-14 24 5
11-SEP-14 60 6
What I have tried:
CREATE SEQUENCE user_id_seq
START WITH 1
INCREMENT BY 1
CACHE 20;
ALTER TABLE Product
ADD (ID number);
UPDATE Product SET ID = user_id_seq.nextval;
This is updating the ID in the usual way like 1,2,3,4,5..
I have no idea how to do it using basic SQL commands. Please suggest how can I make it. Thank you in advance.
Here is one way to create a view from your base data. I assume you have more than one product (identified by product id), and that the price dates aren't necessarily consecutive. The sequence is separate for each product id. (Also, product should be the name of a different table - where the product id is primary key, and you have other information such as product name, category, etc. The table in your post would be more properly called something like price_history.)
alter session set nls_date_format='dd-MON-rr';
create table product ( prod_id number, dt date, price number );
insert into product ( prod_id, dt, price )
select 101, '02-SEP-14', 50 from dual union all
select 101, '03-SEP-14', 60 from dual union all
select 101, '04-SEP-14', 60 from dual union all
select 101, '05-SEP-14', 60 from dual union all
select 101, '07-SEP-14', 71 from dual union all
select 101, '08-SEP-14', 45 from dual union all
select 101, '09-SEP-14', 45 from dual union all
select 101, '10-SEP-14', 24 from dual union all
select 101, '11-SEP-14', 60 from dual union all
select 102, '02-SEP-14', 45 from dual union all
select 102, '04-SEP-14', 45 from dual union all
select 102, '05-SEP-14', 60 from dual union all
select 102, '06-SEP-14', 50 from dual union all
select 102, '09-SEP-14', 60 from dual
;
commit;
create view product_vw ( prod_id, dt, price, seq ) as
select prod_id, dt, price,
count(flag) over (partition by prod_id order by dt)
from ( select prod_id, dt, price,
case when price = lag(price) over (partition by prod_id order by dt)
then null else 1 end as flag
from product
)
;
Now check what the view looks like:
select * from product_vw;
PROD_ID DT PRICE SEQ
------- ------------------- ---------- ----------
101 02/09/0014 00:00:00 50 1
101 03/09/0014 00:00:00 60 2
101 04/09/0014 00:00:00 60 2
101 05/09/0014 00:00:00 60 2
101 07/09/0014 00:00:00 71 3
101 08/09/0014 00:00:00 45 4
101 09/09/0014 00:00:00 45 4
101 10/09/0014 00:00:00 24 5
101 11/09/0014 00:00:00 60 6
102 02/09/0014 00:00:00 45 1
102 04/09/0014 00:00:00 45 1
102 05/09/0014 00:00:00 60 2
102 06/09/0014 00:00:00 50 3
102 09/09/0014 00:00:00 60 4
NOTE: This answers the question that was originally asked. The OP changed the data.
If your data is not too large, you can use a correlated subquery:
update product p
set id = (select count(distinct p2.price)
from product p2
where p2.date <= p.date
);
If your data is larger, then merge is more appropriate.
WITH cts AS
(
SELECT row_number() over (partition by price order by price ) as id
,date
,price
FROM Product
)
UPDATE p
set p.id = cts.id
from product p join cts on cts.id = p.id
This is the best way by which you try to do.
There is no another simple way to do this using simple statements