Oracle Running Multiplication - sql

we all know how to generate a running total column with
SELECT id, date, value, sum(value)
OVER (partition by id order by date) total
FROM dual
ORDER BY date
Which will give me something like
ID DATE VALUE TOTAL
1 1/1/14 0.001 0.001
2 2/1/14 0.003 0.004
3 3/1/14 0.002 0.006
Now I want to generate a "running multiplication" which generated 0.001 * 0.004 * 0.006. I know that if I just want the value for the whole multiplication can be done by something like
SELECT exp(sum(ln(value))) from dual
but this one does not work with the partition in oracle. Maybe someone has an idea?
Edit
The desired result would be (don't mind the numbers, they are just dummies, they will not run into an overflow).
ID DATE VALUE TOTAL
1 1/1/14 0.001 0.001
2 2/1/14 0.003 0,000004
3 3/1/14 0.002 0,000000024

The exp(sum(ln())) approach works as long as you add the analytics for the sum() part, not for the exp(). This would give you the product of the original values:
WITH t AS (
SELECT 1 AS id, DATE '2014-01-01' AS dat, 0.001 AS value FROM dual
UNION ALL SELECT 2, DATE '2014-01-02', 0.003 FROM dual
UNION ALL SELECT 3, DATE '2014-01-03', 0.002 FROM dual
)
SELECT id, dat, value, EXP(SUM(LN(value))
OVER (PARTITION BY null ORDER BY dat))
AS total
FROM t
ORDER BY dat;
ID DAT VALUE TOTAL
---------- --------- ---------- ----------
1 01-JAN-14 .001 .001
2 02-JAN-14 .003 .000003
3 03-JAN-14 .002 .000000006
And this would give you the product of the running total:
WITH t AS (
SELECT 1 AS id, DATE '2014-01-01' AS dat, 0.001 AS value FROM dual
UNION ALL SELECT 2, DATE '2014-01-02', 0.003 FROM dual
UNION ALL SELECT 3, DATE '2014-01-03', 0.002 FROM dual
),
u AS (
SELECT id, dat, value, SUM(value)
OVER (PARTITION BY null ORDER BY dat) AS total
FROM t
)
SELECT id, dat, value, total, EXP(SUM(LN(total))
OVER (PARTITION BY null ORDER BY dat)) AS product
FROM u
ORDER BY dat;
ID DAT VALUE TOTAL PRODUCT
---------- --------- ---------- ---------- ----------
1 01-JAN-14 .001 .001 .001
2 02-JAN-14 .003 .004 .000004
3 03-JAN-14 .002 .006 .000000024
Use your own table instead of the CTE obviously; and if you're trying to get the product/sum over multiple values with an ID when change it to partition by id. Using null is to make this work with your sample data.

Unashamedly riffing off this demonstration of a custom aggregate product() function, which supports windowing, you could create your own analytic function to do the calculation for you:
CREATE OR REPLACE TYPE product_total_impl AS OBJECT
(
product NUMBER,
total NUMBER,
product_total NUMBER,
STATIC FUNCTION ODCIAggregateInitialize(ctx IN OUT product_total_impl) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateIterate(SELF IN OUT product_total_impl,
VALUE IN NUMBER) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateMerge(SELF IN OUT product_total_impl,
ctx2 IN product_total_impl) RETURN NUMBER,
MEMBER FUNCTION ODCIAggregateTerminate(SELF IN OUT product_total_impl,
returnvalue OUT NUMBER,
flags IN NUMBER) RETURN NUMBER
);
/
CREATE OR REPLACE TYPE BODY product_total_impl IS
STATIC FUNCTION ODCIAggregateInitialize(ctx IN OUT product_total_impl) RETURN NUMBER IS
BEGIN
ctx := product_total_impl(1, 0, 1);
RETURN ODCIConst.Success;
END ODCIAggregateInitialize;
MEMBER FUNCTION ODCIAggregateIterate(SELF IN OUT product_total_impl,
VALUE IN NUMBER) RETURN NUMBER IS
BEGIN
IF VALUE IS NOT NULL THEN
SELF.product := SELF.product * VALUE;
SELF.total := SELF.total + VALUE;
SELF.product_total := SELF.product_total * SELF.total;
END IF;
RETURN ODCIConst.Success;
END ODCIAggregateIterate;
MEMBER FUNCTION ODCIAggregateMerge(SELF IN OUT product_total_impl,
ctx2 IN product_total_impl) RETURN NUMBER IS
BEGIN
SELF.product := SELF.product * ctx2.product;
SELF.total := SELF.total + ctx2.total;
SELF.product_total := ctx2.product_total * ctx2.total;
RETURN ODCIConst.Success;
END ODCIAggregateMerge;
MEMBER FUNCTION ODCIAggregateTerminate(SELF IN OUT product_total_impl,
returnvalue OUT NUMBER,
flags IN NUMBER) RETURN NUMBER IS
BEGIN
returnvalue := SELF.product_total;
RETURN ODCIConst.Success;
END ODCIAggregateTerminate;
END;
/
CREATE OR REPLACE FUNCTION product_total(x IN NUMBER) RETURN NUMBER
PARALLEL_ENABLE
AGGREGATE USING product_total_impl;
/
Then you can do:
WITH t AS (
SELECT 1 AS id, DATE '2014-01-01' AS dat, 0.001 AS value FROM dual
UNION ALL SELECT 2, DATE '2014-01-02', 0.003 FROM dual
UNION ALL SELECT 3, DATE '2014-01-03', 0.002 FROM dual
)
SELECT id, dat, value,
SUM(value) OVER (PARTITION BY null ORDER BY dat) AS total,
PRODUCT_TOTAL(value) OVER (PARTITION BY null ORDER BY dat) AS product_total
FROM t
ORDER BY dat;
ID DAT VALUE TOTAL PRODUCT_TOTAL
---------- --------- ---------- ---------- -------------
1 01-JAN-14 .001 .001 .001
2 02-JAN-14 .003 .004 .000004
3 03-JAN-14 .002 .006 .000000024
SQL Fiddle with the original product as well.
As before, use your own table instead of the CTE obviously; and if you're trying to get the product/sum over multiple values with an ID when change it to partition by id. Using null is to make this work with your sample data.

Related

Calculating the days in a certain period

I have a form which is connected to database,
so this form can have more blocks, where each block have date from, date until
for example
Block
Date_From
Date_until
1
25.07.2022
11.08.2022
2
05.08.2022
15.08.2022
3
10.08.2022
20.08.2022
4
11.08.2022
05.09.2022
I'm trying to make a SELECT statement which going to display number of days between 01.08.2022 and 31.08.2022.
first block date_from = 25.07.2022, date_until = 11.08.2022 ->11 days
second block and third block should remain NULL or some default text, because the interval of these blocks is in fourth block.
fourth block date_from = 11.08.2022, date_until = 05.09.2022-> 20 days (until the end of the month).
Could you help me guys with creating this select? The select should have date_from, date_until and number of days.
WITH data(Block,Date_From,Date_until) AS (
SELECT 1, TO_DATE('25.07.2022','DD.MM.YYYY'), TO_DATE('11.08.2022','DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 2, TO_DATE('05.08.2022','DD.MM.YYYY'), TO_DATE('15.08.2022','DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 3, TO_DATE('10.08.2022','DD.MM.YYYY'), TO_DATE('20.08.2022','DD.MM.YYYY') FROM DUAL UNION ALL
SELECT 4, TO_DATE('11.08.2022','DD.MM.YYYY'), TO_DATE('05.09.2022','DD.MM.YYYY') FROM DUAL -- UNION ALL
),
clipped(Block,Date_From,Date_until) AS (
SELECT Block, GREATEST(Date_From, TO_DATE('01.08.2022','DD.MM.YYYY')), LEAST(Date_until, TO_DATE('31.08.2022','DD.MM.YYYY')) FROM DATA
)
SELECT c.*,
CASE WHEN NOT(
EXISTS(SELECT 1 FROM clipped d WHERE d.Date_From < c.Date_until AND d.Date_until > c.Date_From AND d.block < c.block)
AND
EXISTS(SELECT 1 FROM clipped d WHERE d.Date_From < c.Date_until AND d.Date_until > c.Date_From AND d.block > c.block) )
THEN c.Date_until - c.Date_from ELSE NULL
END AS days
FROM clipped c
ORDER BY c.block
;
1 01/08/22 11/08/22 10
2 05/08/22 15/08/22
3 10/08/22 20/08/22
4 11/08/22 31/08/22 20

PL/SQL: How to sum a set of values if they fall within a specific time frame?

I have a query (below) that shows the number of terminations since 1/1/17 in one column and the associated date of the terminations in the only other column. If there were no terminations on a specific date, then there is no record for that date.
I want to create rolling 12-month time buckets and sum the number of terminations in those time buckets.
For example, the most recent time bucket would have an ending date of 11:59pm on 6/30/22. The start of that time bucket would start midnight on 7/1/21. I want to sum the number of terminations in that time bucket.
I need to create 12-month time buckets and the associated number of terminations for the last 60 months, resulting in 60 time buckets.
Here is my current query:
select
count(distinct employee_number) Number_of_terminations
, to_char(term_date, 'MM/DD/YYYY') term_date
from
(
select paa.person_id
,max(paa.effective_end_date)+1 term_date
,pap.employee_number
from
apps.per_all_assignments_f paa
, apps.per_assignment_status_types past
,(select distinct paa.person_id
from
apps.per_all_assignments_f paa
, apps.per_assignment_status_types past
where paa.assignment_status_type_id = past.assignment_status_type_id
and sysdate between paa.effective_start_date and paa.effective_end_date
and past.user_status in ('Active Assignment','Transitional - Active','Transitional - Inactive','Sabbatical','Sabbatical 50%')) active_person
, apps.per_all_people_f pap
, apps.hr_organization_units org
,(select case when orgp.name = 'Random University' then orgc.attribute1 else orgp.attribute1 end unit_number
,case when orgp.name = 'Random State University' then orgc.name else orgp.name end unit_name
,orgc.attribute1 dept_number
,orgc.name dept_name
from apps.per_org_structure_elements_v2 pose
,apps.per_org_structure_versions posv
,apps.hr_all_organization_units orgp
,apps.hr_all_organization_units orgc
where pose.org_structure_version_id = posv.org_structure_version_id
and pose.organization_id_parent = orgp.organization_id
and pose.organization_id_child = orgc.organization_id
and trunc(sysdate) between posv.date_from and nvl(posv.date_to,'31-dec-4712')
and pose.org_structure_hierarchy = 'Units'
order by case when orgp.name = 'Colorado State University' then orgc.attribute1 else orgp.attribute1 end
,orgc.attribute1) u
, apps.per_jobs pj
, apps.per_job_definitions pjd
where paa.assignment_status_type_id = past.assignment_status_type_id
and paa.person_id = active_person.person_id(+)
and active_person.person_id is null
and past.user_status in ('Active Assignment','Transitional - Active','Transitional - Inactive','Sabbatical','Sabbatical 50%')
and pap.person_id = paa.person_id
and paa.organization_id = org.organization_id
and org.attribute1 = u.dept_number(+)
and paa.job_id = pj.job_id
and pj.job_definition_id = pjd.job_definition_id
and pap.employee_number is not null
and (
paa.effective_end_date like '%17' or
paa.effective_end_date like '%18' or
paa.effective_end_date like '%19' or
paa.effective_end_date like '%20' or
paa.effective_end_date like '%21' or
paa.effective_end_date like '%22'
)
group by paa.person_id
, pap.employee_number
) terms
--group by substr(term_date, 4, 6)
group by to_char(term_date, 'MM/DD/YYYY')
Here are the first rows of the results:
enter image description here
In Excel the first sum would like be calculated like this: Excel example
I don't have your data and I don't want to spend time generating some test data to match that monster query but here is a simplified example explaining how to do this:
Create a calendar table: 1 record per bucket (monthly) with start and end date.
CREATE TABLE last_60_months (start_dt, end_dt)
AS
(SELECT TRUNC(ADD_MONTHS(SYSDATE,-LEVEL+1), 'MON'), TRUNC(ADD_MONTHS(SYSDATE,-LEVEL+13), 'MON') - 1 FROM DUAL
CONNECT BY LEVEL < 61
);
Create a test table with 10000 employees and a termination date within the test buckets boundaries:
CREATE table test_emps (employee_number NUMBER, term_date DATE);
DECLARE
l_dt DATE;
l_min_dt DATE;
l_max_dt DATE;
BEGIN
SELECT MIN(start_dt), MAX(start_dt) INTO l_min_dt, l_max_dt FROM last_60_months;
FOR r IN 1 .. 10000 LOOP
SELECT TO_DATE(
TRUNC(
DBMS_RANDOM.VALUE(TO_CHAR(l_min_dt,'J')
,TO_CHAR(l_max_dt,'J')
)
),'J'
)
INTO l_dt
FROM DUAL;
INSERT INTO test_emps (employee_number, term_date) VALUES (r, l_dt );
END LOOP;
COMMIT;
END;
/
Put it all together:
SELECT COUNT(e.employee_number) as "Number_of_terminations", d.start_dt, d.end_dt
FROM test_emps e JOIN last_60_months d ON e.term_date BETWEEN d.start_dt AND d.end_dt
GROUP BY start_dt, end_dt
ORDER BY start_dt;
It should be trivial to use this technique for your own data.

Write a PL/SQL program using an implicit cursor that displays the whole table OILPRICE on the screen with a third column

Table Data:
01-FEB-21 2.25
02-FEB-21 2.36
03-FEB-21 2.47
04-FEB-21 2.51
05-FEB-21 2.4
Question
Write a PL/SQL program using an implicit cursor that displays the whole table OILPRICE on the screen with a third column (Change; it does not exist in the table) in the following format:
Date= 01-FEB-21 Price= 2.25 Change= 0
Date= 02-FEB-21 Price= 2.36 Change= +
Etc.
Now here is the explanation of how to compute the Change value.
If the Price in the current row is larger by more than 0.1 than the value in the PREVIOUS row, print a + as the value of Change.
If the Price in the current row is smaller by more than 0.1 than the value in the PREVIOUS row, print a - (minus sign) as the value of Change.
If the absolute value of the difference between the price in the current row and in the previous row is less than 0.1 then the value of Change should be 0.
In the first row the value of Change should be 0.
Cursor I made
begin
for implicit_cursor in
(select * from oilprice)
loop
dbms_output.put_line(
'Date= ' || implicit_cursor.dateline
|| ' ' || 'Price= '|| implicit_cursor.price
);
end loop;
end;
You can add the third column in the above code or you can make your own.
Thank you!!!
Sample data:
SQL> alter session set nls_date_format = 'dd.mm.yyyy';
Session altered.
SQL> select * from test order by datum;
DATUM PRICE
---------- ----------
01.02.2021 2,25
02.02.2021 2,36
03.02.2021 2,47
04.02.2021 2,51
05.02.2021 2,4
PL/SQL anonymous block which does what you wanted. How? It uses the lag analytic function to fetch previous row's price and compares it to current row's value in order to calculate the change value.
SQL> begin
2 for cur_r in
3 (select datum,
4 price,
5 lag(price) over (order by datum) previous_price,
6 --
7 case when price - lag(price) over (order by datum) > 0.1 then '+'
8 when price - lag(price) over (order by datum) < 0.1 then '-'
9 else '0'
10 end as change
11 from test
12 order by datum
13 )
14 loop
15 dbms_output.put_line
16 ('Date = ' || to_char(cur_r.datum, 'dd.mm.yyyy') ||
17 ' Price = ' || to_char(cur_r.price, 'fm999G990D00') ||
18 ' Change = ' || cur_r.change
19 );
20 end loop;
21 end;
22 /
Date = 01.02.2021 Price = 2,25 Change = 0
Date = 02.02.2021 Price = 2,36 Change = +
Date = 03.02.2021 Price = 2,47 Change = +
Date = 04.02.2021 Price = 2,51 Change = -
Date = 05.02.2021 Price = 2,40 Change = -
PL/SQL procedure successfully completed.
SQL>

Oracle SQL - Round - Half

Oracle ROUND function rounds "half up" by default :
select 3.674 my_number,
round(3.674,2) round_on_number
from dual
union
select 3.675 my_number,
round(3.675,2) round_on_number
from dual
union
select 3.676 my_number,
round(3.676,2) round_on_number
from dual
;
MY_NUMBER ROUND_ON_NUMBER
---------- ---------------
3,674 3,67
3,675 3,68
3,676 3,68
I need to round "half down", which essentially means that I should get the following result instead :
MY_NUMBER EXPECTED_ROUND_ON_NUMBER
---------- ------------------------
3,674 3,67
3,675 3,67
3,676 3,68
It should be fast as I need to do this on millions of items.
I could probably detect if the number ends with a "5" and trunc that last digit in that case, round otherwise, but I have the feeling this will be inefficient (?)
Thank you !
David
The documentation shows you the algorithm used:
If n is 0, then ROUND always returns 0 regardless of integer.
If n is negative, then ROUND(n, integer) returns -ROUND(-n, integer).
If n is positive, then
ROUND(n, integer) = FLOOR(n * POWER(10, integer) + 0.5) * POWER(10, -integer)
So you could modify the positive, non-zero version:
FLOOR(n * POWER(10, integer) + 0.4) * POWER(10, -integer)
^
e.g. for a fixed rounding, and ignoring zeros/negative for now:
with t (my_number) as (
select 3.674 from dual
union all select 3.675 from dual
union all select 3.676 from dual
)
select my_number,
floor(my_number * power(10, 2) + 0.4) * power(10, -2) as round_on_number
from t;
MY_NUMBER ROUND_ON_NUMBER
---------- ---------------
3.674 3.67
3.675 3.67
3.676 3.68
You could include zero/negative via a case expression; or write your own function to handle it more neatly.
Knock .001 from the value, then round as normal:
select round(my_number-.001,2)
from MyTable
For rounding to 3dp, change to -0.0001, etc

Generate rows and insert into a table

I've following table
CREATE TABLE public.af01
(
id integer NOT NULL DEFAULT nextval('af01_id_seq'::regclass),
idate timestamp without time zone,
region text,
city text,
vtype text,
vmake text,
vmodel text,
vregno text,
intime time without time zone,
otime time without time zone,
vstatus boolean,
remarks text,
vowner text
);
I need to add data into it.This data should be for 1 Year, (data from 01-01-2016 to 31-12-2016). in a single date can have 5 entries,
Region column must have 3 values (Central,Western,Eastern),
City column must have 3 values(City1,City2,City3)
vtype column is the Vehicle type for example Heavy,light,Other.
vmake column is the manufacturer Audi,Nissan,Toyota,Hyundai,GMC etc.
vregno this column is for vechicle registration number and it should be unique (Ex.reg no CFB 4587).
intime any random time in day('10:15 AM').
otime this column should be intime+ 5 or 10 or 15 or 20.vstatus column should have True or false.
I've ended up with this select query to generate date rows
select '2013-01-01'::date + (n || ' days')::interval days
from generate_series(0, 365) n;
and
to generate first part of the Vehicle regno.
SELECT substring(string_agg (substr('ABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil (random() * 62)::integer, 1), ''),1,3) t
FROM generate_series(0,45);
expected output;
id idate region city vtype vmake vmodel vregno intime otime vstatus remarks vowner
-- ---------- ------- ----- -------------- ------ ------ -------- ------------------- ------------------- ------- ------- ------
1 2016-01-01 Central City1 Heavy Vechicle Nissan Model1 NGV 4578 12:15:00 12:30:00 1 NULL Tom
2 2016-01-01 Western City1 Light Audi S3 BFR 4587 10:20:00 10:40:00 1 NULL Jerry
r_dates relation is just simple way to generate dates in ranges.
other_const and max_const are arrays and its length respectively for population. region[(random() * region_max)::int2 + 1] - choose element in array by random
INSERT INTO af01 (idate, region, city, vtype, vmake, vregno, intime, otime, vstatus)
SELECT cd, r, c, v, vm, rn, intime, intime + len as otime, status
FROM (
WITH r_dates AS (
SELECT generate_series('2013-01-01'::date, '2013-12-31'::date, '1 day'::interval) as cd
), other_const AS (
SELECT '{Central,Western,Eastern}'::text[] AS region,
'{City1,City2,City3}'::text[] as cities,
'{Heavy,light,Other}'::text[] as vehicles,
'{Audi,Nissan,Toyota,Hyundai,GMC}'::text[] as vmakes,
'{5,10,15,20}'::int4[] AS lengths,
'ABCDEFGHIJKLMNOPQRSTUVWXYZ'::text AS regnosrc
), max_const AS (
SELECT array_upper(region, 1) - 1 AS region_max,
array_upper(cities, 1) - 1 AS cities_max,
array_upper(vehicles, 1) - 1 AS vehicles_max,
array_upper(vmakes, 1) - 1 AS vmakes_max,
array_upper(lengths, 1) - 1 AS lengths_max
FROM other_const
)
SELECT cd,
region[(random() * region_max)::int2 + 1] AS r,
cities[(random() * cities_max)::int2 + 1] AS c,
vehicles[(random() * vehicles_max)::int2 + 1] AS v,
vmakes[(random() * vmakes_max)::int2 + 1] AS vm,
(
SELECT string_agg(s, '')
FROM (
SELECT substr(regnosrc, (random() * (length(regnosrc) - 1))::int4 + 1, 1) AS s
FROM generate_series(1, 3)
) AS a
)
|| lpad(((random() * 9999)::int8)::text, 4, '0') AS rn,
'00:00:00'::time + (((random() * 24 * 60)::int8)::text || 'min')::interval AS intime,
((lengths[(random() * lengths_max)::int2 + 1])::text || 'min')::interval AS len,
random() > 0.5 AS status
FROM r_dates, other_const, max_const, generate_series(1, 5)
) AS A