I'm trying to execute this SQL in Oracle:
select z.*
from (select (CASE
WHEN trunc(to_date('01.02.2015', 'DD.MM.YY'), 'MM') =
to_date('01.02.2015', 'DD.MM.YY') THEN
trunc(ADD_MONTHS(sysdate, -1), 'MM')
ELSE
trunc(sysdate - 1)
END) as sd,
trunc(sysdate) ed
from dual) t,
(SELECT *
FROM table(SOMEOWNERUSER.SOMEPACKAGE.getPipelinedTable(to_char(t.sd,
'dd.mm.yyyy'),
to_char(t.ed,
'dd.mm.yyyy')))) z
But I get error ORA-00904: "T"."SD": invalid identifier. What am I doing wrong?
You just need to directly reference the table() in the same level join as the t subquery:
create type dt_type as object (dt date);
/
create type dt_tab as table of dt_type;
/
create or replace function getpipelinedtable (p_sd date, p_ed date)
return dt_tab
as
l_tab dt_tab := dt_tab();
begin
for i in 1 .. (p_ed - p_sd) + 1
loop
l_tab.extend;
l_tab(l_tab.last) := dt_type(p_sd -1 + i);
end loop;
return l_tab;
end getpipelinedtable;
/
SELECT z.*
FROM (SELECT (CASE WHEN TRUNC (TO_DATE ('01.02.2015', 'DD.MM.YY'), 'MM') = TO_DATE ('01.02.2015', 'DD.MM.YY')
THEN TRUNC (ADD_MONTHS (SYSDATE, -1), 'MM')
ELSE TRUNC (SYSDATE - 1)
END) AS sd,
TRUNC (SYSDATE) ed
FROM DUAL) t,
TABLE ( getpipelinedtable (t.sd, t.ed) ) z;
DT
----------
01/01/2015
02/01/2015
03/01/2015
04/01/2015
05/01/2015
<snip>
06/02/2015
07/02/2015
08/02/2015
09/02/2015
drop function getpipelinedtable;
drop type dt_tab;
drop type dt_type;
Related
I come from SQL Server and some times I'm not familiar to Oracle syntax, I want to create a function that takes a date and number of dates as a parameters and create a table function.
My original query is:
VAR TREND = 1;
VAR OBS_DATE = 20221109;
VAR N_DAYS = 21;
WITH CAL AS
(
SELECT
TO_DATE(:OBS_DATE, 'YYYYMMDD') + (LEVEL - 1 * :TREND) DT, ROW_NUMBER() OVER(ORDER BY NULL) - 1 IX
FROM
DUAL
WHERE
TO_CHAR(TO_DATE(:OBS_DATE, 'YYYYMMDD') + (LEVEL - 1 * :TREND) , 'D') NOT IN (1,7)
CONNECT BY LEVEL <= :N_DAYS + :N_DAYS/5*2+1
)
SELECT DT
FROM CAL
WHERE IX <= :N_DAYS;
But when I try to convert as a function it sends me an error and I don't know what the correct syntax is.
My attempt is:
CREATE OR REPLACE FUNCTION FUN_BUS_CALENDAR(
OBS_DATE IN DATE := SYSDATE
, NDAYS IN NUMBER
, TREND IN NUMBER
)
RETURN OBS_DATE DATE;
BEGIN
WITH CAL AS(
SELECT
TO_DATE(:OBS_DATE, 'YYYYMMDD') + (LEVEL - 1 * :TREND) OBS_DATE, ROW_NUMBER() OVER(ORDER BY NULL) - 1 IX
FROM DUAL
WHERE TO_CHAR(TO_DATE(:OBS_DATE, 'YYYYMMDD') + (LEVEL - 1 * :TREND) , 'D') NOT IN (1,7)
CONNECT BY LEVEL <= :N_DAYS + :N_DAYS/5.*2.+1.
)
SELECT OBS_DATE FROM CAL WHERE IX <= :N_DAYS
RETURN OBS_DATE
END
/
You should probably just use the initial query.
However, if you did want a function then you can use a pipelined function:
CREATE FUNCTION BARRRAF.FUN_BUS_CALENDAR(
OBS_DATE IN DATE := SYSDATE,
NDAYS IN NUMBER,
TREND IN NUMBER
) RETURN SYS.ODCIDATELIST PIPELINED
IS
BEGIN
FOR i IN 1 .. ndays LOOP
PIPE ROW( obs_date + i - trend );
END LOOP;
END;
/
Then if you want to generate a row number then just use a sub-query:
SELECT column_value AS obs_date,
ROWNUM - 1 AS rn
FROM TABLE(BARRRAF.FUN_BUS_CALENDAR(ndays => 3, trend=>1))
Which outputs:
OBS_DATE
RN
2022-11-17 23:40:22
0
2022-11-18 23:40:22
1
2022-11-19 23:40:22
2
fiddle
Here is a generic function that can be used to create a calendar for the following INTERVALs seconds, minutes, hours or days.
You can pass it any start and END date/time you like. The lower or higher date can go in any position as there is logic to figure out which is what least/greatest command
CREATE OR REPLACE FUNCTION generate_dates(i_from_dat IN TIMESTAMP, i_to_dat IN TIMESTAMP, i_interval IN NUMBER, i_interval_type IN VARCHAR2)
RETURN VARCHAR2
SQL_MACRO
IS
BEGIN
RETURN q'~SELECT LEAST(i_from_dat,i_to_dat) + NUMTODSINTERVAL( (LEVEL-1)*i_interval, i_interval_type ) AS dt
FROM DUAL
CONNECT BY LEAST(i_from_dat,i_to_dat) + NUMTODSINTERVAL( (LEVEL-1)*i_interval, i_interval_type) < GREATEST(i_from_dat, i_to_dat)~';
END ;
SELECT * FROM generate_dates(
TIMESTAMP '2022-11-03 09:47:31',
TIMESTAMP '2022-11-03 12:37:11',
30, 'MINUTE') ;
DT
03-NOV-22 09.47.31.000000 AM
03-NOV-22 10.17.31.000000 AM
03-NOV-22 10.47.31.000000 AM
03-NOV-22 11.17.31.000000 AM
03-NOV-22 11.47.31.000000 AM
03-NOV-22 12.17.31.000000 PM
SELECT * FROM generate_dates(
TIMESTAMP '2022-11-03 00:00:00',
TIMESTAMP '2022-11-08 00:00:00',
1, 'DAY') ;
DT
03-NOV-22 12.00.00.000000 AM
04-NOV-22 12.00.00.000000 AM
05-NOV-22 12.00.00.000000 AM
06-NOV-22 12.00.00.000000 AM
07-NOV-22 12.00.00.000000 AM
I need to insert only date into a table MONTH_YEAR from 01-01-2010 to 01-01-2040:
For example, I need to insert record on my table month wise
DATE:
01-01-2010
01-02-2010
01-03-2010
01-04-2010
01-05-2010
01-06-2010
01-07-2010
01-08-2010
01-09-2010
01-10-2010
01-11-2010
01-12-2010
01-01-2011
01-02-2011
01-03-2011
01-04-2011
01-05-2011
.....................................
01-06-2040
01-07-2040
01-08-2040
01-09-2040
01-10-2040
01-11-2040
01-12-2040
Like this I want to insert only date into my table for month wise from 01-01-2010 to 01-01-2040
You can use a hierarchical query:
INSERT INTO month_year (column_name)
SELECT ADD_MONTHS(DATE '2010-01-01', LEVEL - 1)
FROM DUAL
CONNECT BY LEVEL <= 31*12;
Or a recursive query:
INSERT INTO month_year (column_name)
WITH range (dt) AS (
SELECT DATE '2010-01-01' FROM DUAL
UNION ALL
SELECT ADD_MONTHS(dt, 1)
FROM range
WHERE dt < DATE '2040-12-01'
)
SELECT dt FROM range;
db<>fiddle here
Row generator it is:
SQL> insert into month_year (datum)
2 select date '2010-01-01' + level - 1
3 from dual
4 connect by level <= date '2040-01-01' - date '2010-01-01' + 1;
10958 rows created.
SQL> select min(datum) min_date,
2 max(datum) max_date
3 from month_year;
MIN_DATE MAX_DATE
---------- ----------
01.01.2010 01.01.2040
SQL>
If you only need 1st of every month, then
SQL> insert into month_year (datum)
2 select add_months(date '2010-01-01', level - 1)
3 from dual
4 connect by level <= months_between(date '2040-01-01', date '2010-01-01') + 1;
361 rows created.
SQL>
Do you really need 40 years of dates, it seems unlikely or can you make due with a virtual calendar, where you specify a start and end_date and all the dates are generated for you. This example does every day in the range but feel free to modify it to produce what you need. I use it in several places.
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;
/
I need to find the 'n'th business day in oracle plsql function which should exclude weekends and custom list of holidays. I got something like this however unable to fit the custom holiday logic in here,
create or replace function add_n_working_days (
start_date date, working_days pls_integer
) return date as
end_date date := start_date;
counter pls_integer := 0;
begin
if working_days = 0 then
end_date := start_date;
elsif to_char(start_date, 'fmdy') in ('sat', 'sun') then
end_date := next_day(start_date, 'monday');
end if;
while (counter < working_days) loop
end_date := end_date + 1;
if to_char(end_date, 'fmdy') not in ('sat', 'sun') then
counter := counter + 1;
end if;
end loop;
return end_date;
end add_n_working_days;
/
I have the custom holiday list in a different table which can be extracted using the sql
select holidays from holiday_table
I tried adding elsif condition with a sub query but that is not supported
if to_char(end_date, 'fmdy') not in ('sat', 'sun') then
counter := counter + 1;
elsif to_char(end_date, 'YYYYMMDD') in (select holidays from holiday_table) then
counter := counter + 1;
end if;
I will try with counting the number of weekend days with a similar loop:
WHILE (v_copy > 0) LOOP
end_date := end_date + 1;
IF to_char(end_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := end_date + 1;
ELSE
v_copy := v_copy - 1;
END IF;
END LOOP;
And then after the looping process you can count the number of holidays in the table which are after the start_date and end_date and not in ('sat', 'sun').
SELECT COUNT(*) INTO v_custom_cnt
FROM holiday_table
WHERE to_char(holidays, 'fmdy') not in ('sat', 'sun') AND
holidays BETWEEN start_date AND end_date;
Now add this number to your end_date and you will get the date of the nth business day. Of course before returning check if the new end_date is in ('sat', 'sun').
end_date := end_date + v_custom_cnt;
IF to_char(holidays, 'fmdy') in ('sat', 'sun') THEN
end_date := next_day(end_date, 'monday');
END IF;
Finally the end_date is the date of the nth working day.
CREATE OR REPLACE FUNCTION add_n_working_days (
start_date DATE,
working_days PLS_INTEGER
) RETURN DATE AS
end_date DATE := start_date;
counter PLS_INTEGER := 0;
v_copy PLS_INTEGER := working_days;
v_custom_cnt INTEGER := 0;
BEGIN
IF working_days = 0 THEN
end_date := start_date;
ELSIF to_char(start_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := next_day(start_date, 'monday');
END IF;
WHILE (v_copy > 0) LOOP
end_date := end_date + 1;
IF to_char(end_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := end_date + 1;
ELSE
v_copy := v_copy - 1;
END IF;
END LOOP;
SELECT COUNT(*) INTO v_custom_cnt
FROM holiday_table
WHERE to_char(end_date, 'fmdy') NOT IN ('sat', 'sun')
AND holidays BETWEEN start_date AND end_date;
end_date := end_date + v_custom_cnt;
IF to_char(end_date, 'fmdy') IN ('sat', 'sun') THEN
end_date := next_day(start_date, 'monday');
END IF;
RETURN end_date;
END add_n_working_days;
It is not 100% tested
Different approach: create a table that only lists business day and take nth value:
CREATE OR REPLACE FUNCTION add_n_working_days (
start_date DATE, working_days PLS_INTEGER
) RETURN DATE AS
l_end_date DATE := start_date;
l_counter pls_integer := 0;
BEGIN
SELECT
business_day
INTO l_end_date
FROM
(
WITH
dates AS
(SELECT start_date + level - 1 as dt FROM dual CONNECT BY level < 100)
,weekdates AS
(SELECT dt as weekday FROM dates WHERE TO_CHAR(dt,'fmdy') NOT IN ('sat','sun'))
,business_days AS
(
SELECT weekday as business_day FROM weekdates
MINUS
SELECT holiday FROM so_holidays
)
SELECT business_day, ROW_NUMBER() OVER (ORDER BY 1) as rn from business_days
)
WHERE rn = working_days + 1;
RETURN l_end_date;
END add_n_working_days;
SQL is set orientated so start thinking in terms of sets NOT iteration (no loops). Assuming you can designate a maximum number of days to be tested you can get what you want with a single simple query.
create or replace function add_n_working_days (
start_date date, working_days pls_integer
) return date as
working_date_ot date;
begin
with date_list as (select trunc(start_date) + level - 1 tdate
from dual
connect by level <= 5*working_days
)
select tdate
into working_date_ot
from ( select tdate, row_number() over(order by tdate) date_num
from date_list
where to_char(tdate,'fmdy') not in ('sat','sun')
and not exists
(select null
from holiday_dates
where trunc(holiday_date) = tdate
)
)
where date_num = working_days;
return working_date_ot;
end add_n_working_days;
How it works:
The function assumes the desired date exists within 5 times the number of days requested. So if 10 business hence are requested the resulting date will be within 50 days (that would be a whole lot of holidays if not).
The date_list CTE builds a list of potential dates out to the identified limit.
The sub select in the main filters the generated list eliminating Sat and Sun. It then probes the holiday table for the remaining dates and eliminating any any date in the table.
Remaining dates are numbered based on ascending value.
The main outer select then chooses the date number matching the working_days specified.
No Looping required. Just my .02 cents worth.
I have this query and result:
SQL> select AVG(SYSDATE - DOB) AS AVERAGE_AGE_IN_DAYS
2 FROM MORTAL;
AVERAGE_AGE_IN_DAYS
-------------------
17877.44
Is there a way to convert this to years, months, and days, within the same query?
To get the number of months you could use months_between
SELECT select AVG(months_between(SYSDATE,DOB)) AS AVERAGE_AGE_IN_MONTHS
For years, just do months_between/12
SELECT select AVG(months_between(SYSDATE,DOB)/12) AS AVERAGE_AGE_IN_YEARS
For weeks, i would just take your result for days and divide it by 7.
You might have to handle the results to fit your needs, but i think this should cover it.
How about:
select
floor(months_between(SYSDATE,DOB)/12) AS years,
trunc( months_between(SYSDATE,DOB) ) AS months,
SYSDATE - add_months( SYSDATE, trunc(months_between(SYSDATE,DOB)) ) AS days
This is years,months,days as the detail suggests. The title said weeks too but I'm not if you actually want them.
Perhaps your query would be:
with dates as (select AVG(SYSDATE - DOB) as d --average_in_days
from MORTAL
)
select trunc(d/365) as years
, trunc((d/365 - trunc(d/365)) * 12 ) as months
, trunc((((d/365 - trunc(d/365)) * 12) - trunc((d/365 - trunc(d/365)) * 12 )) * 4.348214) as weeks
, trunc(((((d/365 - trunc(d/365)) * 12) - trunc((d/365 - trunc(d/365)) * 12 )) * 4.348214
- trunc((((d/365 - trunc(d/365)) * 12) - trunc((d/365 - trunc(d/365)) * 12 )) * 4.348214 )
) * 7
) as days
from dates;
CREATE FUNCTION time_spell(p_days VARCHAR2) RETURN VARCHAR2 IS
v_y NUMBER;
v_yd NUMBER;
v_m NUMBER;
v_md NUMBER;
v_d NUMBER;
v_w NUMBER;
v_wd NUMBER;
v_days NUMBER;
v_yi NUMBER;
v_mi NUMBER;
v_wi NUMBER;
BEGIN
SELECT p_days/365 INTO v_y FROM dual;
if v_y not like '%.%' then
SELECT trunc(v_y) INTO v_yi FROM dual;
select substr(v_y||'.0',(select instr(v_y||'.0','.') from dual)) INTO v_yd from dual;
SELECT v_yd * 12 INTO v_m FROM dual;
SELECT trunc(v_m) INTO v_mi FROM dual;
select substr(v_m,(select instr(v_m,'.') from dual)) INTO v_md from dual;
SELECT v_md * 30 INTO v_d FROM dual;
SELECT v_d / 7 INTO v_w FROM dual;
SELECT trunc(v_w) INTO v_wi FROM dual;
select substr(v_w,(select instr(v_w,'.') from dual)) INTO v_wd from dual;
SELECT round(v_wd * 7) INTO v_days FROM dual;
RETURN v_yi||' Year(s) '||v_mi||' Month(s) '||v_wi||' Week(s) '||v_days||' Day(s) ';
else
SELECT trunc(v_y) INTO v_yi FROM dual;
select substr(v_y,(select instr(v_y,'.') from dual)) INTO v_yd from dual;
SELECT v_yd * 12 INTO v_m FROM dual;
SELECT trunc(v_m) INTO v_mi FROM dual;
select substr(v_m,(select instr(v_m,'.') from dual)) INTO v_md from dual;
SELECT v_md * 30 INTO v_d FROM dual;
SELECT v_d / 7 INTO v_w FROM dual;
SELECT trunc(v_w) INTO v_wi FROM dual;
select substr(v_w,(select instr(v_w,'.') from dual)) INTO v_wd from dual;
SELECT round(v_wd * 7) INTO v_days FROM dual;
RETURN v_yi||' Year(s) '||v_mi||' Month(s) '||v_wi||' Week(s) '||v_days||' Day(s) ';
end if;
END;
SELECT time_spell(17877.44) FROM dual;
I've thougth this way to implement a parametrizable query.
Do you know any variant?
WITH temp AS (SELECT 'case1' case FROM DUAL)
SELECT 1
FROM temp
WHERE ( (1 = DECODE (case, 'case1', 1, 0))
AND SYSDATE > TO_DATE ('01/01/2013', 'DD/MM/YYYY'))
OR ( (1 = DECODE (case, 'case2', 1, 0))
AND SYSDATE < TO_DATE ('01/01/2013', 'DD/MM/YYYY'))
you can use case
WITH temp AS (SELECT 'case1' _case FROM DUAL)
SELECT 1
FROM temp
WHERE
1 = case
when _case = 'case1'
AND SYSDATE > TO_DATE ('01/01/2013', 'DD/MM/YYYY')
then 1
when _case = 'case2'
AND SYSDATE > TO_DATE ('01/01/2013', 'DD/MM/YYYY')
then 1
else 0
end
;
This seems awkward. You can do this with just basic logic:
WITH temp AS (SELECT 'case1' case FROM DUAL)
SELECT 1
FROM temp
WHERE ((case = 'case1') and SYSDATE > TO_DATE('01/01/2013', 'DD/MM/YYYY')) or
((case = 'case2') and SYSDATE < TO_DATE('01/01/2013', 'DD/MM/YYYY'))
Why not use a function:
CREATE FUNCTION x ( p_case IN VARCHAR2, p_date IN VARCHAR2 )
RETURN INTEGER
IS
BEGIN
IF p_case = 'case1' AND TO_DATE(p_date, 'DD/MM/YYYY') < SYSDATE THEN RETURN 1;
ELSE IF p_case = 'case2' AND TO_DATE(p_date, 'DD/MM/YYYY') < SYSDATE THEN RETURN 1;
ELSE RETURN 0;
END IF;
END;
SELECT 1 FROM dual WHERE x('case1', '01/01/2013') = 1;