Oracle PL/SQL string compare issue - sql

I have the following Oracle PL/SQL codes that may be rusty from you guys perspective:
DECLARE
str1 varchar2(4000);
str2 varchar2(4000);
BEGIN
str1:='';
str2:='sdd';
IF(str1<>str2) THEN
dbms_output.put_line('The two strings is not equal');
END IF;
END;
/
This is very obvious that two strings str1 and str2 are not equal, but why 'The two strings are not equal' was not printed out? Do Oracle have another common method to compare two string?

As Phil noted, the empty string is treated as a NULL, and NULL is not equal or unequal to anything. If you expect empty strings or NULLs, you'll need to handle those with NVL():
DECLARE
str1 varchar2(4000);
str2 varchar2(4000);
BEGIN
str1:='';
str2:='sdd';
-- Provide an alternate null value that does not exist in your data:
IF(NVL(str1,'X') != NVL(str2,'Y')) THEN
dbms_output.put_line('The two strings are not equal');
END IF;
END;
/
Concerning null comparisons:
According to the Oracle 12c documentation on NULLS, null comparisons using IS NULL or IS NOT NULL do evaluate to TRUE or FALSE. However, all other comparisons evaluate to UNKNOWN, not FALSE. The documentation further states:
A condition that evaluates to UNKNOWN acts almost like FALSE. For example, a SELECT statement with a condition in the WHERE clause that evaluates to UNKNOWN returns no rows. However, a condition evaluating to UNKNOWN differs from FALSE in that further operations on an UNKNOWN condition evaluation will evaluate to UNKNOWN. Thus, NOT FALSE evaluates to TRUE, but NOT UNKNOWN evaluates to UNKNOWN.
A reference table is provided by Oracle:
Condition Value of A Evaluation
----------------------------------------
a IS NULL 10 FALSE
a IS NOT NULL 10 TRUE
a IS NULL NULL TRUE
a IS NOT NULL NULL FALSE
a = NULL 10 UNKNOWN
a != NULL 10 UNKNOWN
a = NULL NULL UNKNOWN
a != NULL NULL UNKNOWN
a = 10 NULL UNKNOWN
a != 10 NULL UNKNOWN
I also learned that we should not write PL/SQL assuming empty strings will always evaluate as NULL:
Oracle Database currently treats a character value with a length of zero as null. However, this may not continue to be true in future releases, and Oracle recommends that you do not treat empty strings the same as nulls.

Let's fill in the gaps in your code, by adding the other branches in the logic, and see what happens:
SQL> DECLARE
2 str1 varchar2(4000);
3 str2 varchar2(4000);
4 BEGIN
5 str1:='';
6 str2:='sdd';
7 IF(str1<>str2) THEN
8 dbms_output.put_line('The two strings is not equal');
9 ELSIF (str1=str2) THEN
10 dbms_output.put_line('The two strings are the same');
11 ELSE
12 dbms_output.put_line('Who knows?');
13 END IF;
14 END;
15 /
Who knows?
PL/SQL procedure successfully completed.
SQL>
So the two strings are neither the same nor are they not the same? Huh?
It comes down to this. Oracle treats an empty string as a NULL. If we attempt to compare a NULL and another string the outcome is not TRUE nor FALSE, it is NULL. This remains the case even if the other string is also a NULL.

I compare strings using = and not <>. I've found out that in this context = seems to work in more reasonable fashion than <>. I have specified that two empty (or NULL) strings are equal. The real implementation returns PL/SQL boolean, but here I changed that to pls_integer (0 is false and 1 is true) to be able easily demonstrate the function.
create or replace function is_equal(a in varchar2, b in varchar2)
return pls_integer as
begin
if a is null and b is null then
return 1;
end if;
if a = b then
return 1;
end if;
return 0;
end;
/
show errors
begin
/* Prints 0 */
dbms_output.put_line(is_equal('AAA', 'BBB'));
dbms_output.put_line(is_equal('AAA', null));
dbms_output.put_line(is_equal(null, 'BBB'));
dbms_output.put_line(is_equal('AAA', ''));
dbms_output.put_line(is_equal('', 'BBB'));
/* Prints 1 */
dbms_output.put_line(is_equal(null, null));
dbms_output.put_line(is_equal(null, ''));
dbms_output.put_line(is_equal('', ''));
dbms_output.put_line(is_equal('AAA', 'AAA'));
end;
/

To fix the core question, "how should I detect that these two variables don't have the same value when one of them is null?", I don't like the approach of nvl(my_column, 'some value that will never, ever, ever appear in the data and I can be absolutely sure of that') because you can't always guarantee that a value won't appear... especially with NUMBERs.
I have used the following:
if (str1 is null) <> (str2 is null) or str1 <> str2 then
dbms_output.put_line('not equal');
end if;
Disclaimer: I am not an Oracle wizard and I came up with this one myself and have not seen it elsewhere, so there may be some subtle reason why it's a bad idea. But it does avoid the trap mentioned by APC, that comparing a null to something else gives neither TRUE nor FALSE but NULL. Because the clauses (str1 is null) will always return TRUE or FALSE, never null.
(Note that PL/SQL performs short-circuit evaluation, as noted here.)

I've created a stored function for this text comparison purpose:
CREATE OR REPLACE FUNCTION TextCompare(vOperand1 IN VARCHAR2, vOperator IN VARCHAR2, vOperand2 IN VARCHAR2) RETURN NUMBER DETERMINISTIC AS
BEGIN
IF vOperator = '=' THEN
RETURN CASE WHEN vOperand1 = vOperand2 OR vOperand1 IS NULL AND vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '<>' THEN
RETURN CASE WHEN vOperand1 <> vOperand2 OR (vOperand1 IS NULL) <> (vOperand2 IS NULL) THEN 1 ELSE 0 END;
ELSIF vOperator = '<=' THEN
RETURN CASE WHEN vOperand1 <= vOperand2 OR vOperand1 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '>=' THEN
RETURN CASE WHEN vOperand1 >= vOperand2 OR vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '<' THEN
RETURN CASE WHEN vOperand1 < vOperand2 OR vOperand1 IS NULL AND vOperand2 IS NOT NULL THEN 1 ELSE 0 END;
ELSIF vOperator = '>' THEN
RETURN CASE WHEN vOperand1 > vOperand2 OR vOperand1 IS NOT NULL AND vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = 'LIKE' THEN
RETURN CASE WHEN vOperand1 LIKE vOperand2 OR vOperand1 IS NULL AND vOperand2 IS NULL THEN 1 ELSE 0 END;
ELSIF vOperator = 'NOT LIKE' THEN
RETURN CASE WHEN vOperand1 NOT LIKE vOperand2 OR (vOperand1 IS NULL) <> (vOperand2 IS NULL) THEN 1 ELSE 0 END;
ELSE
RAISE VALUE_ERROR;
END IF;
END;
In example:
SELECT * FROM MyTable WHERE TextCompare(MyTable.a, '>=', MyTable.b) = 1;

Only change the line
str1:='';
to
str1:=' ';

The '' would be treated as NULL, so, both the strings need to be checked as NULL.
Function:
CREATE OR REPLACE FUNCTION str_cmpr_fnc(str_val1_in IN VARCHAR2, str_val2_in IN VARCHAR2) RETURN VARCHAR2
AS
l_result VARCHAR2(50);
BEGIN
-- string comparison
CASE
WHEN str_val1_in IS NULL AND str_val2_in IS NULL THEN
l_result := 'Both Unknown';
WHEN str_val1_in IS NULL THEN
l_result := 'Str1 Unknown';
WHEN str_val2_in IS NULL THEN
l_result := 'Str2 Unknown';
ELSE
CASE
WHEN str_val1_in = str_val2_in THEN
l_result := 'Both are equel';
ELSE
l_result := 'Both strings are not equal';
END CASE;
END CASE;
-- return result
RETURN l_result;
EXCEPTION
WHEN OTHERS THEN
-- set serveroutput on to get the error information
DBMS_OUTPUT.put_line(SQLERRM||' ,'|| DBMS_UTILITY.FORMAT_ERROR_BACKTRACE);
-- return result
RETURN l_result;
END str_cmpr_fnc;
Sql Statement:
SELECT str_cmpr_fnc('7', 'd') FROM DUAL;

To the first question:
Probably the message wasn't print out because you have the output turned off. Use these commands to turn it back on:
set serveroutput on
exec dbms_output.enable(1000000);
On the second question:
My PLSQL is quite rusty so I can't give you a full snippet, but you'll need to loop over the result set of the SQL query and CONCAT all the strings together.

Related

How to implement a function that check a condition

I can't use boolean in a sql query.
Therefore I can't create a function that return true or false and use it to test a condition.
I must create a function that return something (1 for instance) and test it. Like that:
WITH
FUNCTION f (input INTEGER)
RETURN INTEGER
IS
BEGIN
RETURN CASE WHEN input = 1 THEN 1 ELSE 0 END;
END;
A AS (SELECT 1 a FROM DUAL)
SELECT *
FROM a
WHERE f(a.a) = 1
instead of that:
WITH
FUNCTION f (input INTEGER)
RETURN boolean
IS
BEGIN
RETURN input = 1 ;
END;
A AS (SELECT 1 a FROM DUAL)
SELECT *
FROM a
WHERE f(a.a)
Unless there is another way?
code
I've tried to use a macro but to no avail
WITH
FUNCTION ft
RETURN VARCHAR2 SQL_MACRO
IS
BEGIN
RETURN q'{
SELECT 1
FROM dual
}';
END;
FUNCTION fc
RETURN VARCHAR2 SQL_MACRO
IS
BEGIN
RETURN q'{
1=1
}';
END;
SELECT *
FROM ft()
WHERE fc()
ORA-00920: invalid relational operator
code
The BOOLEAN data type is a PL/SQL only data type and is not supported in Oracle SQL statements.
Use two constants:
0 for false, 1 (or non-zero) for true (as per the C language).
0 for no errors, non-zero for errors (as per Unix program exit codes).
'Y' for yes, 'N' for no.
'success' and 'failure'
etc.
Whatever you return is your personal preference. Document the convention you are going to use and then use it consistently so that everyone on the same project uses the same convention.
Unless there is another way?
No, just pick a convention for truthy/falsy values and stick to that.
WITH
FUNCTION f_check_int ( p_str VARCHAR2 )
RETURN VARCHAR2 ------- Y / N
IS
lv_data NUMBER;
BEGIN
lv_data := TO_NUMBER(p_str);
IF lv_data>0 AND MOD(lv_data,1)=0 THEN
RETURN 'Y';
ELSE
RETURN 'N';
END IF;
EXCEPTION
WHEN VALUE_ERROR THEN
RETURN 'N';
END;
A AS (SELECT 1 a FROM DUAL)
SELECT *
FROM a
WHERE f_check_int(a.a) = 'Y'

I have a string value i need to check the format of the value in plsql

I have a INPUT VARIABLE as string 'AB1234567' it should not be more than 9 digits. i need a function in oracle Using regular expressions i need to check the format of the string.
i.e The first two characters of the string should be alphabetes and the next 7 characters should be numbers.
If i get any other special characters in the first two characters of string the function need's to return 'F' and next 7 characters should be numbers if i get any junk characters in the next 7 variables then it needs to return 'f'.
The universal format of the string is 'AB1234567' first two characters are alpha and the next 7 should be the numbers .
Thank you
You can use regexp_like with different character classes to check for different patterns.
create or replace function str_test(txt in varchar2) return varchar2 as
begin
if not regexp_like(txt, '^[[:alpha:]]{2}') then
return 'F';
elsif not regexp_like(txt, '^.{2}\d{7}$') then
return 'f';
else
return 'some other output';
end if;
end;
/
If I correctly understand, you need something like this:
(upss, I missed not be more than 9 digits part, updated)
create function func(var nvarchar2)
return nvarchar2
as
begin
if NOT REGEXP_LIKE(var, '^[A-Z]{2}') then
return 'F';
elsif NOT REGEXP_LIKE(var, '^..[0-9]{7}$') then
return 'f';
else
return 'ok';
end if;
end;
CREATE OR REPLACE FUNCTION str_test (
str IN VARCHAR2
) RETURN CHAR
AS
BEGIN
IF str IS NULL THEN
RETURN 'X';
ELSE IF LENGTH( str ) != 9 THEN
RETURN 'Y'
ELSE IF SUBSTR( str, 1, 1 ) NOT BETWEEN 'A' AND 'Z'
OR SUBSTR( str, 2, 1 ) NOT BETWEEN 'A' AND 'Z' THEN
RETURN 'F';
ELSE
TO_NUMBER( SUBSTR( str, 3 ) );
RETURN NULL;
END IF;
EXCEPTION
WHEN other THEN
RETURN 'f';
END;
/

Oracle string compare in query

I have two string and want to know, if in alphabetically sorted list the string2 is before string1, is the same, or comes after string1.
Something like STRCMP (string1, string2) for MySql.
Is there some kind of built in function, that isn't that popular, or should I use a non-function approach manually comparing each char (if that is even considerable)?
Thanks in advance.
Yes, it is <. You can use boolean comparators to get "alphabetic" ordering. I put that in quotes because it depends on the collation of the strings, but it is usually reasonable.
In a where clause:
where string1 < string2
If you want a value, then this works in both Oracle and MySQL:
select (case when string1 < string2 then 1 else 0 end) as string_compare_flag
You'll need to consider nulls and case.
In PL/SQL you could do something like this...
declare
BEFORE CONSTANT PLS_INTEGER := -1;
AFTER CONSTANT PLS_INTEGER := 1;
SAME CONSTANT PLS_INTEGER := 0;
NULLS_FIRST CONSTANT VARCHAR2(1) := CHR(9);
NULLS_LAST CONSTANT VARCHAR2(1) := CHR(127);
string1 varchar2(100);
string2 varchar2(100);
result pls_integer;
--If this was a function then these would be your input parameters
inputString1 varchar2(100) := 'A'; --try null here
inputString2 varchar2(100) := 'b';
--swap this over if you want nulls first
nullPos VARCHAR2(1) := NULLS_LAST; --try swapping to NULLS_FIRST
begin
string1 := upper(nvl(inputString1,nullPos));
string2 := upper(nvl(inputString2,nullPos));
result := case when string1 < string2 then BEFORE
when string1 = string2 then SAME
else AFTER end;
dbms_output.put_line(result);
end;
The values chosen for NULLS_FIRST and NULLS_LAST are just arbitrary but outside of the standard a-z A-Z so that a null value will get placed either before or after a non null one, and so two null values will return 0 (same).
If you aren't bothered about case i.e A comes after b then you can ignore the upper function code.
You could put the plsql code (or similar) in a function and call it from your SQL or you could do
SELECT case when upper(nvl(string1,CHR(127))) < upper(nvl(string2,CHR(127))) then -1
when upper(nvl(string1,CHR(127))) = upper(nvl(string2,CHR(127))) then 0
else 1
end
...

PL/SQL Relational operator <> , != Syntax issue

I am trying below code in pl/sql program.
I didn't get my code running through any of the below condition, it just skips all the 'if' conditions.
Code snap :---
IF(null != 'C') THEN
DBMS_OUTPUT.put_line ('1');
END IF;
IF(Trim('') <> 'C') THEN
DBMS_OUTPUT.put_line ('2');
END IF;
IF(Trim('') != 'C') THEN
DBMS_OUTPUT.put_line ('3');
END IF;
Unlike in other databases, in Oracle an empty string ('') is actually the same as NULL, so Trim('') is NULL too. Therefor, all three if statements actually have the same condition.
Normal comparison operators don't work for null
The normal comparison operators like =, <> and != don't work for NULL, so they also don't work for '' either.
By don't work I mean, they always return false, regardless of the other operand. Every comparison with NULL will result in false, so X = null and X != null are both false. Sounds like quantum physics, doesn't it? :)
This rule does apply to all databases, by the way.
IS and IS NOT for NULL
You can use the special is and is not operators for null, so the following two ifs are semantically the same (in Oracle):
if C is not null then
if C is not '' then
But this one is wrong:
if C != '' then
Because it actually means the following, and always returns false:
if C != null then
Most string functions return NULL as well for NULL input
This can be a huge nuisance, since even length('') returns null instead of 0: Easy to make a mistake there.
select length('') from dual -- Returns NULL
Extra tricky, because a 'normal' comparison with 0 returns false. So the following (accidentally) works
select
case when length(StringField) > 0 then
'string is not empty'
else
'string is empty'
end
from dual
But this one does not:
select
case when length(StringField) = 0 then
'string is empty'
else
'string is not empty'
end
from dual
In Oracle NULL cannot be compared with a value
hence you can use the function NVL(x,y) in which if x has value the function returns x and if x is null it returns y
IF (NVL(NULL,'D') != 'C')
THEN
DBMS_OUTPUT.put_line ('1');
END IF;
IF(NVL(Trim(''),'D') <> 'C') THEN
DBMS_OUTPUT.put_line ('2');
END IF;
IF(NVL(Trim(''),'D') != 'C') THEN
DBMS_OUTPUT.put_line ('3');
END IF;
For me every condition from your question means the same as:
IF('C' is not null) THEN
DBMS_OUTPUT.put_line ('1');
END IF;
This is properly way to compare your value with NULL
Trim('') means null, so this condition isn't correct
Trim('') <> 'C'
Trim('') != 'C'
and this one isn't correct too
null != 'C'

How can I determine if a string is numeric in SQL?

In a SQL query on Oracle 10g, I need to determine whether a string is numeric or not. How can I do this?
You can use REGEXP_LIKE:
SELECT 1 FROM DUAL
WHERE REGEXP_LIKE('23.9', '^\d+(\.\d+)?$', '')
You ca try this:
SELECT LENGTH(TRIM(TRANSLATE(string1, ' +-.0123456789', ' '))) FROM DUAL
where string1 is what you're evaluating. It will return null if numeric. Look here for further clarification
I don't have access to a 10G instance for testing, but this works in 9i:
CREATE OR REPLACE FUNCTION is_numeric (p_val VARCHAR2)
RETURN NUMBER
IS
v_val NUMBER;
BEGIN
BEGIN
IF p_val IS NULL OR TRIM (p_val) = ''
THEN
RETURN 0;
END IF;
SELECT TO_NUMBER (p_val)
INTO v_val
FROM DUAL;
RETURN 1;
EXCEPTION
WHEN OTHERS
THEN
RETURN 0;
END;
END;
SELECT is_numeric ('333.5') is_numeric
FROM DUAL;
I have assumed you want nulls/empties treated as FALSE.
As pointed out by Tom Kyte in http://asktom.oracle.com/pls/apex/f?p=100:11:0::::P11_QUESTION_ID:7466996200346537833, if you're using the built-in TO_NUMBER in a user defined function, you may need a bit of extra trickery to make it work.
FUNCTION is_number(x IN VARCHAR2)
RETURN NUMBER
IS
PROCEDURE check_number (y IN NUMBER)
IS
BEGIN
NULL;
END;
BEGIN
PRAGMA INLINE(check_number, 'No');
check_number(TO_NUMBER(x);
RETURN 1;
EXCEPTION
WHEN INVALID_NUMBER
THEN RETURN 0;
END is_number;
The problem is that the optimizing compiler may recognize that the result of the TO_NUMBER is not used anywhere and optimize it away.
Says Tom (his example was about dates rather then numbers):
the disabling of function inlining will make it do the call to
check_date HAS to be made as a function call - making it so that the
DATE has to be pushed onto the call stack. There is no chance for the
optimizing compiler to remove the call to to_date in this case. If the
call to to_date needed for the call to check_date fails for any
reason, we know that the string input was not convertible by that date
format.
Here is a method to determine numeric that can be part of a simple query, without creating a function. Accounts for embedded spaces, +- not the first character, or a second decimal point.
var v_test varchar2(20);
EXEC :v_test := ' -24.9 ';
select
(case when trim(:v_test) is null then 'N' ELSE -- only banks, or null
(case when instr(trim(:v_test),'+',2,1) > 0 then 'N' ELSE -- + sign not first char
(case when instr(trim(:v_test),'-',2,1) > 0 then 'N' ELSE -- - sign not first char
(case when instr(trim(:v_test),' ',1,1) > 0 then 'N' ELSE -- internal spaces
(case when instr(trim(:v_test),'.',1,2) > 0 then 'N' ELSE -- second decimal point
(case when LENGTH(TRIM(TRANSLATE(:v_test, ' +-.0123456789',' '))) is not null then 'N' ELSE -- only valid numeric charcters.
'Y'
END)END)END)END)END)END) as is_numeric
from dual;
I found that the solution
LENGTH(TRIM(TRANSLATE(string1, ' +-.0123456789', ' '))) is null
allows embedded blanks ... it accepts "123 45 6789" which for my purpose is not a number.
Another level of trim/translate corrects this. The following will detect a string field containing consecutive digits with leading or trailing blanks such that to_number(trim(string1)) will not fail
LENGTH(TRIM(TRANSLATE(translate(trim(string1),' ','X'), '0123456789', ' '))) is null
For integers you can use the below. The first translate changes spaces to be a character and the second changes numbers to be spaces. The Trim will then return null if only numbers exist.
TRIM(TRANSLATE(TRANSLATE(TRIM('1 2 3d 4'), ' ','#'),'0123456789',' ')) is null