Sum values by date range in multiple columns - oracle

I need to sum value by date range in multiple columns. Every date range is one week of a month. It can be shorter than 7 days if it is the start of the month or the end of the month.
For example, I have dates for February:
my_user my_date my_value
A 01.02.2019 100
A 02.02.2019 150
B 01.02.2019 90
Z 28.02.2019 120
How can I have in date range format such as below?
my_user 01/02-03/02 04/02-10/02 11/02-17/02 18/02-24/02 25/02-28/02
A 250 0 0 0 0
B 90 0 0 0 0
Z 0 0 0 0 120
Any suggestions? Thanks!

You can do this:
select *
from (
select to_char(dt, 'iw') - to_char(trunc(dt, 'month'), 'iw') + 1 wk, usr, val from t)
pivot (sum(val) for wk in (1, 2, 3, 4, 5, 6))
demo
USR 1 2 3 4 5 6
--- ---------- ---------- ---------- ---------- ---------- ----------
A 250
B 90
Z 120
Header numbers are the weeks of month. Maximum may be 6 if month starts at the end of the week and is longer than 28 days.
Similiar way you can find first and last day of each week if needed, but you can't put them as headers, or at least not easily.
Edit:
it is possible to define certain date range with pivot, simple as two
dates? For example, I need to sum values from 5 December 2018 to 4
January 2019, 5 January 2019 to 4 February 2019, 5 March 2019 to 4
April 2019
Yes. Everything depends on how we count first and next weeks. Here:
to_char(dt, 'iw') - to_char(trunc(dt, 'month'), 'iw') + 1
i am subtracting week in year for given date and week in year of first day in month for this date. You can simply replace this second value with your starting date, either by hardcoding it in your query or by sending parameter to query or finding minimum date at first in a subquery:
(to_char(dt, 'iw') - to_char(date '2019-03-05', 'iw')) + 1
or
(to_char(dt, 'iw') - to_char((select min(dt) from data), 'iw')) + 1
Edit 2:
There is one problem however. When user defined period contains two or more years. to_date(..., 'iw') works fine for one year, but for two we get values 51, 52, 01, 02... We have to deal with this somehow, for instance like here:
with t(dt1, dt2) as (select date '2018-12-16', date '2019-01-15' from dual)
select min(dt) mnd, max(dt) mxd, iw, row_number() over (order by min(dt)) rn
from (select dt1 + level - 1 dt, to_char(dt1 + level - 1, 'iw') iw
from t connect by level -1 <= dt2 - dt1)
group by iw
which gives us:
MND MXD IW RN
----------- ----------- -- ----------
2018-12-16 2018-12-16 50 1
2018-12-17 2018-12-23 51 2
2018-12-24 2018-12-30 52 3
2018-12-31 2019-01-06 01 4
2019-01-07 2019-01-13 02 5
2019-01-14 2019-01-15 03 6
In first line we have user defined date ranges. Then I did hierarchical query looping through all dates in range assigning week, then grouped by this week, found start and end dates for each week and assigned row number rn which can be further used by pivot.
You can now simply join your input data with this query, let's name it weeks:
from data join weeks on dt between mnd and mxd
and make pivot. But for longer periods you have to find how many weeks there can be and specify them in pivot clause in (1, 2, 3, 4...). You can also add aliases if you need:
pivot ... for rn in (1 week01, 2 week02... 12 week12)
There is no simply way to avoid listing them manually. If you need it please look for oracle dynamic pivot in SO, there where hundreds similiar questions already. ;-)

Related

Oracle rounding down sysdate to the nearest minute point that divisible by 30

I have to convert sysdate by rounding down to nearest minute point that divisible by 30. For example:
If sysdate is between 2020-10-14 09:00:00 and 2020-10-14 09:29:59 then return 2020-10-14 09:00:00
If sysdate is between 2020-10-14 09:30:00 and 2020-10-14 09:59:59 then return 2020-10-14 09:30:00
How can I get my expected result in Oracle?
The minutes logic here
get the minutes
divide by 30 and truncate (which gives 0 or 1)
*30/1440 to get 0 or 30minutes of a day
and then add to the hour of day
SQL> with d as
2 ( select to_date('09:27','HH:MI') x from dual
3 union all
4 select to_date('09:37','HH:MI') x from dual
5 )
6 select x, trunc(x,'HH') + 30*trunc(to_number(to_char(x,'MI'))/30)/1440
7 from d;
X TRUNC(X,'HH')+30*TR
------------------- -------------------
01/10/2020 09:27:00 01/10/2020 09:00:00
01/10/2020 09:37:00 01/10/2020 09:30:00

PL/SQL Oracle 11g Looping

I am having trouble solve. I am suppose to be getting a record every time there is a change to an account in our data warehouse, but I am only receiving one. The table below is a sample of what I am working with.
Row Acct1 Acct2 Date Total_Reissued Reissue_Per_Day
1 A 1 1/1/2016 2 2
2 A 1 1/2/2016 3 1
3 A 1 1/3/2016 5 2
4 A 1 1/4/2016 6 1
1 B 3 1/1/2016 1 1
2 B 3 1/2/2016 2 1
1 B 4 1/1/2016 1 1
2 B 4 1/2/2016 2 1
The Reissued Column is a running total. For Acct A on 1/1/2016 there were 2 reissues, then On 1/2/2016 there was 1 more making a total of 3. My problem is calculating the actual number of reissues per day.
You can use the lag() function to peek back at the previous row; assuming that 'previous' is the last date you saw for the acct1/acct2 combination you can do:
select row_number() over (partition by acct1, acct2 order by dt) as row_num,
acct1, acct2, dt, total_reissued,
total_reissued - nvl(lag(total_reissued)
over (partition by acct1, acct2 order by dt), 0) as reissue_per_day
from your_table;
ROW_NUM A ACCT2 DT TOTAL_REISSUED REISSUE_PER_DAY
---------- - ---------- ---------- -------------- ---------------
1 A 1 2016-01-01 2 2
2 A 1 2016-01-02 3 1
3 A 1 2016-01-03 5 2
4 A 1 2016-01-04 6 1
1 B 3 2016-01-01 1 1
2 B 3 2016-01-02 2 1
1 B 4 2016-01-01 1 1
2 B 4 2016-01-02 2 1
I'm not sure if your 'row' column actually exists, or is required, or was just to illustrate your data. I've generated it anyway, in case you need it.
The main bit of interest is:
lag(total_reissued) over (partition by acct1, acct2 order by dt)
which finds the previous date's value (using dt as a column name, since date isn't a valid name). That then has an nvl() wrapper so the first row sees a dummy value of zero instead of null. And then that is subtracted from the current row's value to get the difference.

Split 2 db rows into 3 by date ranges

I have a problem I can't solve. I have A and B money which I can spend in a defined period. These are the following two rows in the DB (with begin_date, end_date and amount columns):
A: 2015.01.01.-2015.09.30. 10.000$
B: 2015.07.01.-2015.12.31. 7.000$
So these dates are overlapped, and it means I can spend more money between 2017.07.01. and 2015.09.30. So in the output I have to get the following:
2015.01.01.-2015.07.01. x$
2015.07.01.-2015.09.30. y$
2015.09.30.-2015.12.31. z$
How can I select these ranges and count the amounts considering I spend money equally per months? If I can define the 3 date ranges I think I can count the amounts, but the dates are really tricky, and I can't handle them.
I use Oracle 11g.
Borrowing heavily from this approach, which is also explained here in more detail along with some alternatives, to just get the date ranges you can do:
with cte1 as
(
select begin_date as marker_date, 1 as type
from your_table
union all
select end_date + 1 as marker_date, -1 as type
from your_table
),
cte2 as (
select marker_date as begin_date,
lead(marker_date) over (order by marker_date) - 1 as end_date,
sum(type) over (order by marker_date) as periods
from cte1
)
select begin_date, end_date from cte2
where end_date is not null and periods > 0;
Which gives you:
BEGIN_DATE END_DATE
---------- ----------
2015-01-01 2015-06-30
2015-07-01 2015-09-30
2015-10-01 2015-12-31
I've assumed that you don't actually want the generated periods to overlap by a day, and instead want them to be the start and ends of months like the original two rows.
To get the amounts - if I've understood what you described - you can modify that to include the amount change at each date, as either positive or negative depending on whether it's the start or end of a period:
with cte1 as
(
select begin_date as marker_date,
amount / months_between(end_date + 1, begin_date) as monthly_amount
from your_table
union all
select end_date + 1 as marker_date,
-amount / months_between(end_date + 1, begin_date) as monthly_amount
from your_table
),
cte2 as (
select marker_date as begin_date,
lead(marker_date) over (order by marker_date) - 1 as end_date,
sum(monthly_amount) over (order by marker_date) as total_monthly_amount
from cte1
)
select begin_date, end_date,
total_monthly_amount * months_between(end_date + 1, begin_date) as amount
from cte2
where end_date is not null and total_monthly_amount > 0;
BEGIN_DATE END_DATE AMOUNT
---------- ---------- ----------
2015-01-01 2015-06-30 6.66666667
2015-07-01 2015-09-30 6.83333333
2015-10-01 2015-12-31 3.5
This works by dividing the amount for the original period by the number of months it covers:
select begin_date as marker_date, amount,
months_between(end_date + 1, begin_date) as months,
amount / months_between(end_date + 1, begin_date) as monthly_amount
from your_table
union all
select end_date + 1 as marker_date, amount,
months_between(end_date + 1, begin_date) as months,
-amount / months_between(end_date + 1, begin_date) as monthly_amount
from your_table;
MARKER_DATE AMOUNT MONTHS MONTHLY_AMOUNT
----------- ---------- ---------- --------------
2015-01-01 10 9 1.11111111
2015-07-01 7 6 1.16666667
2015-10-01 10 9 -1.11111111
2016-01-01 7 6 -1.16666667
And then using that as a CTE and applying the lead analytic function to reconstruct the new, non-overlapping periods:
with cte1 as
(
select begin_date as marker_date,
months_between(end_date + 1, begin_date) as months,
amount / months_between(end_date + 1, begin_date) as monthly_amount
from your_table
union all
select end_date + 1 as marker_date,
months_between(end_date + 1, begin_date) as months,
-amount / months_between(end_date + 1, begin_date) as monthly_amount
from your_table
)
select marker_date as begin_date,
lead(marker_date) over (order by marker_date) - 1 as end_date,
sum(monthly_amount) over (order by marker_date) as total_monthly_amount,
months_between(lead(marker_date) over (order by marker_date), marker_date) as months
from cte1;
BEGIN_DATE END_DATE TOTAL_MONTHLY_AMOUNT MONTHS
---------- ---------- -------------------- ----------
2015-01-01 2015-06-30 1.11111111 6
2015-07-01 2015-09-30 2.27777778 3
2015-10-01 2015-12-31 1.16666667 3
2016-01-01 0.00000000
And finally excluding the artificial open-ended period at the end, plus any that have a zero total in case there are any gaps (which you don't have in the small sample, but could appear in a larger data set); and multiplying the new monthly amount by the number of months in the new period.

Order by query results in specific row positions

This is an extension to a previous quesiton, Order by depending on 2 col values. I have a query which returns the output as below,
SELECT DISTINCT a.rev_date ,fruitname,
fruit_id , primary_fruit_id
FROM fruits a, fruit_lookup s,fruit_reference r
WHERE a.id = s.id(+)
and primary_fruit_id = r.fruit_id(+)
AND (fruit_id = 24 or fruit_id = 0)
ORDER BY case when fruit_id = primary_fruit_id then 0 else 1 end,
fruit_id desc,
a.rev_date desc
How to handle this such that I get the desired output in the below fashion. So, where ever the fruit_id is 0, those rows need to go in the 2nd and the 5th row in the result set of say 10 rows. If totals results are around 50, each page will have 10 results, and in 2nd and the 5th position I need to get the output as below. Hope this makes sense. Any ideas are appreciated. thx
NAME FRUIT_ID PRIMARY_FRUIT_ID
--------------------------------------
apple 24 24
apple 24 24
apple 24 24
apple 24 24
orange 24 12
pear 24 7
kiwi 24 6
melon 24 2
grape 0 90
banana 0 45
carrot 0 30
Desired output
NAME FRUIT_ID PRIMARY_FRUIT_ID
--------------------------------------
apple 24 24
grape 0 90
apple 24 24
apple 24 24
banana 0 45
apple 24 24
kiwi 24 6
orange 24 12
melon 24 2
pear 24 7
carrot 0 30
This is much more complicated question than your previous, so answer is somewhat complicated too.
Here is something what I managed to do:
with t as (
select x.*,
row_number() over (
partition by fid order by decode(fid , pfid, 1, 2), rd desc) rbr
from (
select distinct a.rev_date rd, s.fruitname fn,
a.fruit_id fid, primary_fruit_id pfid
from fruits a left join fruit_lookup s on a.id = s.id
left join fruit_reference r
on primary_fruit_id = r.fruit_id and r.fruit_id in (0, 24) ) x),
ca as (select count(1) cnt from t),
cx as (
select row_number() over (partition by cwm order by lvl) rn, cwm, lvl
from (
select level lvl, case when mod(level, 10) in (2, 5) then 0 else 24 end cwm
from ca connect by level <= cnt*5))
select rd, fn, fid, pfid
from t join cx on cx.rn = t.rbr and cx.cwm = t.fid
order by lvl
You did not provide data structures and sample rows, so I tried to reproduce them to get original input, here is SQLFiddle with data and query.
Subquery cx generates numbers, according to your rules, then these numbers are assigned to your original query
and final select sorts data using these numbers.
The general logic is: insert rows with fruit_id=0 numbered as 2, 5, 12, 15, 22... between other rows.

Is there an algorithm that can divide a number into three parts and have their totals match the original number?

For example if you take the following example into consideration.
100.00 - Original Number
33.33 - 1st divided by 3
33.33 - 2nd divided by 3
33.33 - 3rd divided by 3
99.99 - Is the sum of the 3 division outcomes
But i want it to match the original 100.00
One way that i saw it could be done was by taking the original number minus the first two divisions and the result would be my third number. Now if i take those 3 numbers i get my original number.
100.00 - Original Number
33.33 - 1st divided by 3
33.33 - 2nd divided by 3
33.34 - 3rd number
100.00 - Which gives me my original number correctly. (33.33+33.33+33.34 = 100.00)
Is there a formula for this either in Oracle PL/SQL or a function or something that could be implemented?
Thanks in advance!
This version takes precision as a parameter as well:
with q as (select 100 as val, 3 as parts, 2 as prec from dual)
select rownum as no
,case when rownum = parts
then val - round(val / parts, prec) * (parts - 1)
else round(val / parts, prec)
end v
from q
connect by level <= parts
no v
=== =====
1 33.33
2 33.33
3 33.34
For example, if you want to split the value among the number of days in the current month, you can do this:
with q as (select 100 as val
,extract(day from last_day(sysdate)) as parts
,2 as prec from dual)
select rownum as no
,case when rownum = parts
then val - round(val / parts, prec) * (parts - 1)
else round(val / parts, prec)
end v
from q
connect by level <= parts;
1 3.33
2 3.33
3 3.33
4 3.33
...
27 3.33
28 3.33
29 3.33
30 3.43
To apportion the value amongst each month, weighted by the number of days in each month, you could do this instead (change the level <= 3 to change the number of months it is calculated for):
with q as (
select add_months(date '2013-07-01', rownum-1) the_month
,extract(day from last_day(add_months(date '2013-07-01', rownum-1)))
as days_in_month
,100 as val
,2 as prec
from dual
connect by level <= 3)
,q2 as (
select the_month, val, prec
,round(val * days_in_month
/ sum(days_in_month) over (), prec)
as apportioned
,row_number() over (order by the_month desc)
as reverse_rn
from q)
select the_month
,case when reverse_rn = 1
then val - sum(apportioned) over (order by the_month
rows between unbounded preceding and 1 preceding)
else apportioned
end as portion
from q2;
01/JUL/13 33.7
01/AUG/13 33.7
01/SEP/13 32.6
Use rational numbers. You could store the numbers as fractions rather than simple values. That's the only way to assure that the quantity is truly split in 3, and that it adds up to the original number. Sure you can do something hacky with rounding and remainders, as long as you don't care that the portions are not exactly split in 3.
The "algorithm" is simply that
100/3 + 100/3 + 100/3 == 300/3 == 100
Store both the numerator and the denominator in separate fields, then add the numerators. You can always convert to floating point when you display the values.
The Oracle docs even have a nice example of how to implement it:
CREATE TYPE rational_type AS OBJECT
( numerator INTEGER,
denominator INTEGER,
MAP MEMBER FUNCTION rat_to_real RETURN REAL,
MEMBER PROCEDURE normalize,
MEMBER FUNCTION plus (x rational_type)
RETURN rational_type);
Here is a parameterized SQL version
SELECT COUNT (*), grp
FROM (WITH input AS (SELECT 100 p_number, 3 p_buckets FROM DUAL),
data
AS ( SELECT LEVEL id, (p_number / p_buckets) group_size
FROM input
CONNECT BY LEVEL <= p_number)
SELECT id, CEIL (ROW_NUMBER () OVER (ORDER BY id) / group_size) grp
FROM data)
GROUP BY grp
output:
COUNT(*) GRP
33 1
33 2
34 3
If you edit the input parameters (p_number and p_buckets) the SQL essentially distributes p_number as evenly as possible among the # of buckets requested (p_buckets).
I've solved this problem yesterday by subtracting 2 of 3 parts from the starting number, e.g. 100 - 33.33 - 33.33 = 33.34 and the result of summing it up is still 100.

Resources