I use Oracle Database 11g Release 11.2.0.4.0
When doing this query :
SELECT
CASE WHEN (DATE '2005-11-25') IN ('25/11/05') THEN
'TRUE'
ELSE
'FALSE'
END
FROM DUAL;
I get FALSE from Oracle SQL Developer 21.4.1.349 (French version) and TRUE from DBeaver 21.1.4.202108020335 (French version).
I understand that I compare a date with a string, so in my opinion the result should be FALSE, but that may me implementation-dependant.
How does it come that Oracle answers differently in those situations ? Does DBeaver do a preprocessing on dates before submitting the request?
It depends on the NLS_DATE_FORMAT the session is using.
Oracle will implicitly convert your query to the equivalent of:
SELECT CASE WHEN DATE '2005-11-25' IN (
TO_DATE(
'25/11/05',
(SELECT VALUE FROM NLS_SESSION_PARAMETERS WHERE PARAMETER = 'NLS_DATE_FORMAT')
)
)
THEN 'TRUE'
ELSE 'FALSE'
END
FROM DUAL;
As it relies on an implicit conversion from string to date so Oracle will use the session's date format in the conversion.
For example:
ALTER SESSION SET NLS_DATE_FORMAT = 'DD/MM/RR';
SELECT CASE WHEN DATE '2005-11-25' IN ('25/11/05')
THEN 'TRUE'
ELSE 'FALSE'
END
FROM DUAL;
Outputs: TRUE
But:
ALTER SESSION SET NLS_DATE_FORMAT = 'DD/MM/YYYY HH24:MI:SS';
SELECT CASE WHEN DATE '2005-11-25' IN ('25/11/05')
THEN 'TRUE'
ELSE 'FALSE'
END
FROM DUAL;
Outputs: FALSE
You can see why using:
SELECT TO_CHAR(CAST('25/11/05' AS DATE), 'YYYY-MM-DD HH24:MI:SS') FROM DUAL;
Which outputs 0005-11-25 00:00:00 and is the 1st century, and not the 21st century, as the implicit conversion uses the DD/MM/YYYY HH24:MI:SS format model and expects a 4-digit year and gets the input 05 and assumes it to be a correct value of 5 AD rather than the 5th year of the current century (which you would get with the YY format model).
The best solution is to not rely on implicit string-to-date conversions.
Either using date literals:
SELECT CASE WHEN DATE '2005-11-25' IN (DATE '2005-11-25')
THEN 'TRUE'
ELSE 'FALSE'
END
FROM DUAL;
Or, providing an explicit format model for the conversion:
SELECT CASE WHEN DATE '2005-11-25' IN (TO_DATE('25/11/05', 'DD/MM/RR'))
THEN 'TRUE'
ELSE 'FALSE'
END
FROM DUAL;
db<>fiddle here
Because you are comparing a date to a string, you are causing the string to be implicitly converted to a date, using your session's NLS settings. Those may be derived from your locale, or may be set explicitly by your client, e.g. through preferences.
If NLS_DATE_FORMAT is is set to 'DD/MM/YYYY' then '25/11/05' is converted to 0005-11-25, so the comparison is false. If it is set to 'DD/MM/YY' (or 'DD/MM/RR', or 'DD/MM/RRRR') then it is converted to 2005-11-25, so the comparison is true.
db<>fiddle
You should not rely on implicit conversions, or NLS settings - since you can't control the environment of whoever runs your code.
Related
According to the H2 documentation, in the Oracle compatibility mode:
DATE data type is treated like TIMESTAMP(0) data type.
Meantime, DATE and TIMESTAMP(0) datatypes are not the same in Oracle. Compare:
SELECT CAST(SYSDATE AS TIMESTAMP(0)), CAST(SYSDATE AS DATE) from dual
gives
25-MAR-22 13.07.42.000000000 25-MAR-22
respectively.
In particular, this weird treating of DATE as TIMESTAMP(0) influences on how H2 calculates the difference between two dates.
Again, in Oracle:
SELECT CAST(TO_DATE('2022-01-05', 'YYYY-MM-DD') AS TIMESTAMP(0)) - CAST(TO_DATE('2022-01-01', 'YYYY-MM-DD') AS TIMESTAMP(0)) from dual
gives
+04 00:00:00.000000
and
SELECT CAST(TO_DATE('2022-01-05', 'YYYY-MM-DD') AS DATE) - CAST(TO_DATE('2022-01-01', 'YYYY-MM-DD') AS DATE) from dual
produces just:
4
Apparently, for H2 both above queries produce the result in nanoseconds and not days as expected.
So, it is an H2 bug or I am missing something?
Meantime, DATE and TIMESTAMP(0) datatypes are not the same in Oracle
Oracle differs from many other RDBMS in that its DATE data type ALWAYS contains both a date and a time component. Its implementation predates the ANSI standard.
In Oracle, if you have the table:
CREATE TABLE table_name (ts TIMESTAMP(0), dt DATE);
and insert the data:
INSERT INTO table_name (ts, dt) VALUES (SYSDATE, SYSDATE);
Then you can look at the binary data being stored using the DUMP function:
SELECT DUMP(ts) AS dump_ts,
DUMP(dt) AS dump_dt
FROM table_name;
Which outputs:
DUMP_TS
DUMP_DT
Typ=180 Len=7: 120,122,3,25,15,13,37
Typ=12 Len=7: 120,122,3,25,15,13,37
Then you can see that they are both stored as 7-byte binary values:
120 = Century + 100
122 = Year-of-century + 100
3 = Month
25 = Day
15 = Hour + 1
13 = Minutes + 1
37 = Seconds + 1
And the binary values are identical (the only difference is in the meta-data Typ where 180 = TIMESTAMP and 12 = DATE).
Effectively, they are stored identically.
db<>fiddle here
However
The side-effects of a TIMESTAMP vs. a DATE data type in Oracle may lead to different effects.
When you subtract a TIMESTAMP and either a TIMESTAMP or a DATE then the return value is an INTERVAL DAY TO SECOND data type.
When you subtract a DATE and a DATE then the default return value is a NUMBER representing the number of days difference.
When you display a TIMESTAMP then the client application you are using may default to using the NLS_TIMESTAMP_FORMAT session parameter to format the timestamp as a string and the default for this parameter will typically show date, time and fractional seconds.
When you display a DATE then the client application you are using may default to using the NLS_DATE_FORMAT session parameter to format the date as a string and the default for this parameter will show date but not time (and there will never be any fractional seconds to show). Just because the client application may chose not to show the time component does not mean that the time component does not exist.
If you set the session parameters using:
ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS';
ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS';
Then, provided your client application is using those parameters to format them, they will display identically.
The problem you are seeing with the difference in Oracle is due to these side effects.
If the question is
So, it is an H2 bug or I am missing something?
than the the answer would be:
No, it is not a bug, and what you've missed is the fact, that compatibility modes in H2 are just that - attempt to reach with minimal efforts maximum compatibility with different databases.
H2 is not an emulator for any non-standard features (quirks) of those databases.
It that particular case, to achieve identical behaviour would require to introduce new non-standard data type, which goes beyond "minimal effort" level.
The different in the output of the values in the first query is down to the session's NLS settings. These control the display format for dates and timestamps:
sho parameter nls_date_format
NAME TYPE VALUE
--------------- ------ -----------
nls_date_format string DD-MON-YYYY
sho parameter nls_timestamp_format
NAME TYPE VALUE
-------------------- ------ -------------------------
nls_timestamp_format string DD-MON-YYYY HH24.MI.SSXFF
SELECT CAST(SYSDATE AS TIMESTAMP(0)), CAST(SYSDATE AS DATE) from dual;
CAST(SYSDATEASTIMESTAMP(0)) CAST(SYSDAT
------------------------------ -----------
25-MAR-2022 12.18.24.000000000 25-MAR-2022
If you change these to be the same format, both expressions return the same result:
alter session set nls_date_format = 'DD-MON-YYYY HH24:MI:SS';
alter session set nls_timestamp_format = 'DD-MON-YYYY HH24:MI:SS';
SELECT CAST(SYSDATE AS TIMESTAMP(0)), CAST(SYSDATE AS DATE) from dual;
CAST(SYSDATEASTIMEST CAST(SYSDATEASDATE)
-------------------- --------------------
25-MAR-2022 12:17:43 25-MAR-2022 12:17:43
So they both contain the full date + time with no fractional seconds.
Note that while date and timestamp(0) have the same precision, as your further examples show they work differently:
Subtracting one date from another returns the number of days between the values as a number
Subtracting a timestamp from a date or timestamp returns an interval
So the result of:
SELECT CAST(TO_DATE('2022-01-05', 'YYYY-MM-DD') AS DATE) - CAST(TO_DATE('2022-01-01', 'YYYY-MM-DD') AS DATE) from dual
Is 4 days.
I get the error message: ORA-01843: not a valid month after executing a sql plus script.
I try using the "standard" date format yyyy-mm-dd.
Is SQL/PL not understanding the alter session statement?
set linesize 200
set pagesize 1000
alter session set NLS_NUMERIC_CHARACTERS = ',.';
alter session set NLS_DATE_FORMAT = 'yyyy-mm-dd';
select
*
from my_table
where
date >= '2019-08-31';
exit
What you do need - from my point of view - is not to compare date values to strings.
Presuming that date here actually represent a DATE datatype column (why didn't you post table description?) (as already commented, you can't name a column that way, not unless you enclosed its name into double quotes), then
where date >= '2019-08-31'
---- ------------
DATE this is a string
datatype
Use date literal, which always has a DATE keyword and date in format 'yyyy-mm-dd':
where date >= date '2019-08-31'
Or, use to_date function with appropriate format mask:
where date >= to_date('2019-08-31', 'yyyy-mm-dd')
If date column (wrong name, as we already know) actually contains strings and you hope all of them are following the 'yyyy-mm-dd' format, well, some values don't. Storing dates into varchar2 datatype column is almost always a bad idea. Nobody prevents you from storing e.g. '2019-ac-31' into it, and that isn't a valid date value.
I was trying the TO_DATE function. Specifically, I noted that the following queries
1. SELECT TO_CHAR(TO_DATE('01-01-2015','DD-MM-YYYY'),'DD-MON-YY') FROM DUAL
2. SELECT TO_DATE('01-01-2015','DD-MM-YYYY') FROM DUAL
have the same output: 01-JAN-2015.
Why does the TO_DATE function return the month in its abbreviated form?
My expected output for the second query is something like 01-01-2015 (simply, a TYPE conversion, NOT a format conversion).
Am I wrong?
Thanks
Dates do not have a format - they are represented by 7- or 8-bytes.
SELECT DUMP( SYSDATE ) FROM DUAL;
Might output:
Typ=13 Len=8: 220,7,11,26,16,41,9,0
This format is very useful for computers to compare dates but not so useful to people; so, when the SQL client (SQL/plus, SQL Developers, TOAD, etc) displays a date it does not display the the bytes but displays it as a string.
It does this by making an implicit call to TO_CHAR() (or some other internal method of stringifying dates) and uses a default format mask to perform this conversion.
SQL/Plus and SQL Developer will use the user's session parameter NLS_DATE_FORMAT to perform this conversion - see this answer regarding this.
So your second query is implicitly being converted to do something approaching this (but, almost certainly, more efficiently):
SELECT TO_CHAR(
TO_DATE('01-01-2015','DD-MM-YYYY'),
( SELECT VALUE FROM NLS_SESSION_PARAMETERS WHERE PARAMETER = 'NLS_DATE_FORMAT' )
)
FROM DUAL
The default output format of DATE value, resp TO_DATE() function is set by NLS_DATE_FORMAT value. You can verify it with this query:
SELECT *
FROM V$NLS_PARAMETERS
WHERE PARAMETER = NLS_DATE_FORMAT';
You can change it on session level for example with
alter session set NLS_DATE_FORMAT = 'DD-MM-YYYY';
The output format of TO_CHAR is not correct, try:
SELECT TO_CHAR(TO_DATE('01-01-2015','DD-MM-YYYY'),'DD-MM-YYYY') FROM DUAL;
Oracle TO_DATE: is used to convert a character string to a date format.
and related to your concern; you need to alter your session like below:
alter session set nls_date_format='DD-MM-YYYY'
in your apps right after the connect.
So now if you run again your query :
SELECT TO_DATE ('01-01-2015', 'DD-MM-YYYY')
FROM DUAL;
the result would be as expected:
01-01-2015
Hope that will help.
I am writing the query like
select * from tablename where date between '27-mar-2015' and '1-apr-2015'.
but records with date '1-apr-2015' is not retrieved from the oracle database date type is TIMESTAMP(6).
between '27-mar-2015' and '1-apr-2015'
'27-mar-2015' is NOT a DATE, it is a string literal. You are forcing Oracle to do an implicit datatype conversion.
date type is TIMESTAMP(6)
Since your data type is TIMESTAMP, you need to take care of the precision of the TIMESTAMP datatype which holds fractions of a second to a precision between 0 and 9 decimal places, the default being 6.
BETWEEN
to_timestamp('27-mar-2015 01.01.01.000001','dd-mon-yyyy HH24:MI:SS.FF')
AND
to_timestamp('01-apr-2015 23.59.59.999999','dd-mon-yyyy HH24:MI:SS.FF')
For example,
SQL> ALTER SESSION SET nls_timestamp_tz_format = 'DD-MON-YYYY HH24:MI:SS.FF';
Session altered.
SQL> SELECT
2 to_timestamp('27-mar-2015 01.01.01.000001','dd-mon-yyyy HH24:MI:SS.FF') st,
3 to_timestamp('01-apr-2015 23.59.59.999999','dd-mon-yyyy HH24:MI:SS.FF') end
4 FROM dual
5 /
ST END
---------------------------------- ----------------------------------
27-MAR-15 01.01.01.000001000 AM 01-APR-15 11.59.59.999999000 PM
SQL>
You have to note that timestamp-columns (and with oracle even date-columns) always include the time of day and that date '1-apr-2015' actually means '1-apr-2015 00:00:00' - then everything makes sense. The exact string-representation of timestamps might vary according to the configured locale.
To get date including the first of april you best use between ... and '2-apr-2015' if you don't mind having the first microsecond of that day included.
Try convert to_date:
select * from tablename
where date between to_Date('27-mar-2015','dd-mm-yyy')
and to_date('1-apr-2015','dd-mm-yyyy')
I have faced same issue. Using TRUNC Date Function helped.
select * from tablename where trunc(date,'DAY') between '27-mar-2015' and '1-apr-2015'
Try to use this code:
select * from tablename where to_date(date,'DD-MON-YYYY') between '27-mar-2015' and '1-apr-2015'.
I am using Application Express (OracleXE 11g Express Edition installed on Win7 32-bit) on-line and the below stored procedure compiles and executes successfully with no errors. When compiling in SQL Plus command line, code compiles successfully, but when I execute, it gives me an error. Now I already solved the error (code snippet shown below), my question is why doesn't the database engine in Application Express throw an error ? Hope I am explaining my question correctly.
Table definition
CREATE TABLE DATE_DIMENSION
(
DATE_KEY DATE NOT NULL,
FULL_DATE_DESCRIPTION VARCHAR2(64) NOT NULL,
DAY_OF_WEEK NUMBER(1,0) NOT NULL,
DAY_OF_MONTH NUMBER(2,0) NOT NULL,
DAY_OF_YEAR NUMBER(3,0) NOT NULL,
LAST_DAY_OF_WEEK_INDICATOR CHAR(1) NOT NULL,
LAST_DAY_OF_MONTH_INDICATOR CHAR(1) NOT NULL,
WEEK_ENDING_DATE DATE NOT NULL,
MONTH_NUMBER NUMBER(2,0) NOT NULL,
MONTH_NAME VARCHAR2(32) NOT NULL,
YEAR_MONTH CHAR(32) NOT NULL,
QUARTER_NUMBER NUMBER(1,0) NOT NULL,
YEAR_QUARTER CHAR(32) NOT NULL,
YEAR_NUMBER NUMBER(4,0) NOT NULL,
CONSTRAINT DATE_DIMENSION_PK PRIMARY KEY (DATE_KEY)
)
/
Stored Procedure
create or replace PROCEDURE sp_DATE_DIMENSION(v_START_YEAR IN INT, v_END_YEAR IN INT) AS
--Declare two variables as DATE datatypes
v_CURRENT_DATE DATE;
v_END_DATE DATE;
BEGIN
--Assign the start year and end year to it's respective variables
v_CURRENT_DATE := TO_DATE('0101' || v_START_YEAR, 'MMDDYYYY');
v_END_DATE := TO_DATE('1231' || v_END_YEAR, 'MMDDYYYY');
--Clear/Dump what is currently stored in the table
DELETE FROM DATE_DIMENSION;
--Check the condition to see if the start year is less than the end year (Input Parameters)
WHILE v_CURRENT_DATE <= v_END_DATE
LOOP
--DATE_DIMENSION Table
INSERT INTO DATE_DIMENSION
(
DATE_KEY,
FULL_DATE_DESCRIPTION,
DAY_OF_WEEK,
DAY_OF_MONTH,
DAY_OF_YEAR,
LAST_DAY_OF_WEEK_INDICATOR,
LAST_DAY_OF_MONTH_INDICATOR,
WEEK_ENDING_DATE,
MONTH_NUMBER,
MONTH_NAME,
YEAR_MONTH,
QUARTER_NUMBER,
YEAR_QUARTER,
YEAR_NUMBER
)
VALUES
(
v_CURRENT_DATE, --DATE_KEY
TO_CHAR(v_CURRENT_DATE, 'Day, Month DD, YYYY'), --FULL_DATE_DESCRIPTION
TO_NUMBER(TO_CHAR(v_CURRENT_DATE, 'D')) -1, --DAY_OF_WEEK
TO_CHAR(v_CURRENT_DATE,'DD'), --DAY_OF_MONTH
TO_CHAR(v_CURRENT_DATE,'DDD'), --DAY_OF_YEAR
CASE --LAST_DAY_OF_WEEK_INDICATOR
WHEN TO_CHAR(v_CURRENT_DATE,'FMDay') = 'Saturday' THEN 'Y'
ELSE 'N'
END,
CASE --LAST_DAY_OF_MONTH_INDICATOR
WHEN LAST_DAY(TO_DATE(v_CURRENT_DATE, 'MM/DD/YYYY')) = TO_DATE(v_CURRENT_DATE, 'MM/DD/YYYY') THEN 'Y'
ELSE 'N'
END,
CASE --WEEK_ENDING_DATE OF CURRENT WEEK ENDING ON SATURDAY
WHEN TO_CHAR(v_CURRENT_DATE,'FMDay') = 'Saturday' THEN v_CURRENT_DATE
ELSE NEXT_DAY(v_CURRENT_DATE,'SATURDAY')
END,
TO_CHAR(v_CURRENT_DATE,'MM'), --MONTH_NUMBER
TO_CHAR(v_CURRENT_DATE,'MONTH'), --MONTH_NAME
TO_CHAR(v_CURRENT_DATE,'MONTH YYYY'), --YEAR_MONTH
TO_CHAR(v_CURRENT_DATE,'Q'), --QUARTER_NUMBER
TO_CHAR(v_CURRENT_DATE,'YYYY Q'), --YEAR_QUARTER
TO_CHAR(v_CURRENT_DATE,'YYYY') --YEAR_NUMBER
);
--Increment and assign the current date value to be re-evaluated
v_CURRENT_DATE := v_CURRENT_DATE + 1;
END LOOP;
END;
FYI - This piece of code solved my issue to make it execute using SQL Plus.
CASE --LAST_DAY_OF_MONTH_INDICATOR
WHEN LAST_DAY(v_CURRENT_DATE) = v_CURRENT_DATE THEN 'Y'
ELSE 'N'
END,
You're doing this:
WHEN LAST_DAY(TO_DATE(v_CURRENT_DATE, 'MM/DD/YYYY'))
= TO_DATE(v_CURRENT_DATE, 'MM/DD/YYYY') THEN 'Y'
But v_current_date is already a DATE type, so for both of those calls to to_date, you're really doing to_date(to_char(v_current_date), 'MM/DD/YYYY'), and the to_char is using your session NLS_DATE_FORMAT - which is presumably MM/DD/YYYY in Apex, but something else in SQL*Plus.
You haven't shown your actual error, so I'm speculating a little, but you're effectively doing something like:
to_date(to_char(v_current_date, 'DD/MM/YYYY'), 'MM/DD/YYYY')
That would work sometimes, but get an invalid month error if the day of the month is after the 12th, since it's transposing the month and day numbers. Or your NLS setting mat be using MON, which would get the same error as Oracle is quite forgiving about using names instead of month numbers. Or some other format which gives a different error - there are several you could hit.
Your fix, to just use WHEN LAST_DAY(v_CURRENT_DATE) = v_CURRENT_DATE, avoids both the explicit conversion to a date, and the implicit conversion from a date to string, so there is no impact from your NLS settings.
It sounds like you are saying that neither environment threw a compilation error but one environment threw a runtime error. I would guess in that case that you've written code that depends on environmental settings that are different in the two environments.
Looking at your LAST_DAY_OF_WEEK_INDICATOR, that is in fact what you did by passing a DATE to TO_DATE. Functionally, that doesn't make sense, to_date does not take a DATE as a parameter, it only accepts a VARCHAR2. When you call
TO_DATE(v_CURRENT_DATE, 'MM/DD/YYYY')
therefore, Oracle has to do a few things.
First, it implicitly casts v_current_date to a string. Because it is an implicit cast, it will use your session's NLS_DATE_FORMAT setting. Every session in a database may have a different NLS_DATE_FORMAT and the NLS_DATE_FORMAT for a single session can change over time so the behavior of this implicit cast is not known at compile time. If your NLS_DATE_FORMAT is DD-MON-RR, which is the default setting if you did an Oracle client install on an English language Windows machine, the string that is passed to to_date would be "20-FEB-15" (assuming you called the procedure today). If your NLS_DATE_FORMAT is DD/MM/YYYY, the string passed to to_date would be "20/02/2015".
Next, it calls to_date passing in the string that was just generated and the format mask that you specified. If you happen to be in a session where the NLS_DATE_FORMAT matches the format mask you passed to to_date, you'll get the same date back from to_date. If there is a mismatch, however, you may get an error (a string in the format DD-MON-YYYY will never convert to a valid date in the MM/DD/YYYY format) or you may get an unexpected result (a string in the format DD/MM/YYYY may convert to a valid date using the MM/DD/YYYY format mask but that date won't be the same one that you started with-- March 1 would be converted to January 3 for example).
If you want your code to run correctly regardless of the environment, avoid implicit casts.