I am newbie so that maybe this question has been made one or two million times, but it is not findable / searchable in the knowledge database.
In Oracle PL/SQL, it is normal to query as follows:
select a,b,c
from table_foo
where c in (select k from table(array_bar));
But I need all the opposite of that. I need a kind of "IS_CONTAINED" operator, like this:
select a,b,c
from table_foo
where AT_LEAST_ONE_OF_THE_ITEMS_IN (select k from table(array_bar)) IS_CONTAINED_IN c;
I have my own ideas to implement it using a function with a loop. But maybe some genius has found a simple way to do it without a function. This is to say, maybe the operator IS_CONTAINED is already invented by Oracle and I haven't found it out.
Sorry if this question is repeated. I promise I have searched for it in the knowledge base. But it seems that nobody in the space-time of this Universe has never needed the super-obvious operator IS_CONTAINED.
SOLUTION:
Thanks to everybody for the suggestions. In the end, I had to use some functions, but I think I got a good solution. The situation is: I have a table of centers. Each center can be in one or more cities, this is to say, it's a 1 to N relationship. But this relationship is done using a single table. This table contains some fields. One of these fields, named 'cities_list', contains all related cities separated by semicolons. It's like this:
CODE DESCRIPTION CITIES_LIST
---- ----------- -----------
0001 Desc 0001 London; Berlin; NY; SF
0002 Desc 0002 Paris; Madrid; Rome
0003 Desc 0003 Berlin; Paris; London
0004 Desc 0004 Madrid;NY;Tokyo
0005 Repe 0005 Rome;Rome;Rome;LA;LA;LA;
0006 One 0006 NY
0007 Desc 0007 Sydney;Tokyo;Madrid
0008 Desc 0008 LA;SF;NY
0009 Desc 0009 Seoul;Beijing;
0010 Error0010 Beijing;;;;OZ;
0011 None 0011 (null)
0012 All 0012 London;Paris;Berlin;Madrid;Rome;NY;SF;LA;Seoul;Beijing;Tokyo;Sydney
Possible cities are: London; Paris; Berlin; Madrid; Rome; NY; SF; LA; Seoul; Beijing; Tokyo; Sydney.
In order to filter records of that table, the user can select, through a combo, one or more of those cities. Selected cities are passed to the PL/SQL query as a string (varchar) of cities separated by a hash sign (#). For instance 'London#Paris#Sydney'.
The PL/SQL has to select the records that have at least one city in common between the field 'cities_list' and the string of cities passed from the combo. First, I put here the PL/SQL code and I will explain it later on:
--1.SELECT AND EXECUTE THIS:
SET SERVEROUTPUT ON;
--2.SELECT AND EXECUTE THIS:
DROP TABLE table_centers; CREATE GLOBAL TEMPORARY TABLE table_centers (code VARCHAR2(10), description VARCHAR2(100), cities_list VARCHAR2(1000));
--3.SELECT AND EXECUTE THIS:
CREATE OR REPLACE TYPE table_TYPE IS TABLE OF VARCHAR2(250);
--4.SELECT AND EXECUTE THIS:
CREATE OR REPLACE FUNCTION VARCHAR_TO_TABLE (input_varchar VARCHAR2, separator VARCHAR2 DEFAULT ';')
RETURN table_TYPE
IS
--VARS
output_table table_TYPE := table_TYPE();
BEGIN
--For better performance, input_varchar is splitted without blanks into output_table using the regular expression [^;]+
SELECT
--The Keyword 'level' in statement 'regexp_substr' refers to a pseudocolumn in Oracle
TRIM(regexp_substr(input_varchar,'[^' || separator || ']+', 1, level))
BULK COLLECT INTO
output_table
FROM DUAL
CONNECT BY
regexp_substr(input_varchar,'[^' || separator || ']+', 1, level) IS NOT NULL;
--Now we have all chunks into the table output_table
RETURN output_table;
END VARCHAR_TO_TABLE;
--5.SELECT AND EXECUTE THIS:
CREATE OR REPLACE FUNCTION INTERSECT_TABLES(input_A VARCHAR2 , separator_A VARCHAR2 , input_B VARCHAR2 , separator_B VARCHAR2)
RETURN NUMBER
IS
--VARS
A table_TYPE;
B table_TYPE;
result BOOLEAN;
BEGIN
--Splits input_A and input_B into tables and checks if there is overlapping
A := VARCHAR_TO_TABLE(input_A, separator_A);
B := VARCHAR_TO_TABLE(input_B, separator_B);
--If intersection is not empty result is TRUE
result := A multiset intersect B is not empty;
-- Returns 1 if intersection is not empty, returns 0 otherwise (Note that functions called from a SQL query cannot take any BOOLEAN parameters)
IF result = TRUE THEN RETURN 1; ELSE RETURN 0; END IF;
END INTERSECT_TABLES;
--6.SELECT AND EXECUTE THIS:
CREATE OR REPLACE PROCEDURE GET_CENTERS (cities_input VARCHAR2 , separator_input VARCHAR2 , out_Cursor OUT sys_refcursor)
AS
BEGIN
OPEN out_Cursor FOR
SELECT tc.code, tc.description, tc.cities_list
FROM table_centers tc
--Has current record some city in common with cities_input? If yes, select current record
WHERE INTERSECT_TABLES(cities_input , separator_input , tc.cities_list , ';') = 1;
END GET_CENTERS;
--7.SELECT AND EXECUTE THIS:
BEGIN
DELETE FROM table_centers; COMMIT;
INSERT ALL
--We'll use following cities: London Paris Berlin Madrid Rome NY SF LA Seoul Beijing Tokyo Sydney
INTO table_centers (code,description,cities_list) VALUES ('0001', 'Desc 0001', 'London; Berlin; NY; SF')
INTO table_centers (code,description,cities_list) VALUES ('0002', 'Desc 0002', 'Paris; Madrid; Rome')
INTO table_centers (code,description,cities_list) VALUES ('0003', 'Desc 0003', 'Berlin; Paris; London')
INTO table_centers (code,description,cities_list) VALUES ('0004', 'Desc 0004', 'Madrid;NY;Tokyo')
INTO table_centers (code,description,cities_list) VALUES ('0005', 'Repe 0005', 'Rome;Rome;Rome;LA;LA;LA;')
INTO table_centers (code,description,cities_list) VALUES ('0006', 'One 0006', 'NY')
INTO table_centers (code,description,cities_list) VALUES ('0007', 'Desc 0007', 'Sydney;Tokyo;Madrid')
INTO table_centers (code,description,cities_list) VALUES ('0008', 'Desc 0008', 'LA;SF;NY')
INTO table_centers (code,description,cities_list) VALUES ('0009', 'Desc 0009', 'Seoul;Beijing;')
INTO table_centers (code,description,cities_list) VALUES ('0010', 'Error0010', 'Beijing;;;;OZ;')
INTO table_centers (code,description,cities_list) VALUES ('0011', 'None 0011', '')
INTO table_centers (code,description,cities_list) VALUES ('0012', 'All 0012', 'London;Paris;Berlin;Madrid;Rome;NY;SF;LA;Seoul;Beijing;Tokyo;Sydney')
SELECT 1 FROM DUAL;
END;
--8.SELECT AND EXECUTE THIS:
SELECT * FROM table_centers;
I have used 'Oracle SQL Developer'. You can select the sentences one by one and execute them with the F9 key. You can also create a Package.
If someone wants to test that code, you can also select and execute with F9 the following query:
--9.SELECT AND EXECUTE THIS:
DECLARE
--VARS
out_Cursor sys_refcursor;
cities_array table_TYPE;
citiesA varchar(1000) := 'London#Paris#Berlin#Madrid#Rome#NY#SF#LA# Seoul # Beijing # Tokyo # Sydney ';
citiesB varchar(1000) := 'London;Paris;Berlin;Madrid;Rome;NY;SF;LA; Seoul ; Beijing ; Tokyo ; Sydney ';
Rcode table_centers.code%TYPE;
Rdescription table_centers.description%TYPE;
Rcities_list table_centers.cities_list%TYPE;
CR char := CHR(13);
TAB char := CHR(9);
BEGIN
--TEST 1
dbms_output.put_line('TEST 1: ' || CR);
cities_array := table_TYPE();
cities_array := VARCHAR_TO_TABLE(citiesA, '#');
--Now we have all cities in the array cities_array
FOR elem in 1 .. cities_array.count LOOP
dbms_output.put_line(TAB || elem || ':' || cities_array(elem) || '.');
END LOOP;
--TEST 2
dbms_output.put_line('TEST 2: ' || CR);
cities_array := table_TYPE();
cities_array := VARCHAR_TO_TABLE(citiesB, ';');
--Now we have all cities in the array cities_array
FOR elem in 1 .. cities_array.count LOOP
dbms_output.put_line(TAB || elem || ':' || cities_array(elem) || '.');
END LOOP;
--TEST 3
dbms_output.put_line('TEST 3: ' || CR);
GET_CENTERS(citiesA, '#', out_Cursor);
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
WHILE out_Cursor%FOUND LOOP
dbms_output.put_line(TAB || 'CITIES:' || Rcities_list || '.');
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
END LOOP;
close out_Cursor;
--TEST 4
dbms_output.put_line('TEST 4: ' || CR);
GET_CENTERS('London#Paris#Sydney', '#', out_Cursor);
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
WHILE out_Cursor%FOUND LOOP
dbms_output.put_line(TAB || 'CITIES:' || Rcities_list || '.');
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
END LOOP;
close out_Cursor;
--TEST 5
dbms_output.put_line('TEST 5: ' || CR);
GET_CENTERS('Madrid', '#', out_Cursor);
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
WHILE out_Cursor%FOUND LOOP
dbms_output.put_line(TAB || 'CITIES:' || Rcities_list || '.');
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
END LOOP;
close out_Cursor;
--TEST 6
dbms_output.put_line('TEST 6: ' || CR);
GET_CENTERS('Gotham City', '#', out_Cursor);
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
WHILE out_Cursor%FOUND LOOP
dbms_output.put_line(TAB || 'CITIES:' || Rcities_list || '.');
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
END LOOP;
close out_Cursor;
--TEST 7
dbms_output.put_line('TEST 7: ' || CR);
GET_CENTERS('', '#', out_Cursor);
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
WHILE out_Cursor%FOUND LOOP
dbms_output.put_line(TAB || 'CITIES:' || Rcities_list || '.');
fetch out_Cursor into Rcode,Rdescription,Rcities_list;
END LOOP;
close out_Cursor;
END;
You can modify TEST 7 and put your own values in the first parameter of the function 'GET_CENTERS'. I have executed this query and I have got these results:
TEST 1:
1:London.
2:Paris.
3:Berlin.
4:Madrid.
5:Rome.
6:NY.
7:SF.
8:LA.
9:Seoul.
10:Beijing.
11:Tokyo.
12:Sydney.
TEST 2:
1:London.
2:Paris.
3:Berlin.
4:Madrid.
5:Rome.
6:NY.
7:SF.
8:LA.
9:Seoul.
10:Beijing.
11:Tokyo.
12:Sydney.
TEST 3:
CITIES:London; Berlin; NY; SF.
CITIES:Paris; Madrid; Rome.
CITIES:Berlin; Paris; London.
CITIES:Madrid;NY;Tokyo.
CITIES:Rome;Rome;Rome;LA;LA;LA;.
CITIES:NY.
CITIES:Sydney;Tokyo;Madrid.
CITIES:LA;SF;NY.
CITIES:Seoul;Beijing;.
CITIES:Beijing;;;;OZ;.
CITIES:London;Paris;Berlin;Madrid;Rome;NY;SF;LA;Seoul;Beijing;Tokyo;Sydney.
TEST 4:
CITIES:London; Berlin; NY; SF.
CITIES:Paris; Madrid; Rome.
CITIES:Berlin; Paris; London.
CITIES:Sydney;Tokyo;Madrid.
CITIES:London;Paris;Berlin;Madrid;Rome;NY;SF;LA;Seoul;Beijing;Tokyo;Sydney.
TEST 5:
CITIES:Paris; Madrid; Rome.
CITIES:Madrid;NY;Tokyo.
CITIES:Sydney;Tokyo;Madrid.
CITIES:London;Paris;Berlin;Madrid;Rome;NY;SF;LA;Seoul;Beijing;Tokyo;Sydney.
TEST 6:
TEST 7:
CITIES:.
The nub of the issue is the function 'INTERSECT_TABLES'. This function uses the sentence " result := A multiset intersect B is not empty; ". A and B are variables of type 'TABLE'. The operator '... multiset intersect ... is not empty' returns TRUE if tables A and B have at least one item (row) with the same value (text or number), regardless of its order or position in each table.
EXPLANATION:
I have created a temporary table named 'table_centers' and I have filled it in with some data. In order to query this table, I have created following functions:
The function 'VARCHAR_TO_TABLE' converts a string (varchar) into a 'table' type variable. You must pass a separator character as a parameter, so that each chunk of the string separated by that character will be one item (=row) of the resulting table. This way, I can use the same function regardless whether cities are separated by a semicolon (;) or by a hash (#). This function uses 'regexp_substr' and BULK COLLECT instead of a LOOP for better performance. The Keyword 'level' in statement 'regexp_substr' refers to a pseudocolumn in Oracle. See Is there a function to split a string in PL/SQL?.
In order to execute the final query to 'table_centers', I have implemented the function 'GET_CENTERS'. It has only one SELECT that selects the records of 'table_centers' that have in their field 'cities_list' at least one city in common with the string 'cities_input', which is passed as a parameter. Both strings are compared by the function 'INTERSECT_TABLES', being these strings previously splitted into tables through the function 'VARCHAR_TO_TABLE'.
The function 'INTERSECT_TABLES' is used in the clause 'WHERE' because the filtering must be done through this function. This is because a 'table' type can not be used inside a SQL query. Otherwise, you'll get an error "collection types can not be used inside a SQL statement". Therefore, using this function in the WHERE clause is mandatory. Also, boolean types can not be used, therefore, the function 'INTERSECT_TABLES' returns the numbers 0 or 1, not FALSE or TRUE.