Related
I have a problematic SQL Sentence, when it runs
SELECT code, my_date, my_time
FROM my_table
WHERE to_date(to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time, 'YYYY-MM-DD HH24:MI:SS') > sysdate - 5
I always get: ORA-01841: (full) year must be between -4713 and +9999, and not be 0
This is the definition of my table:
CREATE TABLE my_table (code NUMBER(10), my_date DATE, my_time VARCHAR2(8));
ALTER TABLE my_table ADD CONSTRAINT pk_my_table PRIMARY KEY (code);
CREATE INDEX i_my_table_001 ON my_table (my_date);
But If I add an extra restriction, I never get the error:
SELECT code, my_date, my_time
FROM my_table
WHERE my_date > trunc(sysdate - 5)
AND to_date(to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time, 'YYYY-MM-DD HH24:MI:SS') > sysdate - 5
All validations are ok. Not invalid dates, not invalid times
Is there any way to find which is the 'offending' record?
Brute force is not an option because table has more than 25M records.
If you are on Oracle 12.2 or higher, you can use the VALIDATE_CONVERSION function to see which rows could be giving you an error.
Example Query
WITH
my_table (code, my_date, my_time)
AS
(SELECT 1, date '2020-01-01', '08:00:00' FROM DUAL
UNION ALL
SELECT 2, date '2020-01-31', '25:00:00' FROM DUAL)
SELECT *
FROM my_table
WHERE validate_conversion (to_char(my_date, 'YYYY-MM-DD') || ' ' || my_time AS DATE, 'YYYY-MM-DD HH24:MI:SS') = 0;
Result
CODE MY_DATE MY_TIME
_______ ______________ ___________
2 2020-01-31 25:00:00
Update
One update that may be a bit easier than cloning your database and upgrading is to create your own function to validate the date and call that from a test SQL statement like the on below. I'm not sure if defining a function within a common table expression is supported in 12.1, but if it is not, you can make it a standalone function and call that from your query.
WITH
FUNCTION validate_date (p_date DATE, p_time VARCHAR2)
RETURN NUMBER
AS
l_test_date DATE;
BEGIN
l_test_date :=
TO_DATE (TO_CHAR (p_date, 'YYYY-MM-DD') || ' ' || p_time, 'YYYY-MM-DD HH24:MI:SS');
RETURN 1;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END;
SELECT *
FROM (SELECT 1 AS code, DATE '2020-01-01' AS my_date, '08:00:00' AS my_time FROM DUAL
UNION ALL
SELECT 2, DATE '2020-01-31', '25:00:00' FROM DUAL) my_table
WHERE validate_date (my_date, my_time) = 0;
I wrote the below code but I did not get the expected output:
SQL> select TO_CHAR(TO_CHAR(sysdate,'MM')-1,'DD-MM-YYYY') AS PREV_MON_FIRST,
TO_CHAR(TO_CHAR(sysdate,'MM'),'DD-MM-YYYY')-1 AS PREV_MON_LAST from dual;
select TO_CHAR(TO_CHAR(sysdate,'MM')-1,'DD-MM-YYYY') AS PREV_MON from dual
*
ERROR at line 1:
ORA-01481: invalid number format model
It's correct that 'DD' and 'YYYY' values are missing but when I tried to retrieve only month also it shows the same error
You can use the following solution:
SELECT
TRUNC(LAST_DAY(ADD_MONTHS(sysdate, -2))) + 1 AS first_date,
TRUNC(LAST_DAY(ADD_MONTHS(sysdate, -1))) AS last_date
FROM dual
Selecting the first day of the previous month using TO_CHAR only is easy, if YYYYMMDD format suits your needs:
SQL> select (to_char(sysdate, 'yyyymm') - 1) || '01' first_day
2 from dual;
FIRST_DAY
------------------------------------------
20180701
SQL>
But, for the last day - I have no idea.
Usual way to do it works, but involves other functions:
SQL> select trunc(add_months(sysdate, -1), 'mm') first_day,
2 trunc(sysdate, 'mm') - 1 last_day
3 from dual;
FIRST_DAY LAST_DAY
---------- ----------
2018-07-01 2018-07-31
SQL>
For LAST_DAY here is the query with hard_code of values of month without using add_months,trunc,to_date
select to_char(sysdate,'yyyy')||
to_char(sysdate,'mm')-1||
case when to_char(sysdate,'mm')-1 in (1,3,5,7,8,10,12) THEN 31
when to_char(sysdate,'mm')-1 in (4,6,9,11) THEN 30
when mod(to_char(sysdate,'yyyy'),4)=0 and to_char(sysdate,'mm')-1=2 THEN 29
when mod(to_char(sysdate,'yyyy'),4)!=0 and to_char(sysdate,'mm')-1=2 THEN 28
end
from dual
If I take your question literally one solution is
CREATE OR REPLACE FUNCTION MY_LAST_DAY RETURN TIMESTAMP AS
next_run_date TIMESTAMP;
return_date_after TIMESTAMP := SYSTIMESTAMP - INTERVAL '100' DAY;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=DAILY;INTERVAL=1;BYMONTHDAY=-1', NULL, return_date_after, next_run_date);
EXIT WHEN EXTRACT(MONTH FROM next_run_date) = EXTRACT(MONTH FROM SYSTIMESTAMP);
return_date_after := next_run_date;
END LOOP;
RETURN return_date_after;
END;
/
CREATE OR REPLACE FUNCTION MY_FIRST_DAY RETURN TIMESTAMP AS
next_run_date TIMESTAMP;
return_date_after TIMESTAMP := SYSTIMESTAMP - INTERVAL '100' DAY;
BEGIN
LOOP
DBMS_SCHEDULER.EVALUATE_CALENDAR_STRING('FREQ=DAILY;INTERVAL=1;BYMONTHDAY=1', NULL, return_date_after, next_run_date);
EXIT WHEN EXTRACT(MONTH FROM next_run_date) = EXTRACT(MONTH FROM SYSTIMESTAMP);
return_date_after := next_run_date;
END LOOP;
RETURN return_date_after;
END;
/
It returns the first and last date of previous month without using TO_DATE (or TO_TIMESTAMP), TRUNC or ADD_MONTHS
But I hope you agree that your question is rather stupid.
For LAST_DAY here is the query with hard_code of values of month without using add_months,trunc,to_date
select to_char(sysdate,'yyyy')||
to_char(sysdate,'mm')-1||
case when to_char(sysdate,'mm')-1 in (1,3,5,7,8,10,12) THEN 31
when to_char(sysdate,'mm')-1 in (4,6,9,11) THEN 30
when mod(to_char(sysdate,'yyyy'),4)=0 and to_char(sysdate,'mm')-1=2 THEN 29
when mod(to_char(sysdate,'yyyy'),4)!=0 and to_char(sysdate,'mm')-1=2 THEN 28
end
from dual
New Query to handle Janurary and Leap year :
select decode(to_char(sysdate,'mm'),1,to_char(sysdate,'yyyy')-1 ,to_char(sysdate,'yyyy'))||
decode(to_char(sysdate,'mm'),1,12, to_char(sysdate,'mm')-1)||
CASE WHEN to_char(sysdate,'mm')-1 in (1,3,5,7,8,10,0) THEN 31
WHEN to_char(sysdate,'mm')-1 in (4,6,9,11) THEN 30
WHEN to_char(sysdate,'mm')-1=2 AND mod(to_char(sysdate,'yyyy'),4)!=0 AND mod(to_char(sysdate,'yyyy'),400)!=0 THEN 29
ELSE 28
END
FROM dual
How can i find what date will be in the next MONDAY if i have a current date(sysdate) and a current day of week?
p.s please tell me how to get date by day of week NOT day of week by date.
You can use the next_day function:
select next_day(sysdate, 'MONDAY') from dual;
In case the national settings are not English we can have Oracle generate the localized translation of "Monday" and use it like this:
SQL> set serveroutput on
SQL> alter session set nls_date_format="YYYY-MM-DD";
Session altered.
SQL> alter session set nls_date_language=italian;
Session altered.
SQL> declare
2 v_monday constant varchar2(100) := to_char(to_date('2013-09-30', 'yyyy-mm-dd'), 'day');
3 begin
4 dbms_output.put_line('v_monday = ' || v_monday);
5 dbms_output.put_line('next monday will be ' || next_day(sysdate, v_monday));
6 end;
7 /
v_monday = lunedi
next monday will be 2013-09-30
PL/SQL procedure successfully completed.
SQL> alter session set nls_date_language=spanish;
Session altered.
SQL> declare
2 v_monday constant varchar2(100) := to_char(to_date('2013-09-30', 'yyyy-mm-dd'), 'day');
3 begin
4 dbms_output.put_line('v_monday = ' || v_monday);
5 dbms_output.put_line('next monday will be ' || next_day(sysdate, v_monday));
6 end;
7 /
v_monday = lunes
next monday will be 2013-09-30
PL/SQL procedure successfully completed.
You can try this if you want to force giving current day and current date. Else you can make it more simpler with only date or with only month day.
WITH DATASET
AS (SELECT
'MON' AS CURRENT_DAY,
'2013-06-26' AS CURENT_DATE
FROM
DUAL)
SELECT
CASE
WHEN CURRENT_DAY = 'MON'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '7' DAY
WHEN CURRENT_DAY = 'TUE'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '6' DAY
WHEN CURRENT_DAY = 'WED'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '5' DAY
WHEN CURRENT_DAY = 'THU'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '4' DAY
WHEN CURRENT_DAY = 'FRI'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '3' DAY
WHEN CURRENT_DAY = 'SAT'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '2' DAY
WHEN CURRENT_DAY = 'SUN'
THEN
TO_DATE ( CURENT_DATE,
'yyyy-mm-dd' )
+ INTERVAL '1' DAY
END
AS DATE_OF_NEXT_MON
FROM
DATASET;
This depends on whether sunday is the first or last day of "your" week.
if your week starts with monday then use
select trunc(sysdate, 'iw') + 7 from dual;
if your week starts with sunday then use
select trunc(sysdate, 'w') + 8 from dual;
I have requirement where i have to get data of particular day. So my ideal startdate should be 2013-06-07 00:00:01 AM and end date 2013-06-07 23:59:59 AM
Hence i have written this code.
create or replace
PROCEDURE checkChanges
IS
vc_startDate timestamp;
vc_endDate timestamp;
begin
vc_startDate :=to_timestamp(TO_CHAR(trunc(systimestamp)-40+((24*60*60)-1)/(24*60*60),'yyyy-mm-dd hh24:mi:ss'),'yyyy-mm-dd hh24:mi:ss');
vc_endDate :=to_timestamp(TO_CHAR(trunc(systimestamp)+1/(24*60*60),'yyyy-mm-dd hh24:mi:ss'),'yyyy-mm-dd hh24:mi:ss');
Dbms_Output.Put_Line('vc_startDate ' ||vc_startDate);
Dbms_Output.Put_Line('vc_endDate ' ||vc_endDate);
SELECT EMAIL_ADRESS FROM SOMETABLE A,B
AND A.CREATE_TS BETWEEN vc_startDate AND vc_endDate ORDER BY B.START_DT;
end checkChanges;
But the start date and end date i am getting is quite different.
start date:07-JUN-13 12.00.01.000000 AM
end date: 07-JUN-13 11.59.59.000000 PM
Here's a simple way to do this.
DECLARE
v_start TIMESTAMP;
v_end TIMESTAMP;
BEGIN
v_start := TRUNC (SYSTIMESTAMP) + NUMTODSINTERVAL (1, 'second'); --truncate the timestamp and add one second
DBMS_OUTPUT.PUT_LINE (TO_CHAR (v_start, 'yyyy-mm-dd hh24:mi:ss'));
/*alternate way
v_start := TRUNC (SYSTIMESTAMP) + INTERVAL '0 0:0:1' DAY TO SECOND; --truncate the timestamp and add one second
DBMS_OUTPUT.PUT_LINE (TO_CHAR (v_start, 'yyyy-mm-dd hh24:mi:ss AM'));*/
v_end :=
TRUNC (SYSTIMESTAMP) --trunacate the timestamp
+ INTERVAL '1 0:0:0.0' DAY TO SECOND --add a day
- NUMTODSINTERVAL (1, 'second'); --substract a second
DBMS_OUTPUT.PUT_LINE (TO_CHAR (v_end, 'yyyy-mm-dd hh24:mi:ss'));
/*alternate way
v_end := TRUNC (SYSTIMESTAMP) --trunacate the timestamp
+ INTERVAL '0 23:59:59' DAY TO SECOND; --add hours, mins, s
DBMS_OUTPUT.PUT_LINE (TO_CHAR (v_end, 'yyyy-mm-dd hh24:mi:ss AM'));*/
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE (SQLERRM);
END;
Output:
2013-06-07 00:00:01 AM
2013-06-07 23:59:59 PM
UPDATE
I also printed the values without converting to to_char and it displayed similar to what u got.
DBMS_OUTPUT.PUT_LINE (v_start);
DBMS_OUTPUT.PUT_LINE (v_end);
Output:
07-JUN-13 12.00.01.000000 AM
07-JUN-13 11.59.59.000000 PM
So, it seems what you are doing is correct(but, little complicated). When displaying the timestamp it is getting displayed according to the NLS parameters. Check that using
SELECT *
FROM nls_session_parameters
WHERE parameter = 'NLS_TIMESTAMP_FORMAT';
I guess it will return this DD-MON-RR HH.MI.SSXFF AM
Do not worry about how it is being displayed. A date/timestamp variable has no format. It matters only when you want to print it. If don't use to_char function, it'll take the format mask as defined in NLS parameters. If you want to override it, use to_char function and specify the mask.But when used in the query, it'll have the correct value.
I have a query that I run everyday that requires a StartDate and EndDate value. The StartDate and EndDate used to be a manual input but I am trying to get away from that and calculate the StartDate and EndDate to be used in the query. I've developed code to capture the StartDate and EndDate in variables:
DECLARE
c_DateMask VARCHAR2(20) := 'DD-Mon-YYYY';
c_TimeMask VARCHAR2(20) := 'HH24:MI';
v_Month char(4) := 'Prev';
v_StartDate date;
v_EndDate date;
v_Environment char(7) := 'Prod';
BEGIN
if v_MONTH = 'Prev'
THEN
select TO_DATE ('01-' || TO_CHAR (ADD_MONTHS (SYSDATE, -1),'mon-yyyy')) into v_StartDate from dual;
select Last_day(TO_DATE('01-' || TO_CHAR (ADD_MONTHS (SYSDATE, -1),'mon-yyyy'))) into v_EndDate from dual;
ELSE
select TO_DATE ('01-' || TO_CHAR (ADD_MONTHS (SYSDATE, 0),'mon-yyyy')) into v_StartDate from dual;
CASE
WHEN v_Environment = 'Prod'
THEN
-- Production Environment --
select
to_char(sysdate, 'dd-Mon-yyyy ') ||
case
when to_char(sysdate, 'mi') between 00 and 20
then to_char(sysdate, 'hh24')-1||':58'||':00'
when to_char(sysdate, 'mi') between 21 and 40
then to_char(sysdate, ' hh24')||':18'||':00'
when to_char(sysdate, 'mi') between 41 and 60
then to_char(sysdate, ' hh24')||':38'||':00'
END
into v_EndDate from dual;
WHEN v_Environment = 'OldTest'
THEN
-- Test Environment --
select
to_char(sysdate, 'dd-Mon-yyyy ') ||
case
when to_char(sysdate, 'mi') between 10 and 30
then to_char(sysdate, 'hh24')||':08'||':00'
when to_char(sysdate, 'mi') between 31 and 50
then to_char(sysdate, ' hh24')||':28'||':00'
when to_char(sysdate, 'mi') between 51 and 60
then to_char(sysdate, ' hh24')||':48'||':00'
END
into v_EndDate from dual;
end case;
end if;
I then want to use the variables in my select statement below:
-----------------
/* KPI Figures */
-----------------
SELECT
SysDate as RunTime
, v_StartDate
, v_EndDate
, TTM_OFF_CONTRIBUTOR
, SUM(NVL(TTM_PER_OFF_FEE,0)) Fees
FROM [Table]
Where
TTM_PROCESSED_DATE >= v_StartDate
AND TTM_PROCESSED_DATE <= v_EndDate
group by SysDate, v_StartDate, v_EndDate, TTM_OFF_CONTRIBUTOR
END;
It all works up until when I try to use the variable values in the KPI Figures query. What am I missing?
Update:
Regarding Phil's answer: I tried but it didn't work and I get the following error:
PLS-00428: an INTO clause is expected in this SELECT statement.
I am sure I saw another response yesterday which is now gone relating to being able to assign multiple values to variables or something.
Is that what I need and how would I do that?
The variables v_StartDate and v_EndDate are only in scope within the PL/SQL block where they are declared. It looks like you are then trying to use them outside the block in a separate query. To do that you will need to create SQL Developer bind variables outside the PL/SQL block like this:
var v_start_date varchar2(11)
var v_end_date varchar2(11)
Then reference these as bind variables in both the PL/SQL block and the SQL query:
declare
...
begin
....
:v_start_date := '01-' || TO_CHAR (ADD_MONTHS (SYSDATE, -1), 'mon-yyyy');
:v_end_date := TO_CHAR(Last_day(TO_DATE('01-'
|| TO_CHAR (ADD_MONTHS (SYSDATE, -1),'mon-yyyy'))));
-- (NB No need to select from dual)
...
end;
SQL:
...
Where
TTM_PROCESSED_DATE >= TO_DATE(:v_StartDate)
AND TTM_PROCESSED_DATE <= TO_DATE(:v_EndDate)
Note that these variables cannot be declared with a type of DATE, so they need to be converted back to dates in the query (using the correct format mask).
I suggest you wrap the startdate/enddate in two functions (stored procedures)
create or replace function get_startdate(p_month in varchar2, p_env in varchar2)
return date
is
l_return date;
begin
... logic goes here ...
return l_return;
end;
Similar for enddate.
Then use those functions in your query:
SELECT
SysDate as RunTime
, get_startdate('Prev', 'Prod')
, get_enddate('Prev', 'Prod')
, TTM_OFF_CONTRIBUTOR
, SUM(NVL(TTM_PER_OFF_FEE,0)) Fees
FROM [Table]
Where
TTM_PROCESSED_DATE >= get_startdate('Prev', 'Prod')
AND TTM_PROCESSED_DATE <= get_enddate('Prev', 'Prod')
group by SysDate, get_startdate('Prev', 'Prod'), get_enddate('Prev', 'Prod'), TTM_OFF_CONTRIBUTOR
Sidenote: please consider a more concise version to determine first and last day of a month. You also can just assign a value to a variable without using SELECT INTO, e.g.
startdate := trunc(sysdate, 'MM'); -- first day of current month
enddate := last_day(trunc(sysdate, 'MM')); -- last day of current month
To calculate with hours, you could add/subtract a number of minutes, that's more readable than all the to_date/to_char conversions you do, e.g.
enddate := trunc(sysdate, 'HH24');
case
when to_char(sysdate, 'mi') between 00 and 20 then enddate := enddate - 2/(24*60);
when to_char(sysdate, 'mi') between 21 and 40 then enddate := enddate + 18/(24*60);
when to_char(sysdate, 'mi') between 41 and 60 then enddate := enddate + 38/(24*60);
end;
Good luck, Martin
I think the issue is with your query and aliases. Try
SELECT
SysDate as RunTime
, v_StartDate AS StartDate
, v_EndDate AS EndDate
, TTM_OFF_CONTRIBUTOR
, SUM(NVL(TTM_PER_OFF_FEE,0)) Fees
FROM [Table]
Where
TTM_PROCESSED_DATE >= v_StartDate
AND TTM_PROCESSED_DATE <= v_EndDate
group by RunTime, StartDate, EndDate, TTM_OFF_CONTRIBUTOR