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;
/
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
Is the following the correct way to get a distinct list of days for a date range (min and max of a date field) I intend to create a sql view out of this:
with range as (
select min(date) start_date,
max(date) end_date
from table
)
select start_date + level - 1 AS "DATE",
extract(month from start_date + level - 1) AS "MONTH",
extract(year from start_date + level - 1) AS "YEAR"
from range
connect by level <= (
trunc(end_date) - trunc(start_date) + 1
);
Do you really need to create a DATE table when you can generate one on the fly if needed
ALTER SESSION SET NLS_DATE_FORMAT = 'DD-MON-YYYY';
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;
/
SELECT
c.COLUMN_VALUE
FROM
TABLE(generate_dates_pipelined(DATE '2022-07-01',
DATE '2022-07-31')) c
I have this common function which counts business days between two dates.
BUS_DAY := TRUNC(TO_DATE(P_START_DATE, D_FORMAT));
DATES_DIFF := TRUNC(TO_DATE(P_END_DATE, D_FORMAT)) - BUS_DAY;
SELECT MAX(RNUM) INTO T_DAYS
FROM (
SELECT ROWNUM RNUM
FROM ALL_OBJECTS
)
WHERE ROWNUM <= DATES_DIFF
AND TO_CHAR(BUS_DAY + RNUM, 'DY') NOT IN ('SAT', 'SUN');
My problem is it gives an incorrect day of the week for dates.
For example, today is Oct 7 2020 WEDNESDAY, but the function reads this date as a MONDAY, so it gives incorrect number of business days T_T
Anyone have the same issue or have any idea why oracle is reading dates incorrectly?
You can calculate the value without having to use a row generator and independent of the NLS_DATE_LANGUAGE.
Adapted from my answer here (which is the same problem but also ignoring holidays):
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) and multiply by 5/7 to give the week days of the full weeks; then
Add the day of the week (Monday = 1, Tuesday = 2, etc., to a maximum of 5 to ignore weekends) to count the part week for the end date; and
Subtract the day of the week of the start date to remove the counted values beforehand.
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
If you are just calculating for a single value in a function then you can avoid a context-switch to SQL and do it all in PL/SQL:
CREATE FUNCTION count_weekdays_between(
p_start_date IN DATE,
p_end_date IN DATE
) RETURN NUMBER DETERMINISTIC
IS
BEGIN
RETURN ( TRUNC( p_end_date, 'IW' ) - TRUNC( p_start_date, 'IW' ) ) * 5 / 7
+ LEAST( p_end_date - TRUNC( p_end_date, 'IW' ) + 1, 5 )
- LEAST( p_start_date - TRUNC( p_start_date, 'IW' ) + 1, 5 );
END;
/
and:
SELECT count_weekdays_between( DATE '2020-09-29', DATE '2020-10-07' )
AS num_week_days
FROM DUAL;
Outputs: 6
db<>fiddle here
Using SELECT ... FROM ALL_OBJECTS is a really ugly workaround.
What about this proposal?
DECLARE
BUS_DAY DATE := TRUNC(SYSDATE - 10);
DATES_DIFF INTEGER := TRUNC(SYSDATE - BUS_DAY);
T_DAYS INTEGER;
BEGIN
SELECT SUM(1)
INTO T_DAYS
FROM dual
WHERE TO_CHAR(BUS_DAY + LEVEL, 'DY', 'NLS_DATE_LANGUAGE = American') NOT IN ('SAT', 'SUN')
CONNECT BY LEVEL <= DATES_DIFF;
DBMS_OUTPUT.PUT_LINE ( 'T_DAYS = ' || T_DAYS );
END;
Looking just at why you see Monday, and ignoring whether this is a good approach - which #MTO has covered - then if p_start_date and p_end_date are dates, using to_date() on them is a bug, as #a_horse_with_no_name said in a comment.
If your NLS settings have YY or RR and d_format is using YYYY then today's date would end up as 0020-10-07, which was a Monday.
As a demo:
declare
P_START_DATE date := date '2020-10-07';
D_FORMAT varchar2(11) := 'DD-MON-YYYY';
BUS_DAY date;
begin
dbms_output.put_line(P_START_DATE || ' => ' || to_char(P_START_DATE, 'SYYYY-MM-DD Day'));
BUS_DAY := TRUNC(TO_DATE(P_START_DATE, D_FORMAT));
dbms_output.put_line(BUS_DAY || ' => ' || to_char(BUS_DAY, 'SYYYY-MM-DD Day'));
end;
/
07-OCT-20 => 2020-10-07 Wednesday
07-OCT-20 => 0020-10-07 Monday
When you do:
BUS_DAY := TRUNC(TO_DATE(P_START_DATE, D_FORMAT));
it's really:
BUS_DAY := TRUNC(TO_DATE(TO_CHAR(P_START_DATE), D_FORMAT));
and that implicit TO_CHAR(P_START_DATE) is using your NLS settings, so it's something like:
BUS_DAY := TRUNC(TO_DATE(TO_CHAR(P_START_DATE, 'DD-MON-RR'), D_FORMAT));
You end up with that intermediate string value as '07-OCT-20'. If you convert that back to a date with a YYYY year component in the format mask then the year is seen as 0020, not 2020:
select to_char(to_date('07-OCT-20', 'DD-MON-YYYY'), 'DD-MON-YYYY') from dual;
07-OCT-0020
You don't need to convert to and from a string, and you're already truncating to set any time part to midnight, so you only need that part:
BUS_DAY := TRUNC(P_START_DATE);
db<>fiddle
Some clients use their own display preferences rather than NLS settings, so you may be seeing the date as 07-Oct-2020 when you query, while the NLS setting has YY or RR. You can query nls_session_parameters to check.
I have table
CREATE TABLE T_TEST
( KURS_NUMBER NUMBER PRIMARY KEY,
KURS_ID NUMBER NOT NULL,
DATEKURS DATE NOT NULL,
CONSTRAINT UNIQUE2 UNIQUE
(KURS_ID,DATEKURS)
);
TRIGGER for kurs_number
create or replace trigger TR_INSERT_TEST01
before insert on test01
FOR EACH ROW
declare
-- local variables here
begin
IF :NEW.KURS_NUMBER IS NULL
THEN SELECT SEQ_TEST.NEXTVAL INTO :NEW.KURS_NUMBER FROM DUAL;
END IF;
end TR_INSERT_T_TEST;
How can I insert data to kurs_id which will contain only one digit '1'
and datekurs will contain date in order period from 2017year 1 january to 31 december 2017year ( or random date )
connect by level 365
This code works very well but if I want to use my trigger for new column kurs_number it doesn't work due (not enough values). I guess that should be in different way.
insert into t_test
select 1
, date '2017-01-01' + (level-1)
from dual
connect by level <= 365
/
This trick generates 365 rows. We can do arithmetic with dates so adding (level-1) to a root date generates 365 dates.
insert into t_test
select SEQ_TEST.NEXTVAL
, 1
, date '2017-01-01' + (level-1)
from dual
connect by level <= 365
/
"not enough values"
You changed the structure of the target table so you needed to change the projection of the query to match. The revised version includes the additional primary key column.
"i want to use my trigger for new column kurs_number"
You can make this a procedure with parameters for kurs_id and the target year.
As a bonus this code handles leap years correctly.
create or replace procedure generate_kurs_year_recs
( p_kurs_id in number
, p_kurs_year in varchar2 )
is
last_dayno number;
begin
/* find number of days in year */
select to_number(to_char(to_date( p_kurs_year||'-12-31', 'yyyy-mm-dd')
, 'DDD'))
into last_dayno
from dual;
/* generate records for year */
insert into t_test
select SEQ_TEST.NEXTVAL
, p_kurs_id
, to_date( p_kurs_year||'-01-01', 'yyyy-mm-dd') + (level-1)
from dual
connect by level <= last_dayno;
end generate_kurs_year_recs;
/
Call the procedure like this:
begin
generate_kurs_year_recs(1,'2017');
end;
To call from a trigger you will need to pass parameters somehow, presumably using values from the trigger's table.
This will insert all dates in order
insert into t_test (kurs_id, datekurs)
with CTE (DD) as
(
select to_date('20170101','YYYYMMDD') as DD
from dual
union
select DD +1
from CTE
where DD < to_date('20171231','YYYYMMDD')
)
select row_number() over(order by DD) as kurs_id, DD as datekurs
from CTE
Given 2 dates (StartDate and EndDate), how to do i generate quarterly periods in Pl/SQL.
Example:
Start Date: 01-JAN-2009
End Date: 31-DEC-2009
Expected Output:
StartDate EndDate
01-JAN-2009 31-MAR-2009
01-APR-2009 30-JUN-2009
01-JUL-2009 30-SEP-2009
01-OCT-2009 31-DEC-2009
SELECT ADD_MONTHS( TRUNC(PARAM.start_date, 'Q'), 3*(LEVEL-1) ) AS qstart
, ADD_MONTHS( TRUNC(PARAM.start_date, 'Q'), 3*(LEVEL) ) -1 AS qend
FROM ( SELECT TO_DATE('&start_date') AS start_date
, TO_DATE('&end_date') AS end_date
FROM DUAL
) PARAM
CONNECT BY ADD_MONTHS( TRUNC(PARAM.start_date, 'Q'), 3*(LEVEL) ) -1
<= PARAM.end_date
Rules for params, you may need to adjust the query to suit your purposes:
If start_date is not exact quarter start it effectively uses the quarter contain start date.
If end_date is not exact quarter end then we end on the quarter that ended BEFORE end_date (not the one containing end date).
Here's one way that you can do it with PL/SQL
declare
startDate Date := '01-JAN-2009';
endDate Date := '31-DEC-2009';
totalQuarters number := 0;
begin
totalQuarters := round(months_between(endDate, startDate),0)/3;
dbms_output.put_line ('total quarters: ' || totalQuarters);
for i in 1..totalQuarters loop
dbms_output.put_line('start date: '|| startDate || ' end date:' || add_months(startDate -1,3));
startDate := add_months(startDate,3) ;
end loop;
end;