How to make a time dependent distribution in SQL? - sql

I have an SQL Table in which I keep project information coming from primavera.
Suppose that i have columns for Start Date,End Date,Duration, and Total Qty as shown below .
How can i distribute Total Qty over Months using these information. What kind of additional columns, sql queries i need in order to get correct monthly distribution?
Thanks in Advance.
Columns in order:
itemname,quantity,startdate,duration,enddate
item1 -- 108 -- 2013-03-25 -- 720 -- 2013-07-26
item2 -- 640 -- 2013-03-25 -- 720 -- 2013-07-26
.
.

I think the key is to break the records apart by month. Here is an example of how to do it:
with months as (
select 1 as mon union all select 2 union all select 3 union all
select 4 as mon union all select 5 union all select 6 union all
select 7 as mon union all select 8 union all select 9 union all
select 10 as mon union all select 11 union all select 12
)
select item, m.mon, quantity / nummonths
from (select t.*, (month(enddate) - month(startdate) + 1) as nummonths
from t
) t join
months m
on month(t.startDate) <= m.mon and
months(t.endDate) >= m.mon;
This works because all the months are within the same year -- as in your example. You are quite vague on how the split should be calculated. So, I assumed that every month from the start to the end gets an equal amount.

Related

Oracle: Get latest value from a group by query, among other aggregations

I have a group by query returning avg and max from a set of records. I need to return a new column with the latest value of a column("records") based on another column ("dates").
This query
with x as (select 'A' process, 10 records, sysdate-5 dates from dual union all
select 'A' process, 20 records, sysdate-4 dates from dual union all
select 'A' process, 30 records, sysdate-3 dates from dual union all
select 'B' process, 25 records, sysdate-2 dates from dual union all
select 'B' process, 15 records, sysdate-1 dates from dual)
select process,
avg(records) avgu,
max(records) maxu
from x
group by process
order by 1
returns:
Process
AVG.
MAX.
A.
20
30.
B
20
25.
I need a new column (LATEST) with latest value of records based on dates, keeping the old columns too:
Process
MAX.
LATEST.
A.
30
30.
B
25
15.
I'm playing with some window functions like RANK OVER PARTITION but I can't get the desired outcome in a single query.
Thank you in advance for any idea.
Here's one option:
Sample data:
SQL> with x as (
2 select 'A' process,10 records,sysdate-5 dates from dual union all
3 select 'A',20,sysdate-4 from dual union all
4 select 'A',30,sysdate-3 from dual union all
5 select 'B',25,sysdate-2 from dual union all
6 select 'B',15,sysdate-1 from dual),
Query begins here: first find the latest value per each process, then - in the final query - aggregate required values.
7 temp as
8 (select process,
9 records,
10 dates,
11 first_value(records) over (partition by process order by dates desc) latest
12 from x
13 )
14 select process,
15 avg(records) avgu,
16 max(records) maxu,
17 max(latest) latest
18 from temp
19 group by process
20 order by 1;
P AVGU MAXU LATEST
- ---------- ---------- ----------
A 20 30 30
B 20 25 15
SQL>

How to include values that count nothing on certain day (APEX)

I have this query:
SELECT
COUNT(ID) AS FREQ,
TO_CHAR(TRUNC(CREATED_AT),'DD-MON') DATES
FROM TICKETS
WHERE TRUNC(CREATED_AT) > TRUNC(SYSDATE) - 32
GROUP BY TRUNC(CREATED_AT)
ORDER BY TRUNC(CREATED_AT) ASC
This counts how many tickets where created every day for the past month.
The result looks something like this: (first 10 rows)
FREQ DATES
3 28-DEC
4 04-JAN
8 05-JAN
1 06-JAN
4 07-JAN
5 08-JAN
2 11-JAN
6 12-JAN
3 13-JAN
8 14-JAN
The linechart that I created looks like this:
The problem is that the days where tickets are not created (in particular the weekends) the line just goes straight to the day where there is created a ticket.
Is there a way in APEX or in my query to include the days that aren't counted?
As commented, using one of row generator techniques you'd create a "calendar" table and outer join it with a table that contains data you're displaying.
Something like this (see comments within code):
SQL> with yours (amount, datum) as
2 -- your sample table
3 (select 100, date '2021-01-01' from dual union all
4 select 200, date '2021-01-03' from dual union all
5 select 300, date '2021-01-07' from dual
6 ),
7 minimax as
8 -- MIN and MAX date (so that they could be used in row generator --> CALENDAR CTE (below)
9 (select min(datum) min_datum,
10 max(datum) max_datum
11 from yours
12 ),
13 calendar as
14 -- calendar, from MIN to MAX date in YOUR table
15 (select min_datum + level - 1 datum
16 from minimax
17 connect by level <= max_datum - min_datum + 1
18 )
19 -- final query uses outer join
20 select c.datum,
21 nvl(y.amount, 0) amount
22 from calendar c left join yours y on y.datum = c.datum
23 order by c.datum;
DATUM AMOUNT
---------- ----------
01.01.2021 100
02.01.2021 0
03.01.2021 200
04.01.2021 0
05.01.2021 0
06.01.2021 0
07.01.2021 300
7 rows selected.
SQL>
Applied to your current query:
WITH
minimax
AS
-- MIN and MAX date (so that they could be used in row generator --> CALENDAR CTE (below)
(SELECT MIN (created_at) min_datum, MAX (created_at) max_datum
FROM tickets),
calendar
AS
-- calendar, from MIN to MAX date in YOUR table
( SELECT min_datum + LEVEL - 1 datum
FROM minimax
CONNECT BY LEVEL <= max_datum - min_datum + 1)
-- final query uses outer join
SELECT COUNT (t.id) AS freq, TO_CHAR (TRUNC (c.datum), 'DD-MON') dates
FROM calendar c LEFT JOIN tickets t ON t.created_at = c.datum
WHERE TRUNC (t.created_at) > TRUNC (SYSDATE) - 32
GROUP BY TRUNC (c.datum)
ORDER BY dates ASC
I added a with clause to generate last 31 days, then I left joined with your base table like below.
with last_31_days as (
select trunc(sysdate) - 32 + level dt from dual connect by trunc(sysdate) - 32 + level < trunc(sysdate)
)
SELECT
nvl(COUNT(t.ID), 0) AS FREQ,
TO_CHAR(
nvl(TRUNC(t.CREATED_AT), a.dt)
,'DD-MON') DATES
FROM last_31_days a
LEFT JOIN TICKETS t
ON TRUNC(t.CREATED_AT) = a.dt
GROUP BY nvl(TRUNC(t.CREATED_AT), a.dt)
ORDER BY 2 ASC
;
#Littlefoot answer is perfect. but here is a cheeky way to get the similar table with format match OP output. using a simple cte for this.
WITH cte AS (
SELECT To_Char(Trunc(SYSDATE - ROWNUM),'DD-MON') dtcol
FROM DUAL
CONNECT BY ROWNUM < 366
)
SELECT * FROM cte
here is db<>fiddle
and then you can simply join this cte to fill up empty date. as the origin output column date looks like a string column.
connect by is for oracle only. but I think you can still use recursive cte to get similar result in other DBMS support recursive cte.

Oracle SQL recursive adding values

I have the following data in the table
Period Total_amount R_total
01/01/20 2 2
01/02/20 5 null
01/03/20 3 null
01/04/20 8 null
01/05/20 31 null
Based on the above data I would like to have the following situation.
Period Total_amount R_total
01/01/20 2 2
01/02/20 5 3
01/03/20 3 0
01/04/20 8 8
01/05/20 31 23
Additional data
01/06/20 21 0 (previously it would be -2)
01/07/20 25 25
01/08/20 29 4
Pattern to the additional data is:
if total_amount < previous(r_total) then 0
Based on the filled data, we can spot the pattern is:
R_total = total_amount - previous(R_total)
Could you please help me out with this issue?
As Gordon Linoff suspected, it is possible to solve this problem with analytic functions. The benefit is that the query will likely be much faster. The price to pay for that benefit is that you need to do a bit of math beforehand (before ever thinking about "programming" and "computers").
A bit of elementary arithmetic shows that R_TOTAL is an alternating sum of TOTAL_AMOUNT. This can be arranged easily by using ROW_NUMBER() (to get the signs) and then an analytic SUM(), as shown below.
Table setup:
create table sample_data (period, total_amount) as
select to_date('01/01/20', 'mm/dd/rr'), 2 from dual union all
select to_date('01/02/20', 'mm/dd/rr'), 5 from dual union all
select to_date('01/03/20', 'mm/dd/rr'), 3 from dual union all
select to_date('01/04/20', 'mm/dd/rr'), 8 from dual union all
select to_date('01/05/20', 'mm/dd/rr'), 31 from dual
;
Query and result:
with
prep (period, total_amount, sgn) as (
select period, total_amount,
case mod(row_number() over (order by period), 2) when 0 then 1 else -1 end
from sample_data
)
select period, total_amount,
sgn * sum(sgn * total_amount) over (order by period) as r_total
from prep
;
PERIOD TOTAL_AMOUNT R_TOTAL
-------- ------------ ----------
01/01/20 2 2
01/02/20 5 3
01/03/20 3 0
01/04/20 8 8
01/05/20 31 23
This may be possible with window functions, but the simplest method is probably a recursive CTE:
with t as (
select t.*, row_number() over (order by period) as seqnum
from yourtable t
),
cte(period, total_amount, r_amount, seqnum) as (
select period, total_amount, r_amount, seqnum
from t
where seqnum = 1
union all
select t.period, t.total_amount, t.total_amount - cte.r_amount, t.seqnum
from cte join
t
on t.seqnum = cte.seqnum + 1
)
select *
from cte;
This question explicitly talks about "recursively" adding values. If you want to solve this using another mechanism, you might explain the logic in detail and ask if there is a non-recursive CTE solution.

How to show total profit for each month, show null when there is no record in that month in oracle

i am producing a report to show the total profit of each month IN 2018, and show NIL when there is no profit earned in certain months
the profit earned = 0.1 * Total_payment.
The PROFIT is earned when the service is done, the column "Total_payment" is come from the table BOOKING, i have to join BOOKING and SERVICE in order to get the total profit of each month, Booking_num is the key for joining BOOKING and SERVICE, Actual_end is the end date of the service
Now the problem is there is no profit earned in jan,feb and aug.
is there anyway to show NIL in profit column FOR THIS THREE MONTH?
SELECT EXTRACT(MONTH FROM Actual_end) AS MONTH,SUM(Total_payment *0.1) AS PROFIT
FROM SERVICE,BOOKING
WHERE SERVICE.Booking_num = BOOKING.Booking_num
AND EXTRACT(YEAR FROM Actual_end) = 2018
GROUP BY EXTRACT(MONTH FROM Actual_end);
This is the code of showing profit for 9 months,without jan,feb and aug
MONTH PROFIT
3 88.4
4 146.1
5 112.6
6 108.3
7 102.6
9 130.3
10 72.6
12 124.9
I expect the output to be
MONTH PROFIT
1 NIL
2 NIL
3 88.4
4 146.1
5 112.6
6 108.3
7 102.6
8 NIL
9 130.3
10 72.6
11 124.9
12 25.2
how do i modify it, i have also tried
WITH CALENDAR AS(
SELECT TO_CHAR(add_months(date '2018-01-01',ROWNUM -1),'MM') AS MONTH
FROM DUAL
CONNECT BY LEVEL <=12)
SELECT CALENDER.MONTH, NVL(SUM(Total_payment*0.1),null) AS PROFIT
FROM BOOKING,SERVICE,CALENDER
WHERE BOOKING.Booking_num = SERVICE.Booking_num
AND CALENDER.MONTH = EXTRACT(MONTH FROM Actual_end(+))
AND EXTRACT(MONTH FROM Actual_end) = 2018
GROUP BY CALENDER.MONTH
THE OUTPUT:
NO ROWS SELECTED
You need and outer join(left or right). Btw, get rid of old-fashioned comma seperated join among tables, rather, use explicit join.
Add RIGHT JOIN (SELECT LEVEL AS MNT FROM DUAL CONNECT BY LEVEL <= 12 ) MNT to your query if returning one certain year with whole months is the matter :
SELECT MNT AS MONTH,NVL(TO_CHAR(SUM(Total_payment *0.1)),'NIL') AS PROFIT
FROM SERVICE S
JOIN BOOKING B
ON S.Booking_num = B.Booking_num
RIGHT JOIN (SELECT LEVEL AS MNT
FROM DUAL
CONNECT BY LEVEL <= 12 ) MNT
ON MNT.MNT = EXTRACT(MONTH FROM Actual_end)
AND EXTRACT(YEAR FROM Actual_end)=2018
GROUP BY MNT
ORDER BY MONTH;
Demo
In this case you need a list of all months:
with months as (
select 1 as month from dual union all
select 2 as month from dual union all
select 3 as month from dual union all
select 4 as month from dual union all
select 5 as month from dual union all
select 6 as month from dual union all
select 7 as month from dual union all
select 8 as month from dual union all
select 9 as month from dual union all
select 10 as month from dual union all
select 11 as month from dual union all
select 12 as month from dual
)
select m.month, sum(s.total_payment * 0.1) as profit
from months m left join
booking b
on extract(month from b.actual_end) = m.month and
b.actual_end >= date '2018-01-01' and
b.actual_end < date '2019-01-01' left join
service s join
on s.booking_num = b.booking_num
group by m.month;
Note:
This is guessing that actual_end is in booking and total_payment is in service. The query would be slightly different if this guess is not correct.
Never use commas in the FROM clause.
This query should use LEFT JOIN. The first table has all the rows that you want.
Filters on subsequent tables go in the on clause, not the where clause.
Note the use of date constants. Such comparisons usually make it easier for the engine to optimize the query (typically by using indexes).

Incremental business day column that resets each month

I need to create a table that contains records with 1) all 365 days of the year and 2) a counter representing which business day of the month the day is. Non-business days should be represented with a 0. For example:
Date | Business Day
2019-10-01 1
2019-10-02 2
2019-10-03 3
2019-10-04 4
2019-10-05 0 // Saturday
2019-10-06 0 // Sunday
2019-10-07 5
....
2019-11-01 1
2019-11-02 0 // Saturday
2019-11-03 0 // Sunday
2019-11-04 2
So far, I've been able to create a table that contains all dates of the year.
CREATE TABLE ${TMPID}_days_of_the_year
(
`theDate` STRING
);
INSERT OVERWRITE TABLE ${TMPID}_days_of_the_year
select
dt_set.theDate
from
(
-- last 0~99 months
select date_sub('2019-12-31', a.s + 10*b.s + 100*c.s) as theDate
from
(
select 0 as s 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
) a
cross join
(
select 0 as s 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
) b
cross join
(
select 0 as s union all select 1 union all select 2 union all select 3
) c
) dt_set
where dt_set.theDate between '2019-01-01' and '2019-12-31'
order by dt_set.theDate DESC;
And I also have a table that contains all of the weekend days and holidays (this data is loaded from a file, and the date format is YYYY-MM-DD)
CREATE TABLE ${TMPID}_company_holiday
(
`holidayDate` STRING
)
;
LOAD DATA LOCAL INPATH '${FILE}' INTO TABLE ${TMPID}_company_holiday;
My question is.... how do I join these tables together while creating the business day counter column shown as in the sample data above?
You can use row_number() for the enumeration. This is a little tricky, because it needs to be conditional, but the information you need is provided by a left join:
select dy.*,
(case when ch.holiday_date is null
then row_number() over (partition by trunc(dy.date, 'MONTH'), ch.holiday_date
order by dy.date
)
else 0
end) as business_day
from days_of_the_year dy left join
company_holiday ch
on dy.date = ch.holiday_date;