Showing Different aggregate for different products - oracle

I want a code which is used to generate aggregate product by product. The product aggregate can be any like from Year to Date(YTD), Months to Date(MTD) and Quarter to Date(QTD). The user will pass the parameter on that basis the code should decide what kind of output the user wants.
If the Year is passing in the parameter than the code should generate the aggregate from the starting of the year to the sysdate.
If the Quarter No is passing in the parameter than the code should generate the aggregate from the starting of the quarter to the sysdate.
If the Month is passing in the parameter than the code should generate the aggregate from the starting of the month to the sysdate.
It means that on the basis of the parameter it should be able to decide which kind of user want from those 3. My input data is like this-
Product Table
Product_ID Product_name Price
1 Mobile 200
2 T.V. 400
3 Mixer 300
and
Sales Table-
Product_ID Sales_Date Quantity
1 01-01-2015 30
2 03-01-2015 40
3 06-02-2015 10
1 22-03-2015 30
2 09-04-2015 10
3 21-05-2015 40
1 04-06-2015 40
2 29-07-2015 30
1 31-08-2015 30
3 14-09-2015 30
And my ouput column contains 3 columns that are- Product_id, Product_Name and Total. The column Total_Amount(quantity*price) have to calculate sale on the basis of input given by user and is be something like this-
For example ,
If pro_test is the procedure then
call pro_test('YTD') -- Should Return the ProductWise YTD,
call pro_test('QTD') -- Should Return the ProductWise QTD and so on..

You are looking for a WHERE clause :-) List your conditions with OR and you are done.
select
p.product_id,
p.product_name,
coalesce(sum(s.quantity * p.price), 0) as total
from product p
left join sales s on s.product_id = p.product_id
where
(:aggregate = 'YTD' and to_char(s.sales_date, 'yyyy') = to_char(sysdate, 'yyyy'))
or
(:aggregate = 'MTD' and to_char(s.sales_date, 'yyyymm') = to_char(sysdate, 'yyyymm'))
or
(:aggregate = 'QTD' and to_char(s.sales_date, 'yyyyq') = to_char(sysdate, 'yyyyq'))
group by p.product_id, p.product_name;
EDIT: Here is how the corresponding PL/SQL function would look like:
create or replace function matches_date_aggregate(in_sales_date date, in_aggregate char)
return integer as
begin
if (in_aggregate = 'YTD' and to_char(in_sales_date, 'yyyy') = to_char(sysdate, 'yyyy'))
or (in_aggregate = 'MTD' and to_char(in_sales_date, 'yyyymm') = to_char(sysdate, 'yyyymm'))
or (in_aggregate = 'QTD' and to_char(in_sales_date, 'yyyyq') = to_char(sysdate, 'yyyyq')) then
return 1;
else
return 0;
end if;
end matches_date_aggregate;
Your query's WHERE clause would become:
where matches_date_aggregate(s.sales_date, :aggregate) = 1
The function cannot return BOOLEAN unfortunately, for even though Oracle's PL/SQL knows the BOOLEAN data type, Oracle SQL doesn't.

Related

Pivoting a table in SQL, where the new columns are based on every possible combination of values from two columns

To illustrate, the following table:
ID
Model
Series
Amount
001
productX
SeriesZ
1000
001
productX
SeriesABC
2000
001
productX
SeriesABC
8000
002
productY
SeriesABC
5000
should be transformed such that each record captures a unique id and the total amount it has contributed to each model-series possible combination.
ID
productX_SeriesZ
productX_SeriesABC
productY_SeriesABC
001
1000
10000
0
002
0
0
5000
Can I use the pivot function to pivot on for each possible combination of values in two columns?
SELECT ID,
SUM( CASE WHEN model = 'productX' and series = 'SeriesZ' THEN amount ELSE 0 END) productX_SeriesZ,
SUM( CASE WHEN model = 'productX' and series = 'SeriesABC' THEN amount ELSE 0 END) productX_SeriesABC,
SUM( CASE WHEN model = 'productY' and series = 'SeriesABC' THEN amount ELSE 0 END) productX_SeriesABC
FROM TABLE
GROUP BY ID;
EDIT:
"Works for this particular case, but what if we have hundreds of models and series?"
You can try this.
DECLARE
TYPE model_rec IS RECORD ( mr mytable.model%type);
TYPE series_rec IS RECORD ( sr mytable.series%type);
TYPE model_tab IS TABLE OF model_rec;
TYPE series_tab IS TABLE OF series_rec;
mv model_tab;
sv serie_tab;
query varchar2(32767) := 'SELECT ID';
BEGIN
SELECT DISTINCT model, series INTO mv, sv FROM mytable WHERE model IS NOT NULL AND series IS NOT NULL;
FOR i IN 1..mv.COUNT
LOOP
query := query||', SUM( CASE WHEN model = '|| DBMS_ASSERT.ENQUOTE(mv(i).mr)
|| ' and series = ' || DBMS_ASSERT.ENQUOTE(sv(i).sr)||' THEN amount ELSE 0 END) ' || mv(i).mr||'_'||sv(i).sr||' '
END LOOP;
query := query || ' FROM mytable GROUP BY id;'
EXECUTE IMMEDIATE query;
END;
This may contain syntactic errors and can be optimized and refactored but this is the basic idea. I am in hurry so wrote it down without testing.
I think below query should work for you -
SELECT *
FROM (SELECT ID
,Model
,Series
FROM YOUR_TABLE)
PIVOT(SUM(AMOUNT) FOR Model IN ('productX' productx, 'productY' producty),
SUM(AMOUNT) FOR Series IN ('SeriesABC' Seriesabc, 'SeriesZ' Seriesz))

Referancing value from select column in where clause : Oracle

My tables are as below
MS_ISM_ISSUE
ISSUE_ID ISSUE_DUE_DATE ISSUE_SOURCE_TYPE
I1 25-11-2018 1
I2 25-12-2018 1
I3 27-03-2019 2
MS_ISM_SOURCE_SETUP
SOURCE_ID MODULE_NAME
1 IT-Compliance
2 Risk Assessment
I have written following query.
with rs as
(select
count(ISSUE_ID) as ISSUE_COUNT, src.MODULE_NAME,
case
when ISSUE_DUE_DATE<sysdate then 'Overdue'
when ISSUE_DUE_DATE between sysdate and sysdate + 90 then 'Within 3 months'
when ISSUE_DUE_DATE>sysdate+90 then 'Beyond 90 days'
end as date_range
from MS_ISM_ISSUE issue, MS_ISM_SOURCE_SETUP src
where issue.Issue_source_type = src.source_id
group by src.MODULE_NAME, case
when ISSUE_DUE_DATE<sysdate then 'Overdue'
when ISSUE_DUE_DATE between sysdate and sysdate + 90 then 'Within 3 months'
when ISSUE_DUE_DATE>sysdate+90 then 'Beyond 90 days'
end)
select ISSUE_COUNT,MODULE_NAME, DATE_RANGE,
(select count(ISSUE_COUNT) from rs where rs.MODULE_NAME=MODULE_NAME) as total from rs;
The output of the code is as below.
ISSUE_COUNT MODULE_NAME DATE_RANGE Total
1 IT-Compliance Overdue 3
1 IT-Compliance Within 3 months 3
1 Risk Assessment Beyond 90 days 3
The result is correct till 3rd column. In 4th column what I want is, total of Issue count for given module name. Hence in above case Total column will have value as 2 for first and second row (since there are 2 Issues for IT-Compliance) and value 1 for the third row (since one issue is present for Risk Assessment).
Essentially, I want to achieve is to replace current row's MODULE_NAME in last where clause. How do I achieve this using query?
OK, this condition
where rs.MODULE_NAME=MODULE_NAME
is essentially the same as if you wrote
where MODULE_NAME = MODULE_NAME
which is simply always true (if there are no nulls in module_name).
Try using different table alias for inner query and outer query, e.g.
select count(ISSUE_COUNT) from rs rs2 where rs2.MODULE_NAME=rs.MODULE_NAME
You can also try to use analytic function here, something like
select ISSUE_COUNT,
MODULE_NAME,
DATE_RANGE,
COUNT(ISSUE_COUNT) OVER (PARTITION BY RS.MODULE_NAME) AS TOTAL
from rs
instead of your subquery

Create a PL/SQL procedure that calculate total monthly income. Total must be printed by month

Write a procedure that calculates and displays total income from all sources of all hotels. Totals must be printed by month, and for each month by event and service type. Include discounts.( 10% discount if the reservation date is 2 month before reservation start date).
The tables are:
Hotel Table has:
Hotel_id, hotel_name, Hotel_city, Hotel_state, Hotel_zip,Hotel_phone
Reservation Table has:
Reservation_id, Hotel_id, Room_num, Service_id, Guest_name, Reservation_date, Reservation_start_date, Reservation_end_date, cancelation_date, Num_of_guest, event_type
Room Table has:
Room_num, Hotel_id, Room_type, Room_capacity, Room_cost
service table has:
service_id, Service_type, Service_cost
This is what I tried, but I want to write it in a procedure form; how do I do that? Please help. Thanks
select month (Reservation_end_date) as , event_type,
sum(case when days>= 2 then cost- (cost/100)* 10
else cost) as total_cost)
((select distinct reservation.hotel_id,reservation_date, reservation_start_date,
reservation_end_date, event_type, room.room_type as R_type ,room_cost as R_cost,
months_between(reservation_start_date,reservation_date)as months
from reservation, room
where reservation.hotel_id = room.hotel_id;)
union
(select hotel_name, reservation_date, reservation_start_date,
reservation_end_date, event_type, services_type, services_cost as cost,
months_between(reservation_start_date,reservation_date)as month
from reservation,service, hotel
where reservation.services_id = service.services_id
and reservation.hotel_id = hotel.hotel_id;))
group by month(reservation_end_date),event_type;
The first step is to get the base query right.
To consolidate a set of dates into their common month use trunc(date_col, 'mm'). Presumably room costs and service costs should be calculated on a per night basis.
To calculate the number of nights simply subtract the start date from the end date.
This query should produce the correct result (your stated business rules are incomplete so it's hard to be certain). Like your posted code it has subqueries to calculate the cost of each room reservation and each service reservation. These are aggregated in the outer query:
select to_char(r_month, 'YYYY-MON') as rpt_month
, event_type
, service_type
, sum ( (r_cost - r_discount ) * no_of_nights ) as tot_cost
from (
select trunc(r.reservation_end_date , 'mm') as r_month
, r.event_type
, cast(null as varchar2(10)) as service_type
, rm.room_cost as r_cost
, case when months_between (r.reservation_start_date, r.reservation_date) >= 2
then rm.room_cost * 0.1
else 0 end as r_discount
, (r.reservation_end_date - r.reservation_start_date ) as no_of_nights
from reservation r
join room rm
on ( r.room_num = rm.room_num
and r.hotel_id = rm.hotel_id )
union all
select trunc(r.reservation_end_date , 'mon') as r_month
, r.event_type
, sv.service_type
, sv.service_cost as r_cost
, case when months_between (r.reservation_start_date, r.reservation_date) >= 2
then sv.service_cost * 0.1
else 0 end as r_discount
, (r.reservation_end_date - r.reservation_start_date ) as no_of_nights
from reservation r
join service sv
on ( r.service_id = sv.service_id )
)
group by r_month
, event_type
, service_type
order by r_month
, event_type
, service_type
;
The second step is put this into a procedure. Again your requirements are fuzzy: should the procedure take any parameters? what format should the output be in? As the business domain (hotel bookings) and the format of the question ("write a procedure that ...") this appears to be a homework assignment so here is the simplest interpretation of "display". It uses dbms_output routines to print to the screen, and rpad() and lpad() to give a nice layout (obviously the spacings may be wonky, because you haven't provide the datatypes of the various columns) .
create or replace procedure display_monthly_reservations as
begin
<< heading >>
dbms_output.put(rpad('MONTH', 8));
dbms_output.put( rpad('EVENT_TYPE' , 20 ) || ' ');
dbms_output.put( rpad('SERVICE_TYPE', 20 ) || ' ');
dbms_output.put_line('TOTAL_COST');
<< per_line >>
for r in (
<< insert the query here >>
) loop
dbms_output.put(r.rpt_month || ' ');
dbms_output.put( rpad(r.event_type , 20 ) || ' ');
dbms_output.put( rpad(r.service_type , 20 ) || ' ');
dbms_output.put_line( lpad(to_char(r.tot_cost , '9999999.99'), 10 ) );
end loop per_line;
end display_monthly_reservations;
/

modifying query resultset to include all dates in a range

I have a query which I run on a table TXN_DEC(id, resourceid, usersid, date, eventdesc) which return distinct count of users for a given date-range and resourceid, group by date and eventdesc (each resource can have 4 to 5 eventdesc)
if there is no value of distinct users count on a date in the range, for an eventdesc, then it skips that date row in the resultset.
I need to have all date rows in my resultset or collection such that if there is no value of count for a date,eventdesc combination, then its value is set to 0 but that date still exists in the collection..
How do I go about getting such a collection
I know getting the final dataset entirely from the query result would be too complicated,
but I can use collections in groovy to modify and populate my map/list to get the data in the required format
something similar to following: if
input date range = 5th Feb to 3 March 2011
DataMap = [dateval: '02/05/2011' eventdesc: 'Read' dist_ucnt: 23,
dateval: '02/06/2011' eventdesc: 'Read' dist_ucnt: 23,
dateval: '02/07/2011' eventdesc: 'Read' dist_ucnt: 0, -> this row was not present in query resultset, but row exists in the map with value 0
....and so on till 3 march 2011 and then whole range repeated for each eventdesc
]
If you want all dates (including those with no entries in your TXN_DEC table) for a given range, you could use Oracle to generate your date range and then use an outer join to your existing query. Then you would just need to fill in null values. Something like:
select
d.dateInRange as dateval,
'Read' as eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
to_date('02-FEB-2011','dd-mon-yyyy') + rownum - 1 as dateInRange
from all_objects
where rownum <= to_date('03-MAR-2011','dd-mon-yyyy') - to_date('02-FEB-2011','dd-mon-yyyy') + 1
) d
left join (
select
date,
count(distinct usersid) as dist_ucnt
from
txn_dec
where eventDesc = 'Read'
group by date
) td on td.date = d.dateInRange
That's my purely Oracle solution since I'm not a Groovy guy (well, actually, I am a pretty groovy guy...)
EDIT: Here's the same version wrapped in a stored procedure. It should be easy to call if you know the API....
create or replace procedure getDateRange (
p_begin_date IN DATE,
p_end_date IN DATE,
p_event IN txn_dec.eventDesc%TYPE,
p_recordset OUT SYS_REFCURSOR)
AS
BEGIN
OPEN p_recordset FOR
select
d.dateInRange as dateval,
p_event as eventdesc,
nvl(td.dist_ucnt, 0) as dist_ucnt
from (
select
p_begin_date + rownum - 1 as dateInRange
from all_objects
where rownum <= p_end_date - p_begin_date + 1
) d
left join (
select
date,
count(distinct usersid) as dist_ucnt
from
txn_dec
where eventDesc = p_event
group by date
) td on td.date = d.dateInRange;
END getDateRange;

Create View with 365 days

How to Create a View with all days in year. view should fill with dates from JAN-01 to Dec-31. How can I do this in Oracle ?
If current year have 365 days,view should have 365 rows with dates. if current year have 366 days,view should have 366 rows with dates. I want the view to have a single column of type DATE.
This simple view will do it:
create or replace view year_days as
select trunc(sysdate, 'YYYY') + (level-1) as the_day
from dual
connect by level <= to_number(to_char(last_day(add_months(trunc(sysdate, 'YYYY'),11)), 'DDD'))
/
Like this:
SQL> select * from year_days;
THE_DAY
---------
01-JAN-11
02-JAN-11
03-JAN-11
04-JAN-11
05-JAN-11
06-JAN-11
07-JAN-11
08-JAN-11
09-JAN-11
10-JAN-11
11-JAN-11
...
20-DEC-11
21-DEC-11
22-DEC-11
23-DEC-11
24-DEC-11
25-DEC-11
26-DEC-11
27-DEC-11
28-DEC-11
29-DEC-11
30-DEC-11
31-DEC-11
365 rows selected.
SQL>
The date is generated by applying several Oracle date functions:
trunc(sysdate, 'yyyy') gives us the first of January for the current year
add_months(x, 11) gives us the first of December
last_day(x) gives us the thirty-first of December
to_char(x, 'DDD') gives us the number of the thirty-first of December, 365 this year and 366 next.
This last figure provides the upper bound for the row generator CONNECT BY LEVEL <= X
you can use piplined table, it should be something like this:
create or replace type year_date_typ as object (v_day date);
create or replace type year_date_tab as table of year_date_typ;
CREATE OR REPLACE FUNCTION get_dates(year IN VARCHAR2) RETURN year_date_tab PIPELINED IS
v_start_date date := to_date('0101' || year, 'ddmmyyyy');
res year_date_typ := year_date_typ(null);
v_days_in_year integer := 365;
BEGIN
if to_char(last_day(to_date('0102'||year, 'ddmmyyyy')), 'dd') = '29' then
v_days_in_year := 366;
end if;
FOR i in 0 .. v_days_in_year integer-1 LOOP
res.v_day := v_start_date + i;
pipe row(res);
END LOOP;
return;
END get_dates;
and you can use it:
select * from table(get_dates('2011'));
This works well in MS SQL
SELECT TOP (DATEDIFF(day, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0), DATEADD(yy, DATEDIFF(yy,0,getdate()) + 1, -1))) n = ROW_NUMBER() OVER (ORDER BY [object_id]),
dateadd(day, ROW_NUMBER() OVER (ORDER BY [object_id]) - 1, DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) AS AsOfDate FROM sys.all_objects

Resources