Get percentage of increase/decrease from two results base on their WHERE clause - oracle

I don't know if my Title is understandable but what I am really trying to achieve is get the percentage increase/decrease of Result 2 from Result 1.
I have a SALES table with these sample data:
DATE. AMOUNT
01-JAN-20. 500
02-JAN-20. 400
...
15-MAR-20. 1000
Assume that the table is filled with daily sales.
Now, with the Dashboard app I'm trying to make in APEX, I want to display the TOTAL AMOUNT base from the DATE the user will choose on a SELECT list (Today, This Week, This Month).
So basically, these would be the queries:
SELECT SUM(AMOUNT) TOTAL_SALES FROM TB_SALES WHERE DATE = TRUNC(SYSDATE); - Today
SELECT SUM(AMOUNT) TOTAL_SALES FROM TB_SALES WHERE DATE BETWEEN TRUNC(SYSDATE-7,'SUNDAY') AND TRUNC(SYSDATE); - This Week
SELECT SUM(AMOUNT) TOTAL_SALES FROM TB_SALES WHERE DATE BETWEEN TRUNC(SYSDATE, 'MM' AND TRUNC(SYSDATE); - This Month
To get the percentage of increase or decrease, I now then need to get the result of same queries but different DATE from the WHERE clause.
So with Yesterday, I should query
TRUNC(SYSDATE -1);
And so on. Actually I still don't know how to get the This week part.
With the two results, I can now calculate the percentage.
What I can't figure out is how can I query this two WHERE clause of two DATEs. My current temporary solution is to query the second query and just use JavaScript to update my Card.
I found something about LAG functions but cannot find any sample which uses WHERE clause to compare the results.
EDIT:
To be more clear, this is what I want to compare and get the percentage.
Today VS Yesterday
This week VS Last week
This month VS Last month

You can use conditional aggregation to find the totals:
SELECT SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE )
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_today,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE ) - INTERVAL '1' DAY
AND "DATE" < TRUNC( SYSDATE )
THEN AMOUNT
ELSE NULL
END
) AS total_yesterday,
SUM(
CASE
WHEN "DATE" >= NEXT_DAY( TRUNC( SYSDATE ) - INTERVAL '7' DAY, 'SUNDAY' )
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_week,
SUM(
CASE
WHEN "DATE" >= NEXT_DAY( TRUNC( SYSDATE ) - INTERVAL '14' DAY, 'SUNDAY' )
AND "DATE" < NEXT_DAY( TRUNC( SYSDATE ) - INTERVAL '7' DAY, 'SUNDAY' )
THEN AMOUNT
ELSE NULL
END
) AS total_last_week,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE, 'IW' ),
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_iso_week,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE, 'IW' ) - INTERVAL '7' DAY,
AND "DATE" < TRUNC( SYSDATE, 'IW' )
THEN AMOUNT
ELSE NULL
END
) AS total_last_iso_week,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE ) - INTERVAL '6' DAY,
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_7_days,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE ) - INTERVAL '13' DAY,
AND "DATE" < TRUNC( SYSDATE ) - INTERVAL '6' DAY
THEN AMOUNT
ELSE NULL
END
) AS total_last_7_days,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE, 'MM' )
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_month
SUM(
CASE
WHEN "DATE" >= ADD_MONTHS( TRUNC( SYSDATE, 'MM' ), -1 )
AND "DATE" < TRUNC( SYSDATE, 'MM' )
THEN AMOUNT
ELSE NULL
END
) AS total_this_month
FROM tb_sales
WHERE "DATE" >= ADD_MONTHS( TRUNC( SYSDATE, 'MM' ), -1 )
AND "DATE" <= SYSDATE;
This will find the totals for:
Today and yesterday;
This week and last week (starting from midnight Sunday until before the midnight of the next Sunday or until this instant, whichever is earlier);
This ISO 8601 week and last ISO 8601 week (starting midnight Monday);
The last 7 days (today and the previous 6 days) and 7 days before that; and
This month and last month (from midnight of the first day of the month until this instant).
If you want the percentage increases compared to a previous period then:
SELECT 100 * ( total_this_today / total_yesterday - 1 ) AS percent_increase_this_today,
100 * ( total_this_week / total_last_week - 1 ) AS percent_increase_this_week,
100 * ( total_this_iso_week / total_last_iso_week - 1 ) AS percent_increase_this_iso_week,
100 * ( total_this_7_days / total_last_7_days - 1 ) AS percent_increase_this_7_days,
100 * ( total_this_month / total_last_month - 1 ) AS percent_increase_this_month
FROM (
SELECT SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE )
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_today,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE ) - INTERVAL '1' DAY
AND "DATE" < TRUNC( SYSDATE )
THEN AMOUNT
ELSE NULL
END
) AS total_yesterday,
SUM(
CASE
WHEN "DATE" >= NEXT_DAY( TRUNC( SYSDATE ) - INTERVAL '7' DAY, 'SUNDAY' )
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_week,
SUM(
CASE
WHEN "DATE" >= NEXT_DAY( TRUNC( SYSDATE ) - INTERVAL '14' DAY, 'SUNDAY' )
AND "DATE" < NEXT_DAY( TRUNC( SYSDATE ) - INTERVAL '7' DAY, 'SUNDAY' )
THEN AMOUNT
ELSE NULL
END
) AS total_last_week,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE, 'IW' ),
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_iso_week,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE, 'IW' ) - INTERVAL '7' DAY,
AND "DATE" < TRUNC( SYSDATE, 'IW' )
THEN AMOUNT
ELSE NULL
END
) AS total_last_iso_week,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE ) - INTERVAL '6' DAY,
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_7_days,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE ) - INTERVAL '13' DAY,
AND "DATE" < TRUNC( SYSDATE ) - INTERVAL '6' DAY
THEN AMOUNT
ELSE NULL
END
) AS total_last_7_days,
SUM(
CASE
WHEN "DATE" >= TRUNC( SYSDATE, 'MM' )
AND "DATE" <= SYSDATE
THEN AMOUNT
ELSE NULL
END
) AS total_this_month
SUM(
CASE
WHEN "DATE" >= ADD_MONTHS( TRUNC( SYSDATE, 'MM' ), -1 )
AND "DATE" < TRUNC( SYSDATE, 'MM' )
THEN AMOUNT
ELSE NULL
END
) AS total_this_month
FROM tb_sales
WHERE "DATE" >= ADD_MONTHS( TRUNC( SYSDATE, 'MM' ), -1 )
AND "DATE" <= SYSDATE
)

Related

(Oracle 11g DB) Calculate Number of buisiness days between current time and a date while excluding holidays in a view

So I have this working SQL script that take a date and returns the age from current time to the given date excluding dates defined in a table called exclude dates
SELECT
COUNT(*)
FROM
(
SELECT
ROWNUM rnum
FROM
all_objects
WHERE
ROWNUM <= CAST(current_timestamp AS DATE) - to_date('&2') + 1
)
WHERE
to_char(to_date('&2') + rnum - 1, 'DY') NOT IN ( 'SAT', 'SUN' )
AND NOT EXISTS (
SELECT
NULL
FROM
exclude_dates
WHERE
no_work = trunc(to_date('&2') + rnum - 1)
);
I have a table called
TICKETS
that contains columns named
ID, UPDATED_AT
I want to create a view that uses the above script to return
ID, AGE
where age is the output of the script above.
You code has a few weaknesses.
There is no need for CAST(current_timestamp AS DATE).
If you need the current DATE then simply use TRUNC(SYSDATE)
You don't need to select from all_objects. Better use hierarchical query
SELECT LEVEL as rnum FROM dual CONNECT BY LEVEL <= ...
Using to_date('&2') without a format is usually bad. Either your input value is a string, then you should include the format, e.g. to_date('&2', 'YYYY-MM-DD') or your input value is a DATE, then simply use &2 - never use TO_DATE() on a value which is already a DATE!
Final query could be this one - assuming input parameter is a DATE value:
WITH t AS (
SELECT LEVEL as d
FROM dual
CONNECT BY LEVEL <= TRUNC(SYSDATE) - the_day)
SELECT COUNT(*) AS buisiness_days
FROM t
WHERE TO_CHAR(the_day + d - 1, 'DY', 'NLS_DATE_LANGUAGE = american') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 'x'
FROM exclude_dates
WHERE no_work = TRUNC(the_day + d - 1)
)
However, for me it is not clear how you want to provide this as a view! You would need to create a separate view for each input date, or at least create a new view every day.
I would suggest to create a function:
CREATE OR REPLACE FUNCTION buisiness_days(the_date IN DATE) RETURN INTEGER AS
ret INTEGER;
BEGIN
WITH t AS (
SELECT LEVEL as d
FROM dual
CONNECT BY LEVEL <= TRUNC(SYSDATE) - the_date)
SELECT COUNT(*) AS buisiness_days
INTO ret
FROM t
WHERE TO_CHAR(the_date + d - 1, 'DY', 'NLS_DATE_LANGUAGE = american') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 'x'
FROM exclude_dates
WHERE no_work = TRUNC(the_date + d - 1)
);
RETURN ret;
END;
The function will return a list of dates between the date range you provide so the dates don't have to be stored in a table.
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
/
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
To exclude holidays you need to know what dates they fall on so there needs to be a holiday table.
create table holidays(
holiday_date DATE not null,
holiday_name VARCHAR2(20),
constraint holidays_pk primary key (holiday_date),
constraint is_midnight check ( holiday_date = trunc ( holiday_date ) )
);
INSERT into holidays (HOLIDAY_DATE,HOLIDAY_NAME)
WITH dts as (
select to_date('25-NOV-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'Thanksgiving 2021' from dual union all
select to_date('29-NOV-2021 00:00:00','DD-MON-YYYY HH24:MI:SS'), 'Hanukkah 2021' from dual
)
SELECT * from dts;
This query will provide the count of days between the range, number of working days and number of holidays in the range.
SELECT COUNT (*) AS total_days
, COUNT ( CASE
WHEN h.holiday_date IS NULL
AND d.column_value - TRUNC (d.column_value, 'IW') < 5
THEN 'Business Day'
END
) AS business_days
, COUNT (h.holiday_date) AS holidays
FROM generate_dates_pipelined (DATE '2021-11-01', DATE '2021-11-30') d
LEFT JOIN holidays h ON h.holiday_date = d.column_value;
This query will provide a list of dates excluding sat, sun and holidays that fall between the range.
SELECT
COLUMN_VALUE
FROM
TABLE(generate_dates_pipelined(DATE '2021-11-01',
DATE '2021-11-30')) c
where
to_char(COLUMN_VALUE, 'DY') NOT IN ('SAT', 'SUN')
AND NOT EXISTS (
SELECT 1
FROM holidays h
WHERE c.COLUMN_VALUE = h.holiday_date
);
You don't need a function or to use a row generator function and can calculate the number of business days:
CREATE VIEW business_day_ages (ID, AGE) AS
SELECT id,
( TRUNC( SYSDATE, 'IW' ) - TRUNC( updated_at, 'IW' ) ) * 5 / 7
-- Number of full weeks.
+ LEAST( SYSDATE - TRUNC( SYSDATE, 'IW' ), 5 )
-- Add part weeks at the end.
- LEAST( updated_at - TRUNC( updated_at, 'IW' ), 5 )
-- Subtract part weeks at the start.
- COALESCE(
( SELECT SUM(
LEAST(no_work + INTERVAL '1' DAY, SYSDATE)
- GREATEST(no_work, updated_at)
)
FROM exclude_dates
WHERE no_work BETWEEN TRUNC(updated_at) AND SYSDATE
),
0
)
-- Subtract the holiday days.
FROM tickets;
Or, if you are not calculating using part days then you can simplify it to:
CREATE OR REPLACE VIEW business_day_ages (ID, AGE) AS
SELECT id,
( TRUNC( SYSDATE, 'IW' ) - TRUNC( updated_at, 'IW' ) ) * 5 / 7
-- Number of full weeks.
+ LEAST( TRUNC(SYSDATE) - TRUNC( SYSDATE, 'IW' ), 5 )
-- Add part weeks at the end.
- LEAST( updated_at - TRUNC( updated_at, 'IW' ), 5 )
-- Subtract part weeks at the start.
- COALESCE(
( SELECT 1
FROM exclude_dates
WHERE no_work BETWEEN TRUNC(updated_at) AND TRUNC(SYSDATE)
),
0
)
-- Subtract the holiday days.
FROM tickets;
db<>fiddle here

Extract function not working as I want in Oracle Db

I have tried many ways of getting the day,month and year in PLSQL but it keep throw me the error.
ORA-01841: (full) year must be between -4713 and +9999, and not be 0
Here is the query I run (The mfgdate and receivingdate are timestamp(6) column, and the receivingdate has null value)
select
IC_MST_CodeDateData.mstid AS mstid,
from IC_MST_CodeDateData
where ( ( IC_MST_CodeDateData.consprtydatecode = 'M' )
and ( IC_MST_CodeDateData.mfgdatereqd = 0 )
and ( EXTRACT(day from cast(( IC_MST_CodeDateData.mfgdate - interval '5' hour )as DATE)) = extract(day from cast(( IC_MST_CodeDateData.receivingdate - interval '5' hour )as Date)) )
and ( extract(month from cast(( IC_MST_CodeDateData.mfgdate - interval '5' hour )as DATE) )= EXTRACT( Month from cast(( IC_MST_CodeDateData.receivingdate - interval '5' hour )as DATE)) )
and ( extract(year from cast(( IC_MST_CodeDateData.mfgdate - interval '5' hour )as DATE)) = extract(year from cast(( IC_MST_CodeDateData.receivingdate - interval '5' hour )as DATE)) )
I check the mfgdate column and there are no NULL value.
If anyone can help would be very appreciate!

Oracle: Days between two date and Exclude weekdays how to handle negative numbers

I have two date columns and trying to measure days between the two dates excluding weekends. I'm getting a negative number and need help solving.
Table
CalendarDate DayNumber FirstAssgn FirstCnt DayNumber2 Id BusinessDays
5/21/2017 Sunday 5/21/17 5/21/17 Sunday 1 -1
Query:
TRUNC(TO_DATE(A.FIRST_CONTACT_DT, 'DD/MM/YYYY')) - TRUNC(TO_DATE(A.FIRST_ASSGN_DT, 'DD/MM/YYYY'))
- ((((TRUNC(A.FIRST_CONTACT_DT,'D'))-(TRUNC(A.FIRST_ASSGN_DT,'D')))/7)*2)
- (CASE WHEN TO_CHAR(A.FIRST_ASSGN_DT,'DY','nls_date_language=english') ='SUN' THEN 1 ELSE 0 END)
- (CASE WHEN TO_CHAR(A.FIRST_CONTACT_DT,'DY','nls_date_language=english')='SAT' THEN 1 ELSE 0 END)
- (SELECT COUNT(1) FROM HUM.CALENDAR CAL
WHERE 1=1
AND CAL.CALENDAR_DATE >= A.FIRST_ASSGN_DT
AND CAL.CALENDAR_DATE < A.FIRST_CONTACT_DT
--BETWEEN A.FIRST_ASSGN_DT AND A.FIRST_CONTACT_DT
AND CAL.GRH_HOLIDAY_IND = 'Y'
) AS Business_Days
Looks like below piece needs editing...
- (CASE WHEN TO_CHAR(A.FIRST_ASSGN_DT,'DY','nls_date_language=english')='SUN' THEN 1 ELSE 0 END)
Adapted from my answer here:
Get the number of days between the Mondays of both weeks (using TRUNC( datevalue, 'IW' ) as an NLS_LANGUAGE independent method of finding the Monday of the week) then add the day of the week (Monday = 1, Tuesday = 2, etc., to a maximum of 5 to ignore weekends) for the end date and subtract the day of the week for the start date. Like this:
SELECT ( TRUNC( end_date, 'IW' ) - TRUNC( start_date, 'IW' ) ) * 5 / 7
+ LEAST( end_date - TRUNC( end_date, 'IW' ) + 1, 5 )
- LEAST( start_date - TRUNC( start_date, 'IW' ) + 1, 5 )
AS WeekDaysDifference
FROM your_table
With RANGE_TEMP as (
SELECT
STARTPERIOD start_date,
ENDPERIOD end_date
FROM
TABLE_DATA -- YOUR TABLE WITH ALL DATA DATE
), DATE_TEMP AS (
SELECT
(start_date + LEVEL) DATE_ALL
FROM
RANGE_TEMP
CONNECT BY LEVEL <= (end_date - start_date)
), WORK_TMP as (
SELECT
COUNT(DATE_ALL) WORK_DATE
FROM
DATE_TEMP
WHERE
TO_CHAR(DATE_ALL,'D', 'NLS_DATE_LANGUAGE=ENGLISH') NOT IN ('1','7')
), BUSINESS_TMP as (
SELECT
COUNT(DATE_ALL) BUSINESS_DATE
FROM
DATE_TEMP
WHERE
TO_CHAR(DATE_ALL,'D', 'NLS_DATE_LANGUAGE=ENGLISH') IN ('1','7')
)
SELECT
L.WORK_DATE,
H.BUSINESS_DATE
FROM
BUSINESS_TMP H,
WORK_TMP L
;

Find number of Wednesdays (or other weekday) in a month - Oracle SQL

I found this query for finding the number of Sundays in a month.
I have been tinkering with it but cannot figure out how to change it to get, say, the number of Wednesdays in a month, for example. Can you show me how?
with
months as (
select add_months(trunc(sysdate,'YEAR'),level-13) month
from dual
connect by level <= 36
)
select to_char(month,'YYYY') year,
to_char(month,'Month') month,
to_char(month,'Day') first_day,
to_char(last_day(month),'Day DD') last_day,
4+
case
when to_char(last_day(month),'DD') - decode(to_char(month,'D'),1,0,8 -to_char(month,'D')) >= 29
then 1
else 0
end nb_sunday
from months
Here's the game: You give me a year (like 2015) and a day of the week, in the form of a three-letter string (like 'Wed'). I will return a table with each month of that year and with the count of days-of-week equal to your input in each month.
Simply implementing here the suggestion from my Comment to MT0's Answer. I am hard-coding the year and the day-of-week (in a CTE) since "how to pass parameters to a query" (through bind variables and such) is not the focus in this thread.
with
inputs ( yr, day_of_week ) as (
select 2015, 'Wed' from dual
),
prep ( dec31 ) as (
select to_date(to_char(yr - 1) || '-12-31', 'yyyy-mm-dd') from inputs
)
select to_char(add_months(dec31, level), 'Mon-yyyy') as mth,
( next_day(add_months(dec31, level) , day_of_week) -
next_day(add_months(dec31, level - 1), day_of_week) ) / 7 as cnt
from inputs cross join prep
connect by level <= 12;
MTH CNT
-------- ----
Jan-2015 4
Feb-2015 4
Mar-2015 4
Apr-2015 5
May-2015 4
Jun-2015 4
Jul-2015 5
Aug-2015 4
Sep-2015 5
Oct-2015 4
Nov-2015 4
Dec-2015 5
12 rows selected.
The last wednesday of the month is given by:
TRUNC( NEXT_DAY( LAST_DAY( :month ) - INTERVAL '7' DAY, 'WEDNESDAY' ) )
The first wednesday of the month is given by:
NEXT_DAY( TRUNC( :month, 'MM' ) - INTERVAL '1' DAY, 'WEDNESDAY' )
Subtracting gives the number of days between them. Divide by 7 and add 1 and you get the number of Wednesdays:
SELECT ( TRUNC( NEXT_DAY( LAST_DAY( :month ) - INTERVAL '7' DAY, 'WEDNESDAY' ) )
- NEXT_DAY( TRUNC( :month, 'MM' ) - INTERVAL '1' DAY, 'WEDNESDAY' )
) / 7 + 1
AS number_of_wednesdays
FROM DUAL;
Or you can just use the difference between the first Wednesday of the month and of the next month as suggested by #mathguy
SELECT ( NEXT_DAY(
ADD_MONTHS( TRUNC( :month, 'MM' ), 1 ) - INTERVAL '1' DAY,
'WEDNESDAY'
)
- NEXT_DAY(
TRUNC( :month, 'MM' ) - INTERVAL '1' DAY,
'WEDNESDAY'
)
) / 7
AS number_of_wednesdays
FROM DUAL;

Between MOnths In oracle when date is greater than 10 of every month

My question is basically i want to increment months by one USING MONTHS_BETWEEN IN ORACLE
when date is greater than 10 of every month my query is :
CASE
when
TRUNC( months_between(TO_DATE(K.RECORD_DATE,'DD/MM/YYYY'),TO_DATE(K.DUE_DATE,'DD/MM/YYYY')) ) <= 0 then 0
when
--to_number(to_char(K.RECORD_DATE,'dd')) >10
TO_NUMBER(TO_CHAR( TO_DATE(k.RECORD_DATE,'DD/MM/YYYY'),'DD')) > 10
then
TRUNC( months_between(K.RECORD_DATE,K.DUE_DATE) )+1
else
TRUNC( months_between(K.RECORD_DATE,K.DUE_DATE) )
end as mths
FROM
TBL_PAYMENT_DTL K
Use to_date() in months_between().
select
CASE
when
TRUNC( months_between(TO_DATE(K.RECORD_DATE,'DD/MM/YYYY'),TO_DATE(K.DUE_DATE,'DD/MM/YYYY')) ) <= 0 then 0
when
--to_number(to_char(K.RECORD_DATE,'dd')) >10
TO_NUMBER(TO_CHAR( TO_DATE(k.RECORD_DATE,'DD/MM/YYYY'),'DD')) > 10
then
TRUNC( months_between(TO_DATE(k.RECORD_DATE,'DD/MM/YYYY'),TO_DATE(k.DUE_DATE,'DD/MM/YYYY')) )+1
else
TRUNC( months_between(TO_DATE(k.RECORD_DATE,'DD/MM/YYYY'),TO_DATE(k.DUE_DATE,'DD/MM/YYYY')) )
end as mths
FROM
TBL_PAYMENT_DTL K
You can use the EXTRACT function to get the day part of the date, as below:
SELECT
CASE
when
extract(day from K.RECORD_DATE) > 10 then K.RECORD_DATE
else
add_months(K.RECORD_DATE, 1)
end as mths
from
TBL_PAYMENT_DTL K

Resources