Why are these NVL() statements wrong in SQL? - sql

SELECT inv_no,NVL2(inv_amt,inv_date,'Not Available')
FROM invoice;
SELECT inv_no,NVL2(inv_amt,inv_amt*.25,'Not Available') FROM invoice;
getting
ORA-01858: a non-numeric character was found where a numeric was expected
ORA-01722: invalid number
respectively
Please suggest what are the datatypes I can give in expr1 and expr2.
Also Please tell me how this is right?
SELECT inv_no,NVL2(inv_date,sysdate-inv_date,sysdate)
FROM invoice

The 2nd and 3rd parameters to NVL2 should be the same datatype. So, assuming inv_date is a DATE, you'd need to have that as a varchar2 like;
SELECT inv_no, NVL2(inv_amt, to_char(inv_date, 'dd-mon-yyyy hh24:mi:ss'), 'Not Available') FROM ...
or whatever format string you wanted. Otherwise Oracle will convert the 3rd parameter 'Not Available' to match the 2nd parameter's data type. This will try to convert 'Not Available' to a date and crash.
Similarly in the 2nd example, you have to convert the inv_amt*.25 to a char viato_char, e.g. to_char(inv_amt*.25).

Your first 2 examples attempt to have a date / numeric and text results for the same field. This will cause an error when Oracle attempts to convert this text to either types. You'll need to use the to_char(field) function on the date / numeric fields to convert them to text.
Lastly a date is in fact a number added to a databases base date. For example a date is a number of base day and the decimal it has is the ratio of a day, e.g. 0.5 is 12 hours, and the database has a base date, e.g. 01-Jan-1900 or 01-Jan-2000. This is why when you do date - date the result is a number and a date can also be represented as a number.

Refer to: https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions106.htm
The 2nd argument should be either character or numeric. The 2nd argument gives datatype to which the 3rd argument will be implictly converted. So:
1. The first one SELECT inv_no,NVL2(inv_amt,inv_date,'Not Available') FROM invoice; is incorrect as you cannot have DATE as a 2nd argument.
The same way:
SELECT NVL2(NULLIF(1, 1), SYSDATE, 'A') FROM DUAL; -- FAILS;
SELECT NVL2(NULLIF(1, 0), SYSDATE, 'A') FROM DUAL; -- FAILS;
SELECT NVL2(NULLIF(1, 1), 'A', SYSDATE) FROM DUAL; -- PASS; returns SYSDATE (as char datatype)
SELECT NVL2(NULLIF(1, 0), 'A', SYSDATE) FROM DUAL; -- PASS; returns 'A';
2. The second is invalid too: SELECT inv_no,NVL2(inv_amt,inv_amt*.25,'Not Available') FROM invoice; Oracle tries to implicitly convert 3rd argument to the datatype of the 2nd arugment, meaning it tries to convert 'Not available' to numeric value.
It will work when you swap 2nd and 3rd argument:
SELECT inv_no,NVL2(inv_amt,'Not Available',inv_amt*.25) FROM invoice;
inv_amt*.25 will be implicitly converted to character data; This example is useless as it will get you NULL*.25 making it NULL.
What you can do however is you can make both parameters as characters by converting numeric result to character:
SELECT inv_no,NVL2(inv_amt,TO_CHAR(inv_amt*.25), 'Not Available') FROM invoice;
It will make some sense particularly when you want to display currency.
3. The third one is more complex
The only way I can explain is that the 2nd argument gets converted to character before dealing with the third one. That way the third one will get converted to characters too. This would make sense given the two cases:
SELECT NVL2(2, SYSDATE - TO_DATE('10-JAN-83'), SYSDATE) from dual; -- PASSES
SELECT NVL2(2, 2.2, SYSDATE) from dual; -- FAILS.
Finally: I would advise to stick to Oracle's documentation and use explicit conversion.

Related

Convert varchar/timestamp col to Date field

I have a varchar2 datatype field (lmp_date) that can return either null or what looks like a timestamp value. Changing the database data_type to DATE isn't a possibility, so now I'm needing to convert this to a date, but with the values this column returns, I'm having some problems.
Returned values for lmp_date =
null or 2021-06-11-00.00.00
Date format needed: MM/DD/YYYY
I've tried cast, convert, substr+instr to no avail
ETA - A couple example attempts (because there have been 10+:
select order_no, to_date(lmp_date) lmp_date from table_a - with error message of 'ORA-01861: literal does not match format string'
select order_no, to_date(substr(lmp_date, 1, instr(lmp_date, '00' -15))) lmp_date from table_a - since lmp_date has null value possibilities, this doesn't work successfully
select order_no, cast(lmp_date as date) lmp_date from table_a - with same error message of 'ORA-01861: literal does not match format string'
select order_no, to_date(lmp_date, 'YYYY-MM-DD') lmp_date from table_a - ORA-01830: date format picture ends before converting entire input string
There have been more attempts, this is all I can remember
To convert a string to a date, use the to_date() function with a suitable format mask:
to_date(lmp_date, 'YYYY-MM-DD-HH24:MI:SS')
The format model elements are in the documentation.
The result of that is a date data type, which is an internal 7-byte representation. Your client or application will format that for display, which may be based on your NLS_DATE_FORMAT setting, so you can modify that to change hot all dates are displayed; or use to_char() to convert the date back to a string, e.g.:
to_char(to_date(lmp_date, 'YYYY-MM-DD-HH24:MI:SS'), 'MM/DD/YYYY')
although if you want it as that string you can just use string manipulation with substr() and concatenation:
case when lmp_date is not null then
substr(lmp_date, 6, 2) || '/' || substr(lmp_date, 9, 2) || '/' || substr(lmp_date, 1, 4)
end
db<>fiddle
When you do either of these:
to_date(lmp_date)
cast(lmp_date as date)
this also relies on your session NLS_DATE_FORMAT; and the "literal does not match format string" error indicates that it doesn't match the string, e.g. if you have the still-default 'DD-MON-RR' setting. It would actually work - for you in your current session - if you changed that setting. I've shown that here just for info. But to work for anyone regardless of their session settings, you should use to_date() with an explicit format mask, and don't rely on or assume anything session-specific.
You were nearly there with:
to_date(lmp_date, 'YYYY-MM-DD')
and again the "date format picture ends before converting entire input string" message tells you what is wrong - your string carries on past the YYYY-MM-DD elements. Expanding the format mask to match all of the string, as I did above, means it knows what each part means.
If you were really only interested in the date part then you could cut the end off the string:
to_date(substr(lmp_date, 1, 10), 'YYYY-MM-DD')
but that's only really useful if you have a mix of string values where some have times and some do not. (The resulting date will always have a time; it will just be midnight.) And if you have dates with different formats then it gets a bit complicated - partly why you shouldn't store dates as strings.

How does ORACLE database convert DATE to NUMBER implicitly?

I'm trying to understand this function:
NVL2( NULL, ( SYSDATE - SYSDATE ), DATE '2020-05-24' ))
And its returned value:
NVL2(NULL,(SYSDATE-SYSDATE),DATE '2020-05-24'))
2458994
I'm having trouble understanding where that number, 2458994, comes from, as SYSDATE-SYSDATE is a NUMBER, and you cannot implicity convert a DATE to a NUMBER:
TO_NUMBER(DATE '2020-05-24')
ORA-01722: invalid number
The ORACLE SQL Language Reference NVL2 states:
If expr2 is numeric data, then Oracle Database determines which argument has the highest numeric precedence, implicitly converts the other argument to that data type, and returns that data type.
So my question is, what form of conversion is ORACLE SQL using on the DATE datatype to make it a NUMBER datatype?
It's effectively doing:
to_number(to_char(DATE '2020-05-24','J'))
The 'J' is (from the docs):
Julian day; the number of days since January 1, 4712 BC. Number specified with J must be integers.
If you run that manually it gets the same value you see:
select to_number(to_char(DATE '2020-05-24','J')) from dual;
2458994
It isn't obvious that it should be doing that, but it is. If the second argument is a plain (type-2) number then you get an error:
select NVL2(NULL,42,to_date('2020-05-24','YYYY-MM-DD')) from dual;
ORA-00932: inconsistent datatypes: expected NUMBER got DATE
If you dump the date subtraction result it comes back as a different (internal, undocumented as far as I can see) data type:
select dump(SYSDATE-SYSDATE) from dual;
Typ=14 Len=8: 114,133,37,0,0,0,0,0
That seems to cause the third argument to be converted to that same type; it's almost equivalent to:
select DATE '2020-05-24' - DATE '-4712-01-01' from dual;
2458993
So it looks like it's either doing something similar to that but adjusting it, or doing an internal version of the 'J' conversion, or... something else vaguely similar. It doesn't seem to be documented behaviour.

Same query works fine in a certain database but throws error in different database

I am trying to fetch the year from 'MON-YY' format and then concatenating the fetched year with '01-JUN' . I used to_date to forcefully convert 'string' ('01-JUN-17') to 'date' type
The below code works fine in a particular database for eg db_1
select to_date('01-JUN-'||(EXTRACT(YEAR FROM to_date('MAY-17','mon-yy'))) ) AS YEAR_START_DATE from dual;
returns :
01-JUN-17
But in db_2 the same code throws the below error :
ORA-01858: a non-numeric character was found where a numeric was expected
01858. 00000 - "a non-numeric character was found where a numeric was expected"
*Cause: The input data to be converted using a date format model was
incorrect. The input data did not contain a number where a number was
required by the format model.
*Action: Fix the input data or the date format model to make sure the
elements match in number and type. Then retry the operation.
Can someone please help ?
'01-JUN-' || EXTRACT(YEAR FROM to_date('MAY-17','mon-yy'))
Will generate a string like '01-JUN-2017'
The problem occurs when you try to use TO_DATE( datestring, format_model ) without specifying a format model. In this case the query will use the NLS_DATE_FORMAT session parameter.
You can find the value for this using:
SELECT VALUE
FROM SYS.NLS_SESSION_PARAMETERS
WHERE PARAMETER - 'NLS_DATE_FORMAT';
If this does not match the format you specify then it will raise an exception.
You should explicitly specify the format model (and the language):
SELECT TO_DATE(
'01-JUN-'||EXTRACT(YEAR FROM to_date('MAY-17','mon-yy','NLS_LANGUAGE="ENGLISH"')),
'dd-MON-YYYY',
'NLS_LANGUAGE="ENGLISH"'
) AS YEAR_START_DATE
FROM DUAL;
Or you could use:
SELECT ADD_MONTHS(
TRUNC(
TO_DATE( 'MAY-17', 'MON-YY', 'NLS_LANGUAGE="ENGLISH"' ),
'YY'
),
5
) AS YEAR_START_DATE
FROM DUAL;
You are missing a date format for your first to_date('01-JUN-'..) function. The date format for one database is probably DD-MON-YYYY but it could be DD-MM-YYYY in the other database.
After adding the date format your query looks like this:
select to_date('01-JUN-' || (extract(year from to_date('MAY-17', 'mon-yy'))),'DD-MON-YYYY') as year_start_date
from dual;
It's good practise to always specify a format when converting dates from and to strings.

Oracle - Selecting employees which were hired in the last 20 years

I cannot use months_between, only playing with DATEs is allowed, so I got this:
select * from emp
where ((SYSDATE- hiredate)/(365+1/4-1/100+1/400)) >= ((SYSDATE/(365+1/4-1/100+1/400))-20);
I dont understand why i get error in
(SYSDATE/(365+1/4-1/100+1/400))-20
saying it is an invalid datatype "inconsistent datatypes: expected %s got %s" when
(SYSDATE-hiredate)/(365+1/4-1/100+1/400)
is working properly with no error, WTF?
PS: an example of using (365+1/4-1/100+1/400) is with birth date, for more precision:
((SYSDATE- birth_date)/(365+1/4-1/100+1/400)) >=18
sysdate retuns the current date and time.
In your second test case as below, you are subtracting two dates which returns
numeric value indicating the number of days between the two dates and so calculation (division was made possible).
(SYSDATE-hiredate)/(365+1/4-1/100+1/400)
In your first test case as below, you are directly trying to divide a date type value.
you cannot divide a date datatype in oracle. Instead you can add any value say x (sysdate +x), it means you are adding x days to the date value.
(SYSDATE/(365+1/4-1/100+1/400))-5
In case, you want to convert the sysdate to number you can try like below
select to_number(to_char(sysdate, 'yyyymmddhh24miss')) from dual;
Which will return you DATE+TIME like 20140608165750 for today.
(OR)
select to_number(to_char(sysdate, 'yyyymmdd')) from dual;
Result will be 20140608 (only the DATE part)
According to Oracle on subtracting two Date datatypes you'll get a Number datatype.
From Oracle docs: Reference
So this part of your query
((SYSDATE- birth_date)/(365+1/4-1/100+1/400)) >=18 returns a number, whereas
In this statement (SYSDATE/(365+1/4-1/100+1/400))-5 sysdate is still taken as a date dataype which is why you're getting the error. You can explicitly use to_number to convert sysdate into number.
Edit: Try this ((to_number(to_char(SYSDATE,'DDMMYYYY'))/(365+1/4-1/100+1/400))-5) to convert your sysdate date dataype into number datatype. So, your select query should look like this
select * from emp
where ((SYSDATE- hiredate)/(365+1/4-1/100+1/400)) >= ((to_number(to_char(SYSDATE,'DDMMYYYY'))/(365+1/4-1/100+1/400))-5);

In Oracle, convert number(5,10) to date

When ececute the following SQL syntax in Oracle, always not success, please help.
40284.3878935185 represents '2010-04-16 09:18:34', with microsecond.
an epoch date of 01 January 1900 (like Excel).
create table temp1 (date1 number2(5,10));
insert into temp1(date1) values('40284.3878935185');
select to_date(date1, 'yyyy-mm-dd hh24:mi:ssxff') from temp1
Error report: SQL Error: ORA-01861: literal does not match format
string
01861. 00000 - "literal does not match format string"
*Cause: Literals in the input must be the same length as literals in
the format string (with the exception of leading whitespace). If the
"FX" modifier has been toggled on, the literal must match exactly,
with no extra whitespace.
*Action: Correct the format string to match the literal.
Thanks to Mark Bannister
Now the SQL syntax is:
select to_char(to_date('1899-12-30','yyyy-mm-dd') +
date1,'yyyy-mm-dd hh24:mi:ss') from temp1
but can't fetch the date format like 'yyyy-mm-dd hh24:mi:ss.ff'. Continue look for help.
Using an epoch date of 30 December 1899, try:
select to_date('1899-12-30','yyyy-mm-dd') + date1
Simple date addition doesn't work with timestamps, at least if you need to preserve the fractional seconds. When you do to_timestamp('1899-12-30','yyyy-mm-dd')+ date1 (in a comment on Mark's answer) the TIMESTAMP is implicitly converted to a DATE before the addition, to the overall answer is a DATE, and so doesn't have any fractional seconds; then you use to_char(..., '... .FF') it complains with ORA-01821.
You need to convert the number of days held by your date1 column into an interval. Fortunately Oracle provides a function to do exactly that, NUMTODSINTERVAL:
select to_timestamp('1899-12-30','YYYY-MM-DD')
+ numtodsinterval(date1, 'DAY') from temp3;
16-APR-10 09.18.33.999998400
You can then display that in your desired format, e.g. (using a CTE to provide your date1 value):
with temp3 as ( select 40284.3878935185 as date1 from dual)
select to_char(to_timestamp('1899-12-30','YYYY-MM-DD')
+ numtodsinterval(date1, 'DAY'), 'YYYY-MM-DD HH24:MI:SSXFF') from temp3;
2010-04-16 09:18:33.999998400
Or to restrict to thousandths of a second:
with temp3 as ( select 40284.3878935185 as date1 from dual)
select to_char(to_timestamp('1899-12-30','YYYY-MM-DD')+
+ numtodsinterval(date1, 'DAY'), 'YYYY-MM-DD HH24:MI:SS.FF3') from temp3;
2010-04-16 09:18:33.999
An epoch of 1899-12-30 sounds odd though, and doesn't correspond to Excel as you stated. It seems more likely that your expected result is wrong and it should be 2010-04-18, so I'd check your assumptions. Andrew also makes some good points, and you should be storing your value in the table in a TIMESTAMP column. If you receive data like this though, you still need something along these lines to convert it for storage at some point.
Don't know the epoch date exactly, but try something like:
select to_date('19700101','YYYYMMDD')+ :secs_since_epoch/86400 from dual;
Or, cast to timestamp like:
select cast(to_date('19700101', 'YYYYMMDD') + :secs_since_epoch/86400 as timestamp with local time zone) from dual;
I hope this doesn't come across too harshly, but you've got to totally rethink your approach here.
You're not keeping data types straight at all. Each line of your example misuses a data type.
TEMP1.DATE1 is not a date or a varchar2, but a NUMBER
you insert not the number 40284.3878935185, but the STRING >> '40284.3878935185' <<
your SELECT TO_DATE(...) uses the NUMBER Temp1.Date1 value, but treats it as a VARCHAR2 using the format block
I'm about 95% certain that you think Oracle transfers this data using simple block data copies. "Since each Oracle date is stored as a number anyway, why not just insert that number into the table?" Well, because when you're defining a column as a NUMBER you're telling Oracle "this is not a date." Oracle therefore does not manage it as a date.
Each of these type conversions is calculated by Oracle based on your current session variables. If you were in France, where the '.' is a thousands separator rather than a radix, the INSERT would completely fail.
All of these conversions with strings are modified by the locale in which Oracle thinks your running. Check dictionary view V$NLS_PARAMETERS.
This gets worse with date/time values. Date/time values can go all over the map - mostly because of time zone. What time zone is your database server in? What time zone does it think you're running from? And if that doesn't spin your head quite enough, check out what happens if you change Oracle's default calendar from Gregorian to Thai Buddha.
I strongly suggest you get rid of the numbers ENTIRELY.
To create date or date time values, use strings with completely invariant and unambiguous formats. Then assign, compare and calculate date values exclusively, e.g.:
GOODFMT constant VARCHAR2 = 'YYYY-MM-DD HH24:MI:SS.FFF ZZZ'
Good_Time DATE = TO_DATE ('2012-02-17 08:07:55.000 EST', GOODFMT);