Passing a delimited string in the NOT IN clause - oracle

The below SQL conceptually replicates the problem I'm trying to solve. Despite passing a NOT IN clause all three records are returned.
SELECT * FROM (
SELECT 'JACK' AS VALUE FROM DUAL
UNION
SELECT 'JOHN' AS VALUE FROM DUAL
UNION
SELECT 'BOB' AS VALUE FROM DUAL
) WHERE VALUE NOT IN (SELECT 'BOB,JOHN' FROM DUAL);
I have a table that holds a delimited string that I want to use as the criteria to exclude records from the dataset. However, the problem I have is that the returned string is not broken down into its delimited items. I want the above to translate to:
SELECT * FROM (
SELECT 'JACK' AS VALUE FROM DUAL
UNION
SELECT 'JOHN' AS VALUE FROM DUAL
UNION
SELECT 'BOB' AS VALUE FROM DUAL
) WHERE VALUE NOT IN ('BOB','JOHN');

You can use regexp_substr for that problem:
SELECT * FROM (
SELECT 'JACK' AS VALUE FROM DUAL
UNION
SELECT 'JOHN' AS VALUE FROM DUAL
UNION
SELECT 'BOB' AS VALUE FROM DUAL
)
WHERE VALUE NOT IN (SELECT regexp_substr('BOB,JOHN','[^,]+', 1, LEVEL) FROM dual CONNECT BY regexp_substr('BOB,JOHN', '[^,]+', 1, LEVEL) IS NOT NULL)

'BOB,JOHN' is not a list of two string values it is one string value that just happens to contain a comma in the string and:
'JACK' = 'BOB,JOHN'
'JOHN' = 'BOB,JOHN'
'BOB' = 'BOB,JOHN'
Are all false so your query will return all rows as matched by the NOT IN filter.
You can surround your values and list with the delimiter characters and test whether the value is not a sub-string of the list like this:
SELECT *
FROM (
SELECT 'JACK' AS VALUE FROM DUAL UNION ALL
SELECT 'JOHN' AS VALUE FROM DUAL UNION ALL
SELECT 'BOB' AS VALUE FROM DUAL
)
WHERE INSTR( ',' || 'BOB,JOHN' || ',', ',' || value || ',' ) = 0
Or you can use a user-defined collection:
CREATE OR REPLACE TYPE stringlist IS TABLE OF VARCHAR2(20);
Then use the MEMBER OF operator to test whether a value is a member of the collection:
SELECT *
FROM (
SELECT 'JACK' AS VALUE FROM DUAL UNION ALL
SELECT 'JOHN' AS VALUE FROM DUAL UNION ALL
SELECT 'BOB' AS VALUE FROM DUAL
)
WHERE VALUE NOT MEMBER OF StringList( 'BOB', 'JOHN' );

Related

REGEXP_SUBSTR not able to process only current row

(SELECT LISTAGG(EVENT_DESC, ',') WITHIN GROUP (ORDER BY EVENT_DESC) FROM EVENT_REF WHERE EVENT_ID IN
( SELECT REGEXP_SUBSTR(AFTER_VALUE,'[^,]+', 1, level) FROM DUAL
CONNECT BY REGEXP_SUBSTR(AFTER_VALUE, '[^,]+', 1, level) IS NOT NULL
)
)
A table from which I am fetching AFTER_VALUE has values of integer which is comma seperated like
AFTER_VALUE data
Expected output
1
Event1
1,2
Event1,Event2
1,12,2,5
Event1,Event12,Event2,Event5
15,13
Event15,Event13
these are Ids in EVENT_REF table which have some description. I am trying to basically present
ex. 1,2 as Event1, Event2 and send back from query. There are multiple events so using REPLACE would be very tedious.
When using above query I'm getting error as “ORA-01722: invalid number” whenever there is more than one value in AFTER_VALUE column Ex. if there exists only one id , then the query works but for values like 1,2 or 1,13 etc it throws invalid number error.
PS: The event names are not Event1,Event2 etc , I have just put for reference.
You don't even need regular expressions for this assignment. Standard string function replace() can do the same thing, and faster. You only need an extra 'Event' at the beginning of the string, since that one doesn't "replace" anything.
Like this: (note that you don't need the with clause; I included it only for quick testing)
with
event_ref (after_value) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select after_value,
'Event' || replace(after_value, ',', ',Event') as desired_output
from event_ref
;
AFTER_VALUE DESIRED_OUTPUT
----------- -----------------------------
1 Event1
1,2 Event1,Event2
1,12,2,5 Event1,Event12,Event2,Event5
15,13 Event15,Event13
Ah,ok, looks, like you have other characters in your comma-separated list, so you can use this query:
with EVENT_REF(EVENT_ID,EVENT_DESC) as (
select 1, 'Desc 1' from dual union all
select 2, 'Desc 2' from dual union all
select 3, 'Desc 3' from dual union all
select 4, 'Desc 4' from dual union all
select 5, 'Desc 5' from dual union all
select 12, 'Desc12' from dual union all
select 13, 'Desc13' from dual union all
select 15, 'Desc15' from dual
)
select
(SELECT LISTAGG(EVENT_DESC, ',')
WITHIN GROUP (ORDER BY EVENT_DESC)
FROM EVENT_REF
WHERE EVENT_ID IN
( SELECT to_number(REGEXP_SUBSTR(AFTER_VALUE,'\d+', 1, level))
FROM DUAL
CONNECT BY level<=REGEXP_COUNT(AFTER_VALUE, '\d+')
)
)
from (
select '1' AFTER_VALUE from dual union all
select '1,2' AFTER_VALUE from dual union all
select '1,12,2,5' AFTER_VALUE from dual union all
select '15,13' AFTER_VALUE from dual
);
PS. And do not forget that to_number has 'default on conversion error' now: https://docs.oracle.com/en/database/oracle/oracle-database/12.2/sqlrf/TO_NUMBER.html
There is no need to split and concatenate substrings, just use regexp_replace:
with EVENT_REF (AFTER_VALUE) as (
select '1' from dual union all
select '1,2' from dual union all
select '1,12,2,5' from dual union all
select '15,13' from dual
)
select regexp_replace(AFTER_VALUE,'(\d+)','Event\1') from EVENT_REF;
REGEXP_REPLACE(AFTER_VALUE,'(\D+)','EVENT\1')
-----------------------------------------------
Event1
Event1,Event2
Event1,Event12,Event2,Event5
Event15,Event13

Is is possible to start and Oracle SQL Procedure with a CTE?

I have a CTE that I know works, but I want to use it as a procedure so I can parameterize some of the queries within it. Here is the code I tried to run to create the procedure
CREATE OR REPLACE PROCEDURE VATIS_OWNER.getEnvVariables(PF IN VARCHAR, SN IN VARCHAR) AS
BEGIN
--Joins environemt variables from TESTCASES and ENVIRONMENTS tables and transposes result into columns 'ColumnName' and 'ColumnValue' and the ProfileId
--Is used by getAllDynamicData and unioned with results from sp_getProfileDetails
with
cte as (
SELECT
T.TestcaseID,
T.ProfileID,
T.TrustID,
T.DNIS,
T.TESTID,
T.ACDID,
--Must be 8 digits long, so leading 0's are added
SUBSTR(CONCAT('00000000',T.testcaseid),-8, 8) as TestCaseNo,
T.HostId,
E.TFN
FROM TESTCASES T
FULL JOIN ENVIRONMENTS E ON T.ENV_NAME = E.ENV_NAME
--Only returns env vars associated with testcase #SN
where T.TestcaseID = SN
),
cte2 as (
Select A.ProfileID
,B.*
From cte A
--Transpose happens here
Cross Apply ( SELECT 'TestcaseID' AS ColumnName,A.TestcaseID AS ColumnValue FROM DUAL UNION ALL
SELECT 'ProfileID' AS ColumnName,A.ProfileID AS ColumnValue FROM DUAL UNION ALL
SELECT 'TrustID' AS ColumnName,A.TrustID AS ColumnValue FROM DUAL UNION ALL
SELECT 'DNIS' AS ColumnName,SUBSTR(A.DNIS,-7,7) AS ColumnValue FROM DUAL UNION ALL
SELECT 'TESTID' AS ColumnName,A.TESTID AS ColumnValue FROM DUAL UNION ALL
SELECT 'ACDID' AS ColumnName,A.ACDID AS ColumnValue FROM DUAL UNION ALL
SELECT 'TestCaseNo' AS ColumnName,A.TestCaseNo AS ColumnValue FROM DUAL UNION ALL
SELECT 'HostId' AS ColumnName,A.HostId AS ColumnValue FROM DUAL UNION ALL
SELECT 'TFN' AS ColumnName,A.TFN AS ColumnValue FROM DUAL UNION ALL
SELECT 'INVALIDANI' AS ColumnName,SUBSTR(CONCAT(A.TestcaseID ,A.TESTID),-10,10) AS ColumnValue FROM DUAL
) B
)
select distinct * from cte2 where profileID = PF;
END getEnvVariables;
If I replace SN and PF with string values and run just the CTE and query, it works. And an equivalent version works as a Stored Procedure in SQL Server, but when I try to create this procedure in Oracle, I get this compile error:
PLS-00428: an INTO clause is expected in this SELECT statement
Any idea why I can't use this in a procedure? I am more familiar with SQL Server than Oracle, so if I've forgotten something please let me know. Thanks in advance.
If you want to return the select result from this procedure, You have to use a SYS_REFCURSOR as below -
CREATE OR replace PROCEDURE vatis_owner.Getenvvariables(pf IN VARCHAR,
sn IN VARCHAR,
res OUT SYS_REFCURSOR) AS
BEGIN
--Joins environemt variables from TESTCASES and ENVIRONMENTS tables and transposes result into columns 'ColumnName' and 'ColumnValue' and the ProfileId
--Is used by getAllDynamicData and unioned with results from sp_getProfileDetails
OPEN res FOR
WITH cte AS
(
SELECT t.testcaseid,
t.profileid,
t.trustid,
t.dnis,
t.testid,
t.acdid,
--Must be 8 digits long, so leading 0's are added
Substr(Concat('00000000',t.testcaseid),-8, 8) AS testcaseno,
t.hostid,
e.tfn
FROM testcases t
full join environments e
ON t.env_name = e.env_name
--Only returns env vars associated with testcase #SN
WHERE t.testcaseid = sn ), cte2 AS
(
SELECT a.profileid ,
b.*
FROM cte a
--Transpose happens here
cross apply
(
SELECT 'TestcaseID' AS columnname,
a.testcaseid AS columnvalue
FROM dual
UNION ALL
SELECT 'ProfileID' AS columnname,
a.profileid AS columnvalue
FROM dual
UNION ALL
SELECT 'TrustID' AS columnname,
a.trustid AS columnvalue
FROM dual
UNION ALL
SELECT 'DNIS' AS columnname,
substr(a.dnis,-7,7) AS columnvalue
FROM dual
UNION ALL
SELECT 'TESTID' AS columnname,
a.testid AS columnvalue
FROM dual
UNION ALL
SELECT 'ACDID' AS columnname,
a.acdid AS columnvalue
FROM dual
UNION ALL
SELECT 'TestCaseNo' AS columnname,
a.testcaseno AS columnvalue
FROM dual
UNION ALL
SELECT 'HostId' AS columnname,
a.hostid AS columnvalue
FROM dual
UNION ALL
SELECT 'TFN' AS columnname,
a.tfn AS columnvalue
FROM dual
UNION ALL
SELECT 'INVALIDANI' AS columnname,
substr(concat(a.testcaseid ,a.testid),-10,10) AS columnvalue
FROM dual ) b )
SELECT DISTINCT *
FROM cte2
WHERE profileid = pf;
END getenvvariables;
Then later on you can use this proc to return result in a ref_cursor variable.
DECALRE
RES SYS_REFCURSOR;
BEGIN
vatis_owner.Getenvvariables(pf,
sn,
RES);
FOR I IN 1..RES.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(I.YOUR_DESIRED_COLUMNS);
END LOOP;
END;
I can see that you are using CROSS APPLY in your query that means you must be using version 12C or higher. So you can use DBMS_SQL.RETURN_RESULT function -
CREATE OR replace PROCEDURE vatis_owner.Getenvvariables(pf IN VARCHAR,
sn IN VARCHAR) AS
RES SYS_REFCURSOR;
BEGIN
--Joins environemt variables from TESTCASES and ENVIRONMENTS tables and transposes result into columns 'ColumnName' and 'ColumnValue' and the ProfileId
--Is used by getAllDynamicData and unioned with results from sp_getProfileDetails
OPEN RES FOR
WITH cte AS
(
SELECT t.testcaseid,
t.profileid,
t.trustid,
t.dnis,
t.testid,
t.acdid,
--Must be 8 digits long, so leading 0's are added
Substr(Concat('00000000',t.testcaseid),-8, 8) AS testcaseno,
t.hostid,
e.tfn
FROM testcases t
full join environments e
ON t.env_name = e.env_name
--Only returns env vars associated with testcase #SN
WHERE t.testcaseid = sn ), cte2 AS
(
SELECT a.profileid ,
b.*
FROM cte a
--Transpose happens here
cross apply
(
SELECT 'TestcaseID' AS columnname,
a.testcaseid AS columnvalue
FROM dual
UNION ALL
SELECT 'ProfileID' AS columnname,
a.profileid AS columnvalue
FROM dual
UNION ALL
SELECT 'TrustID' AS columnname,
a.trustid AS columnvalue
FROM dual
UNION ALL
SELECT 'DNIS' AS columnname,
substr(a.dnis,-7,7) AS columnvalue
FROM dual
UNION ALL
SELECT 'TESTID' AS columnname,
a.testid AS columnvalue
FROM dual
UNION ALL
SELECT 'ACDID' AS columnname,
a.acdid AS columnvalue
FROM dual
UNION ALL
SELECT 'TestCaseNo' AS columnname,
a.testcaseno AS columnvalue
FROM dual
UNION ALL
SELECT 'HostId' AS columnname,
a.hostid AS columnvalue
FROM dual
UNION ALL
SELECT 'TFN' AS columnname,
a.tfn AS columnvalue
FROM dual
UNION ALL
SELECT 'INVALIDANI' AS columnname,
substr(concat(a.testcaseid ,a.testid),-10,10) AS columnvalue
FROM dual ) b )
SELECT DISTINCT *
FROM cte2
WHERE profileid = pf;
DBMS_SQL.RETURN_RESULT(RES);
END getenvvariables;
When you will run this proc, You will see the result on console. For further information, Please read.

IN , NOT IN for null value in oracle

In oracle why "Not in" doesn't work on null values but "IN" works
For eg
with temp(n,p) as (
select 1,2 from dual union all
select 3,2 from dual union all
select 4,6 from dual union all
select 5,6 from dual union all
select 2,8 from dual union all
select 6,8 from dual union all
select 8,null from dual
)
1. Select * from temp where n in (2,6,8,null);
2. Select * from temp where n not in (2,6,8,null);
First Statement will give the output = 2,6,8
Second statement will not give any output
Can someone please explain why?
NOT IN essentially works like this:
col NOT IN (value_a, value_b, value_c)
-- is the same as
col != value_a && col != value_b && col != value_c
If one of the values is null, the whole expression evaluates to null, not true (which you probably expect).
You can read more about it here: https://jonathanlewis.wordpress.com/2007/02/25/not-in/

Invalid number of returned comma delimited string in a IN clause in Oracle

I am trying to use a subquery that returns a comma delimited string in a IN clause.
The following way:
SELECT p.person_id, g.name || '>>' || p.firstname || ' ' || p.lastname as GROUP_PATH
FROM PERSON p
LEFT JOIN GROUP g ON (
g.group_id = p.group_id
)
WHERE p.person_id IN (
SELECT person_ids FROM other WHERE other_id = :OTHER_ID
)
ORDER BY lower(GROUP_PATH)
And I am getting the following error:
ORA-01722: invalid number.
Is there a better way to do this or even possible?
The most obvious explanation is that you are trying to do math with a string...
The attempted conversion of a
character string to a number failed
because the character string was not a
valid numeric literal. Only numeric
fields or character fields containing
numeric data may be used in arithmetic
functions or expressions. Only numeric
fields may be added to or subtracted
from dates.
http://ora-01722.ora-code.com/
Update #1:
Your description worries me:
I am trying to use a subquery that
returns a comma delimited string in a
IN clause.
Your subquery should not return a comma delimited string (unless g.group_id is a string and expects a comma delimited string). You must retrieve individual items in as many rows as needed (less than 1,000 anyway).
Update #2:
Just to make it clear:
SELECT *
FROM (
SELECT 1 AS FOO_ID FROM DUAL UNION SELECT 2 FROM DUAL UNION SELECT 3 FROM DUAL
) FOO;
FOO_ID
----------------------
1
2
3
You can do this:
SELECT *
FROM (
SELECT 1 AS FOO_ID FROM DUAL UNION SELECT 2 FROM DUAL UNION SELECT 3 FROM DUAL
) FOO
WHERE FOO_ID IN (1, 2);
FOO_ID
----------------------
1
2
But not this:
SELECT *
FROM (
SELECT 1 AS FOO_ID FROM DUAL UNION SELECT 2 FROM DUAL UNION SELECT 3 FROM DUAL
) FOO
WHERE FOO_ID IN ('1,2');
SQL Error: ORA-01722: invalid number
Because you cannot compare number 1 with string '1,2'. Subqueries follow similar rules:
SELECT *
FROM (
SELECT 1 AS FOO_ID FROM DUAL UNION SELECT 2 FROM DUAL UNION SELECT 3 FROM DUAL
) FOO
WHERE FOO_ID IN (
SELECT 1 AS FOO_ID FROM DUAL UNION SELECT 2 FROM DUAL
);
FOO_ID
----------------------
1
2
SELECT *
FROM (
SELECT 1 AS FOO_ID FROM DUAL UNION SELECT 2 FROM DUAL UNION SELECT 3 FROM DUAL
) FOO
WHERE FOO_ID IN (
SELECT '1,2' AS FOO_ID FROM DUAL
);
SQL Error: ORA-01722: invalid number
At a minimum, in order to reference the alias GROUP_PATH, you would need to need to use a nested subquery where the alias is defined before you reference it in your ORDER BY clause. That's realistically not causing the ORA-01722 error, but it is a problem
SELECT group_id, group_path
FROM (SELECT g.group_id,
g.name || '>>' || p.firstname || ' ' || p.lastname as GROUP_PATH
FROM PERSON p
LEFT JOIN GROUP g ON (
g.group_id = p.group_id
)
WHERE p.person_id IN (
SELECT person_ids FROM other WHERE other_id = :OTHER_ID
)
ORDER BY lower(GROUP_PATH)
If the PERSON_IDS column in the OTHER table is a comma separated list of values, your IN list is not going to do what you'd expect. You would need to transform the scalar string (that happens to have commas in it) into some sort of collection of multiple PERSON_ID values. There are various approaches to doing this, Tom Kyte has one example of using a variable IN list. Assuming you copy Tom's IN_LIST function, you should be able to do something like
SELECT group_id, group_path
FROM (SELECT g.group_id,
g.name || '>>' || p.firstname || ' ' || p.lastname as GROUP_PATH
FROM PERSON p
LEFT JOIN GROUP g ON (
g.group_id = p.group_id
)
WHERE p.person_id IN (
SELECT column_value
FROM TABLE(SELECT in_list(person_ids)
FROM other
WHERE other_id = :OTHER_ID)
)
ORDER BY lower(GROUP_PATH)

how to replace multiple strings together in Oracle

I have a string coming from a table like "can no pay{1},as your payment{2}due on {3}". I want to replace {1} with some value , {2} with some value and {3} with some value .
Is it Possible to replace all 3 in one replace function ? or is there any way I can directly write query and get replaced value ? I want to replace these strings in Oracle stored procedure the original string is coming from one of my table I am just doing select on that table
and then I want to replace {1},{2},{3} values from that string to the other value that I have from another table
Although it is not one call, you can nest the replace() calls:
SET mycol = replace( replace(mycol, '{1}', 'myoneval'), '{2}', mytwoval)
If there are many variables to replace and you have them in another table and if the number of variables is variable you can use a recursive CTE to replace them.
An example below. In table fg_rulez you put the strings with their replacement. In table fg_data you have your input strings.
set define off;
drop table fg_rulez
create table fg_rulez as
select 1 id,'<' symbol, 'less than' text from dual
union all select 2, '>', 'great than' from dual
union all select 3, '$', 'dollars' from dual
union all select 4, '&', 'and' from dual;
drop table fg_data;
create table fg_Data AS(
SELECT 'amount $ must be < 1 & > 2' str FROM dual
union all
SELECT 'John is > Peter & has many $' str FROM dual
union all
SELECT 'Eliana is < mary & do not has many $' str FROM dual
);
WITH q(str, id) as (
SELECT str, 0 id
FROM fg_Data
UNION ALL
SELECT replace(q.str,symbol,text), fg_rulez.id
FROM q
JOIN fg_rulez
ON q.id = fg_rulez.id - 1
)
SELECT str from q where id = (select max(id) from fg_rulez);
So, a single replace.
Result:
amount dollars must be less than 1 and great than 2
John is great than Peter and has many dollars
Eliana is less than mary and do not has many dollars
The terminology symbol instead of variable comes from this duplicated question.
Oracle 11gR2
Let's write the same sample as a CTE only:
with fg_rulez as (
select 1 id,'<' symbol, 'less than' text from dual
union all select 2, '>', 'greater than' from dual
union all select 3, '$', 'dollars' from dual
union all select 4, '+', 'and' from dual
), fg_Data AS (
SELECT 'amount $ must be < 1 + > 2' str FROM dual
union all
SELECT 'John is > Peter + has many $' str FROM dual
union all
SELECT 'Eliana is < mary + do not has many $' str FROM dual
), q(str, id) as (
SELECT str, 0 id
FROM fg_Data
UNION ALL
SELECT replace(q.str,symbol,text), fg_rulez.id
FROM q
JOIN fg_rulez
ON q.id = fg_rulez.id - 1
)
SELECT str from q where id = (select max(id) from fg_rulez);
If the number of values to replace is too big or you need to be able to easily maintain it, you could also split the string, use a dictionary table and finally aggregate the results
In the example below I'm assuming that the words in your string are separated with blankspaces and the wordcount in the string will not be bigger than 100 (pivot table cardinality)
with Dict as
(select '{1}' String, 'myfirstval' Repl from dual
union all
select '{2}' String, 'mysecondval' Repl from dual
union all
select '{3}' String, 'mythirdval' Repl from dual
union all
select '{Nth}' String, 'myNthval' Repl from dual
)
,MyStrings as
(select 'This is the first example {1} ' Str, 1 strnum from dual
union all
select 'In the Second example all values are shown {1} {2} {3} {Nth} ', 2 from dual
union all
select '{3} Is the value for the third', 3 from dual
union all
select '{Nth} Is the value for the Nth', 4 from dual
)
-- pivot is used to split the stings from MyStrings. We use a cartesian join for this
,pivot as (
Select Rownum Pnum
From dual
Connect By Rownum <= 100
)
-- StrtoRow is basically a cartesian join between MyStings and Pivot.
-- There as many rows as individual string elements in the Mystring Table
-- (Max = Numnber of rows Mystring table * 100).
,StrtoRow as
(
SELECT rownum rn
,ms.strnum
,REGEXP_SUBSTR (Str,'[^ ]+',1,pv.pnum) TXT
FROM MyStrings ms
,pivot pv
where REGEXP_SUBSTR (Str,'[^ ]+',1,pv.pnum) is not null
)
-- This is the main Select.
-- With the listagg function we group the string together in lines using the key strnum (group by)
-- The NVL gets the translations:
-- if there is a Repl (Replacement from the dict table) then provide it,
-- Otherwise TXT (string without translation)
Select Listagg(NVL(Repl,TXT),' ') within group (order by rn)
from
(
-- outher join between strings and the translations (not all strings have translations)
Select sr.TXT, d.Repl, sr.strnum, sr.rn
from StrtoRow sr
,dict d
where sr.TXT = d.String(+)
order by strnum, rn
) group by strnum
If you are doing this inside of a select, you can just piece it together, if your replacement values are columns, using string concatenation.

Resources