Can NVL Function be Cascaded? - sql

can nvl() function be cascaded,...it was asked me in IBM interview .....and why????

Better yet, use COALESCE

My answer:
Yes, NVL can be cascaded. It's something I'd expect to see on code ported from Oracle versions up to and including 8i, because COALESCE wasn't supported until Oracle 9i.
COALESCE is an ANSI SQL 99 standard, and works like a CASE/SWITCH statement in how it evaluates each expression in order and does not continue evaluating expressions once it hits a non-null expression. This is called short-circuiting. Another benefit of using COALESCE is that the data types do not need to match, which is required when using NVL.
This:
SELECT COALESCE( 1.5 / NULL,
SUM(NULL),
TO_CHAR(SYSDATE, 'YYYY') ) abc
FROM DUAL
...will return: 2009 (for the next ~32 days, anyways)

Why not? For example:
select NVL( null, NVL( null, 1 )) from dual
It can be something like:
select NVL( delete_date, NVL( edit_date, create_date )) AS last_change
from Table
May be they wanted you to say that it is deterministic function. So it is reentrant.

The most common reason that I have seen for cascaded NVL's is that databases change over time.
The original design had a table that later was changed to have more columns in the table.
Alter statements created the new columns as NULL allowed and this was taken advantage of with view so that the
code on top did not need to change. The problem was that a single column, change_date,
was replaced with multiple columns. Update_Date, Comment_Date, and Approval_date. Each of the
3 new columns is now combined to get a "change_date" in the view with
create or replace view OldTableNmae as
select other_columns
, nvl ( update_date, nvl ( comment_date, approval_date ) ) change_date
, more_columns
from new_table
/

As others said, NVL can be cascaded, but the preferred solution would be to use COALESCE instead. However, the two functions are not entirely interchangeable:
1) as mentioned elsewhere, COALESCE only evaluates the arguments from the first until it meets one that does not evaluate to null
2) however, COALESCE requires all arguments to be of the same data type, thus being STRICTER than NVL, which will first attempt an implicit conversion.
E.g.
SELECT COALESCE( 1.5 / NULL,
SUM(NULL),
TO_CHAR(SYSDATE, 'YYYY') ) abc
FROM DUAL
throws in fact the error ORA-00932: inconsistent datatypes: expected NUMBER got CHAR
NVL would instead return the current year
SELECT NVL( 1.5 / NULL,
NVL( SUM(NULL),
TO_CHAR(SYSDATE, 'YYYY') ) ) abc
FROM DUAL
Other examples:
select coalesce('some text',sysdate) from dual;
throws ORA-00932: inconsistent datatypes: expected CHAR got DATE, while
select nvl('some text',sysdate) from dual;
returns some text, but
select nvl(sysdate,'some text') from dual;
throws ORA-01841: (full) year must be between -4713 and +9999, and not be 0 (because the implicit conversion attempt of 'some text' to a date has failed)

Related

How to convert a date to a string

I want yo get only the 'date hours:minutes:seconds' from the Date column
Date
10/11/22 12:14:01,807000000
11/12/22 13:15:46,650000000
29/12/22 14:30:46,501000000
and I want to get a string column with date hours:minutes:seconds
Date_string
10/11/22 12:14:01
11/12/22 13:15:46
29/12/22 14:30:46
I tried this code but it doesn't work:
select*, TO_CHAR(extract(hour from (Date)))||':'||TO_CHAR(extract(minute from (Date)))||':'||TO_CHAR(extract(second from (Date))) as Date_string
from table;
If this is a date column, you could use to_char directly:
SELECT m.*, TO_CHAR(my_date_column, 'dd/mm/yy hh24:mi:ss')
FROM mytable m
You can use REGEX SUBSTRING function to get the date string on the left.
SELECT REGEXP_SUBSTR (Date_string, '[^,]+', 1, 1)
AS left_part
FROM Table1;
where ^, means look for chars that are NOT comma on 1st position
and get the first occurrence (on the left)
Result:
LEFT_PART
10/11/22 12:14:01
11/12/22 13:15:46
29/12/22 14:30:46
reference:
https://docs.oracle.com/cd/B12037_01/server.101/b10759/functions116.htm
Just do it with the TO_DATE() and TO_CHAR() function pair, both operating on the Oracle date format strings:
Building the scenario:
-- your input ..
WITH indata(dt) AS (
SELECT '10/11/22 12:14:01,807000000' FROM dual UNION ALL
SELECT '11/12/22 13:15:46,650000000' FROM dual UNION ALL
SELECT '29/12/22 14:30:46,501000000' FROM dual
)
-- end of your input. Real query starts here.
-- Change following comma to "WITH" ..
,
-- Now convert to TIMESTAMP(9) ...
as_ts AS (
SELECT
TO_TIMESTAMP(dt ,'DD/MM/YY HH24:MI:SS,FF9') AS ts
FROM indata
)
SELECT
ts
, CAST(ts AS TIMESTAMP(0)) AS recast -- note: this is rounded
, TO_CHAR(ts,'DD/MM/YY HH24:MI:SS') AS reformatted -- this is truncated
FROM as_ts
Result:
TS
RECAST
REFORMATTED
10-NOV-22 12.14.01.807000000
10-NOV-22 12.14.02
10/11/22 12:14:01
11-DEC-22 13.15.46.650000000
11-DEC-22 13.15.47
11/12/22 13:15:46
29-DEC-22 14.30.46.501000000
29-DEC-22 14.30.47
29/12/22 14:30:46
Going by what you have in your question, it appears that the data in the field Date is a timestamp. This isn't a problem, but the names of the table (TABLE) and field (Date) present some challenges.
In Oracle, TABLE is a reserved word - so to use it as the name of a table it must be quoted by putting it inside double-quotes, as "TABLE". Similarly, Date is a mixed-case identifier and must likewise be quoted (e.g. "Date") every time it's used.
Given the above your query becomes:
SELECT TO_CHAR("Date", 'DD/MM/YY HH24:MI:SS') AS FORMATTED_DATE
FROM "TABLE"
and produces the desired results. db<>fiddle here
Generally, it's best in Oracle to avoid using reserved words as identifiers, and to allow the database to convert all names to upper case - if you do that you don't have to quote the names, and you can refer to them by upper or lower case as the database automatically converts all unquoted names to upper case internally.

Any function in Oracle similar to COALESCE, but instead of NULL works on ERRORs

I have a situation where I need to convert a numeric column value into time. It's a 6 digit field, but unfortunately, different processes over the years inserted data in different format, some HHMM, and others HHMMSS. Let's call this column colTime. I use colTime in combination with another 8 digits numeric field which contains a date in YYYYMMDD format, let's call this colDate.
It is being used as below to construct a TIMESTAMP in UTC zone:
select TO_CHAR(SYS_EXTRACT_UTC(TO_TIMESTAMP(CONCAT(NULLIF(colDate,0), LPAD(NULLIF(colTime,0),4,0)), 'YYYY-MM-DD HH24:MI')), 'YYYY-MM-DD"T"HH24:MI:SS.FFTZR') from tab1;
The problem here obviously is that the colTime may contain 4 OR 6 digit data so I cannot know the correct LPAD number in advance. When the above statement encounters a 6 digit field it throws an error.
I was thinking if I have a function similar to COALESCE that can execute the second argument if the first one returns an error then I'd be able to accommodate LPAD 4 and 6 cases.
I can use a CASE statement, but was hoping for something more graceful.
You can still use COALESCE if you also use the DEFAULT NULL ON CONVERSION ERROR syntax in TO_TIMESTAMP:
select
coalesce
(
to_timestamp('2021-01-01' || ' ' ||the_time default null on conversion error, 'YYYY-MM-DD HHMI'),
to_timestamp('2021-01-01' || ' ' ||the_time default null on conversion error, 'YYYY-MM-DD HHMISS')
)
from
(
select '0102' the_time from dual union all
select '010203' the_time from dual
);
You can use regular expressions to find out which format the data comes from and then you can convert it appropriately.
For example:
with
d (col_time) as (
select '0215' from dual
union all select '183207' from dual
union all select '12:34:56' from dual
union all select 'really-bad-data' from dual
)
select d.*,
case when regexp_like(col_time, '^[0-9]{4}$') then 'Short Time Format'
when regexp_like(col_time, '^[0-9]{6}$') then 'Long Time Format'
else 'Other Time Format'
end as guessed_format
from d;
Result:
COL_TIME GUESSED_FORMAT
---------------- -----------------
0215 Short Time Format
183207 Long Time Format
12:34:56 Other Time Format
really-bad-data Other Time Format

SELECT dates where IN(NULL,>=SYSDATE)

Is that possible? Can one
SELECT * FROM DATES_TBL WHERE DATES_TBL.DATES IN(NULL, >=SYSDATE)
? Maybe I am formatting it incorrectly, or missing a tick?
What I would like to select are values from the date field that are NULL or greater than today's date (>=SYSDATE). Any help would be appreciated.
SQL 10g
SELECT *
FROM DATES_TBL
WHERE DATES IS NULL OR DATES >= SYSDATE
The NULL value is a special value so you must write your statement like that:
SELECT *
FROM DATES_TBL
WHERE DATES IS NULL
OR DATES >= SYSDATE
This is definitely not a valid syntax: IN queries require specific items; also, because in SQL null never equals anything, including other nulls, it does not make sense to put nulls into an IN list.
What you need instead is an OR:
SELECT * WHERE (DATES_TBL.DATES IS NULL) OR (DATES_TBL.DATES>=SYSDATE)
-- ^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^
-- | |
-- | +- This is how you compare for >=
-- +------------- This is how you check for NULL
Every time I try to use an OR clause it causes a lot of run time
In cases when OR is introduced to combine non-overlapping results (which is true in this case, because a column is either null or is greater than SYSDATE, but not both) you can use UNION ALL to potentially speed up the search:
SELECT * WHERE DATES_TBL.DATES IS NULL
UNION ALL
SELECT * WHERE DATES_TBL.DATES >= SYSDATE
You need to make sure that DATES_TBL.DATES column is indexed.
Assuming this is for Oracle, try this:
SELECT * FROM DATES_TBL WHERE (DATES_TBL.DATES is null or DATES_TBL.DATES >= sysdate)
The parenthesis make it work.

Are a CASE statement and a DECODE equivalent?

It seems like the simple CASE expression and the DECODE function are equivalent and that the results returned by them should be identical. Are they?
The documentation has the following to say about the simple CASE expression:
The simple CASE expression returns the first result for which
selector_value matches selector. Remaining expressions are not
evaluated. If no selector_value matches selector, the CASE expression
returns else_result if it exists and NULL otherwise.
Comparing this to the DECODE function, the descriptions seem to be identical.
DECODE compares expr to each search value one by one. If expr is equal
to a search, then Oracle Database returns the corresponding result. If
no match is found, then Oracle returns default. If default is omitted,
then Oracle returns null.
As the searched CASE expression can be equivalent to the simple, this could be construed to be the same as well.
These three statements all seem to return the same result, 0.
select case 1 when 2 then null else 0 end as simple_case
, case when 1 = 2 then null else 0 end as searched_case
, decode(1, 2, null, 0) as decode
from dual
Do the simple CASE expression and the DECODE function (and in specific circumstances the searched CASE expression) always return the same result?
Short answer, no.
The slightly longer answer is nearly.
It only appears that the result obtained from each statement is identical. If we use the DUMP function to evaluate the data types returned you'll see what I mean:
SQL> select dump(case 1 when 2 then null else 0 end) as simple_case
2 , dump(case when 1 = 2 then null else 0 end) as searched_case
3 , dump(decode(1, 2, null, 0)) as decode
4 from dual;
SIMPLE_CASE SEARCHED_CASE DECODE
------------------ ------------------ -----------------
Typ=2 Len=1: 128 Typ=2 Len=1: 128 Typ=1 Len=1: 48
SQL Fiddle
You can see that the data type of the DECODE is 1, whereas the two CASE statements "return" a data type of 2. Using Oracle's Data Type Summary, DECODE is returning a VARCHAR2 (data type 1) whereas the CASE statements are "returning" numbers (data type 2).
I assume this occurs because, as the names suggest, DECODE is a function and CASE isn't, which implies they have been implemented differently internally. There's no real way to prove this.
You might think that this doesn't really affect anything. If you need it to be a number Oracle will implicitly convert the character to a number under the implicit conversion rules, right? This isn't true either, it won't work in a UNION as the data types have to be identical; Oracle won't do any implicit conversion to make things easy for you. Secondly, here's what Oracle says about implicit conversion:
Oracle recommends that you specify explicit conversions, rather than rely on implicit or automatic conversions, for these reasons:
SQL statements are easier to understand when you use explicit data type conversion functions.
Implicit data type conversion can have a negative impact on performance, especially if the data type of a column value is converted to that of a constant rather than the other way around.
Implicit conversion depends on the context in which it occurs and may not work the same way in every case. For example, implicit conversion from a datetime value to a VARCHAR2 value may return an unexpected year depending on the value of the NLS_DATE_FORMAT
parameter.
Algorithms for implicit conversion are subject to change across software releases and among Oracle products. Behavior of explicit conversions is more predictable.
That's not a pretty list; but the penultimate point brings me nicely on to dates. If we take the previous query and convert it into one that uses a date instead:
select case sysdate when trunc(sysdate) then null
else sysdate
end as simple_case
, case when sysdate = trunc(sysdate) then null
else sysdate
end as searched_case
, decode(sysdate, trunc(sysdate), null, sysdate) as decode
from dual;
Once again, using DUMP on this query the CASE statements return data type 12, a DATE. The DECODE has converted sysdate into a VARCHAR2.
SQL> select dump(case sysdate when trunc(sysdate) then null
2 else sysdate
3 end) as simple_case
4 , dump(case when sysdate = trunc(sysdate) then null
5 else sysdate
6 end) as searched_case
7 , dump(decode(sysdate, trunc(sysdate), null, sysdate)) as decode
8 from dual;
SIMPLE_CASE
----------------------------------
Typ=12 Len=7: 120,112,12,4,22,18,7
SEARCHED_CASE
----------------------------------
Typ=12 Len=7: 120,112,12,4,22,18,7
DECODE
----------------------------------
Typ=1 Len=19: 50,48,49,50,45,49,50,45,48,52,32,50,49,58,49,55,58,48,54
SQL Fiddle
Note (in the SQL Fiddle) that the DATE has been converted into a character using the sessions NLS_DATE_FORMAT.
Having a date that's been implicitly converted into a VARCHAR2 can cause problems. If you're intending to use TO_CHAR, to convert your date into a character, your query will break where you're not expecting it.
SQL> select to_char( decode( sysdate
2 , trunc(sysdate), null
3 , sysdate )
4 , 'yyyy-mm-dd') as to_char
5 from dual;
select to_char( decode( sysdate
*
ERROR at line 1:
ORA-01722: invalid number
SQL Fiddle
Equally, date arithmetic no longer works:
SQL>
SQL>
SQL> select decode(sysdate, trunc(sysdate), null, sysdate) + 1 as decode
2 from dual;
select decode(sysdate, trunc(sysdate), null, sysdate) + 1 as decode
*
ERROR at line 1:
ORA-01722: invalid number
SQL Fiddle
Interestingly DECODE only converts the expression to a VARCHAR2 if one of the possible results is NULL. If the default value is NULL then this doesn't happen. For instance:
SQL> select decode(sysdate, sysdate, sysdate, null) as decode
2 from dual;
DECODE
-------------------
2012-12-04 21:18:32
SQL> select dump(decode(sysdate, sysdate, sysdate, null)) as decode
2 from dual;
DECODE
------------------------------------------
Typ=13 Len=8: 220,7,12,4,21,18,32,0
SQL Fiddle
Note that the DECODE has returned a data type of 13. This isn't documented but is, I assume, a type of date as date arithmetic etc. works.
In short, avoid DECODE if you possibly can; you might not necessarily get the data types you're expecting. To quote Tom Kyte:
Decode is somewhat obscure -- CASE is very very clear. Things that are
easy to do in decode are easy to do in CASE, things that are hard or
near impossible to do with decode are easy to do in CASE. CASE, logic
wise, wins hands down.
Just to be complete there are two functional differences between DECODE and CASE.
DECODE cannot be used within PL/SQL.
CASE cannot be used to compare nulls directly
SQL> select case null when null then null else 1 end as case1
2 , case when null is null then null else 1 end as case2
3 , decode(null, null, null, 1) as decode
4 from dual
5 ;
CASE1 CASE2 DECODE
---------- ---------- ------
1
SQL Fiddle
Ben has written a lengthy answer on the differences between DECODE and CASE. He demonstrates that DECODE and CASE may return different datatypes for apparently the same set of values without properly explaining why this happens.
DECODE() is quite prescriptive: it is always the datatype of the first result parameter. Oracle applies implicit conversion to all the other result parameters. It will throw an error , if (say) the first result parameter is numeric and the default value is a date.
ORA-00932: inconsistent datatypes: expected NUMBER got DATE
This is described in the documentation: find out more.
In the first scenario the first result parameter is NULL, which Oracle decides to treat as VARCHAR2. If we change it so that the first result parameter is numeric and the default value is null the DECODE() statement will return a NUMBER; a DUMP() proves that this is so.
Whereas CASE insists that all the returned values have the same datatype, and will throw a compilation error if this is not the case. It won't apply implicit conversion. This is also covered in the documentation. Read it here.
The difference boils down to this. The following DECODE statement will run, the CASE statement won't:
select decode(1, 1, 1, '1') from dual;
select case 1 when 1 then 1 else '1' end from dual;
Obligatory SQL Fiddle.
I know i am too late but posting here because if someone search for that hopefully it could help.
I have created a MsSql script for the same-
Declare #Var varchar(399)='DECODE(MyColumnName,''A'',''Auto'',''M'',''Manual'')'
Begin
Declare #Count int, #Counter int=1
Declare #TempTable table (ID int identity(1,1),Items varchar(500))
Declare #SqlText varchar(max)
Select #Var=Replace(Replace(#Var,'DECODE(',''),')','')
Insert Into #TempTable
Select * FROM [dbo].[Split] ( #Var ,',')
--Select * from #TempTable
Select #Count=Count(ID) from #TempTable
While(#Counter<=#Count)
Begin
If(#Counter=1)
Begin
Select #SqlText='Case ' +Items from #TempTable Where ID=1
End
Else If(#Counter=#Count)
Begin
Select #SqlText+=' Then ' +Items +' End' from #TempTable Where ID=#Counter
End
Else If(#Counter%2=0)
Begin
Select #SqlText +=' When ' +Items from #TempTable Where ID=#Counter
End
Else If(#Counter%2=1)
Begin
Select #SqlText +=' Then ' +Items from #TempTable Where ID=#Counter
End
Set #Counter+=1
End
Select #SqlText SqlServerCaseStatement
End
I used Split function in above script, if you need that function you can refer Romil's answer - How to split a comma-separated value to columns

Select throws an ORA-01858 exception

With the following query, the Oracle exception is thrown.
However, I cant see why. Can anyone shed some light?
select visit_id, to_date(response, 'DD/MM/YYYY') as convertedDate from
(
select *
from dat_results_ext
where item_name = 'CALLBACKDATE'
)
where to_date(response, 'DD/MM/YYYY') > sysdate
I understand the exception to be mean that its trying to convert the 'response' field, but it is meeting a non-numeric. Problem is the row that it should bring back has everything in the right format.
The 'response' field is a varchar field, but all the rows coming back with the 'item_name = 'CALLBACKDATE' clause are all of the correct format.
Any ideas?
The optimizer can rewrite your query before trying to find the best execution plan. In your case since you have no hints that would prevent the optimizer from doing this, it will probably unnest your subquery and rewrite your query as:
SELECT *
FROM dat_results_ext
WHERE item_name = 'CALLBACKDATE'
AND to_date(response, 'DD/MM/YYYY') > sysdate
You don't have control over the order of evaluation of the statements in the WHERE clause, so Oracle probably evaluated the to_date function first on a row that is not convertible to a date, hence the error.
I see two options to force Oracle to evaluate the statements in the order you want:
Use rownum. Rownum will materialize the subquery, preventing Oracle from merging it with the outer query:
SELECT visit_id, to_date(response, 'DD/MM/YYYY') AS convertedDate
FROM (SELECT r.*,
rownum /* will materialize the subquery */
FROM dat_results_ext r
WHERE item_name = 'CALLBACKDATE')
WHERE to_date(response, 'DD/MM/YYYY') > sysdate
Use the NO_MERGE hint:
SELECT visit_id, to_date(response, 'DD/MM/YYYY') AS convertedDate
FROM (SELECT /*+ NO_MERGE */ *
FROM dat_results_ext
WHERE item_name = 'CALLBACKDATE')
WHERE to_date(response, 'DD/MM/YYYY') > sysdate
The TO_DATE clause has to be evaluated before the truth of the WHERE clause can be determined. If you have values of response that can't be evaluated in the TO_DATE function, you'll see the error.
To be very precise, this is caused because some value of response do not match the format mask of DD/MM/YYYY. For example, if your session is set to default date format of DD-MON-YY, execute the following and you will receive the error message:-
select to_date('17/SEP/2012','DD/MM/YYYY') from dual;
ERROR:
ORA-01858: a non-numeric character was found where a numeric was expected
Since you passed a character in the month field and Oracle expects a number.