LIKE on multiple values, all must match - sql

I'm querying a legacy Oracle db to match a name using LIKE. It would be good if a multi-word pattern (could be 1 word, 3 words, 8 words, whatever) could be matched against names independently of word order. Eg, if the user searched for "rob jon", it would match
ROBERT JONES
JONATHON ADAM PROBE
but not match
ROB SMITH
PETER JONES
If the requirement was "at least one word should match" I could use REGEXP_LIKE(name, 'rob|jon", 'i').
But I can't work out how to make it match only when ALL words are found somewhere in the name. I could do this using Oracle Text, but I can't use that in this db. I also can't easily store the words in a table to join to either.
The query is currently static and uses bind variables, and so I would prefer not to add an extra LIKE predicate for each word in the search pattern.
Any ideas?

According to your question and the data presented in the post, I Used this table
create table name_pattern
(
name_1 varchar(200)
);
insert statements :
insert into name_pattern
select ( 'ROBERT JONES') from dual union all
select ('JONATHON ADAM PROBE') from dual union all
select ('ROB SMITH') from dual union all
select ('PETER JONES') from dual;
commit;
try this if it works for you if you are searching "rob" and "jon" separately.
select name from table_name where lower(name) like '%rob%' and lower(name) like '%jon%' ;
try this if you want to search "rob jon" combined and to get your expected result
select * from name_pattern where REGEXP_COUNT(name_1, 'rob|jon',1, 'i') =2;

You want your query to accept one bind variable containing a blank-separated list of name parts, e.g. 'rob jon'.
You can write a recursive query to get the single name parts from that string. Then use NOT EXISTS to only keep names for which not exists any mismatch.
with parameters as
(
select :namepartlist as namepartlist from dual
)
, nameparts (namepart, namepartlist, occurrence) as
(
select regexp_substr(namepartlist, '[^ ]+', 1, 1), namepartlist, 1
from parameters
union all
select regexp_substr(namepartlist, '[^ ]+', 1, occurrence + 1), namepartlist, occurrence + 1
from nameparts
where regexp_substr(namepartlist, '[^ ]+', 1, occurrence + 1) is not null
)
select *
from mytable
where not exists
(
select null
from nameparts
where lower(mytable.name) not like '%' || lower(nameparts.namepart) || '%'
)
order by name;
You can replace
where lower(mytable.name) not like '%' || lower(nameparts.namepart) || '%'
by
where not regexp_like(mytable.name, nameparts.namepart, 'i')
if you like that better.
Demo: https://dbfiddle.uk/?rdbms=oracle_18&fiddle=5e48caaa20e9397afe65516504c62acd

if you prefer not to add an extra LIKE predicate for each word in the search pattern (the best solution IMHO) you can create a PL/SQL function to do the matching
FUNCTION PATTERN_MATCHES(pattern in varchar2, name in varchar2) returns number
is
Begin *pseudocode*
*convert pattern to lowercase*
*split pattern by spaces*
*for each part, Loop*
if NOT lower(name) like %part% then
return 0; --early exit
end;
End Loop;
return 1; --all parts have a match
end;
select * from employee where PATTERN_MATCHES(:pattern, name)=1
Then you can have a static parameterized query.
On the bright side, you'll be able to extend the behavior (add advanced search for example) without touching the front-end app.
PS: The function code is intentionally left for you to write. Ask if you need help with that.
PS2: returning "number" and not a boolean will help you with legacy database drivers. Feel free to use integer or a boolean if that works for you.

Shamelessy building on #Jfrd's solution ... is this any good ? (Performance will be shocking on large tables).
Basically, replace all spaces with '%' (not catered here for successive spaces).
var search_text varchar2(30)
exec :search_text := 'Rob Jon';
select name
from table_name
where lower(name) like REPLACE('%' || :search_text || '%', ' ', '%' ) ;

Related

convert varchar value to array to varchar sql oracle [duplicate]

I am having trouble getting a block of pl/sql code to work. In the top of my procedure I get some data from my oracle apex application on what checkboxes are checked. Because the report that contains the checkboxes is generated dynamically I have to loop through the
APEX_APPLICATION.G_F01
list and generate a comma separated string which looks like this
v_list VARCHAR2(255) := (1,3,5,9,10);
I want to then query on that list later and place the v_list on an IN clause like so
SELECT * FROM users
WHERE user_id IN (v_list);
This of course throws an error. My question is what can I convert the v_list to in order to be able to insert it into a IN clause in a query within a pl/sql procedure?
If users is small and user_id doesn't contain commas, you could use:
SELECT * FROM users WHERE ',' || v_list || ',' LIKE '%,'||user_id||',%'
This query is not optimal though because it can't use indexes on user_id.
I advise you to use a pipelined function that returns a table of NUMBER that you can query directly. For example:
CREATE TYPE tab_number IS TABLE OF NUMBER;
/
CREATE OR REPLACE FUNCTION string_to_table_num(p VARCHAR2)
RETURN tab_number
PIPELINED IS
BEGIN
FOR cc IN (SELECT rtrim(regexp_substr(str, '[^,]*,', 1, level), ',') res
FROM (SELECT p || ',' str FROM dual)
CONNECT BY level <= length(str)
- length(replace(str, ',', ''))) LOOP
PIPE ROW(cc.res);
END LOOP;
END;
/
You would then be able to build queries such as:
SELECT *
FROM users
WHERE user_id IN (SELECT *
FROM TABLE(string_to_table_num('1,2,3,4,5'));
You can use XMLTABLE as follows
SELECT * FROM users
WHERE user_id IN (SELECT to_number(column_value) FROM XMLTABLE(v_list));
I have tried to find a solution for that too but never succeeded. You can build the query as a string and then run EXECUTE IMMEDIATE, see http://docs.oracle.com/cd/B19306_01/appdev.102/b14261/dynamic.htm#i14500.
That said, it just occurred to me that the argument of an IN clause can be a sub-select:
SELECT * FROM users
WHERE user_id IN (SELECT something FROM somewhere)
so, is it possible to expose the checkbox values as a stored function? Then you might be able to do something like
SELECT * FROM users
WHERE user_id IN (SELECT my_package.checkbox_func FROM dual)
Personally, i like this approach:
with t as (select 'a,b,c,d,e' str from dual)
--
select val
from t, xmltable('/root/e/text()'
passing xmltype('<root><e>' || replace(t.str,',','</e><e>')|| '</e></root>')
columns val varchar2(10) path '/'
)
Which can be found among other examples in Thread: Split Comma Delimited String Oracle
If you feel like swamping in even more options, visit the OTN plsql forums.

How to separate records from database column and compare then in query ignoring the second text in Oracle

I have following table in oracle
review (
review_id pk,
review_name varchar2,
review_viewers varchar2
)
The review_viewers column contains the combination of user's user_name and last_name and it will contain multiple record separated by colon(:).
for example :
(1,'Planning','John Smith:Max Payne:Maria Garcia');
Now I want to write a query which will convert the above review_viewers into individual user like 'John Smith','Max Payne' striping ":" from it and then only use the user_name from that string to compare.
For Example i only want to compare values
'John','Max' and 'Maria'
as they are unique.
My query will be like following :
SELECT 1 FROM REVIEW WHERE review_viewers = [Input from user]
The Input from user will always be a single string for example 'Max' or 'John' etc.
How can i accomplish it? I tried it several time but I am always hitting a dead end.
variable userinput varchar2(20);
execute :userinput := 'Max';
select review_id, review_name, review_reviewers,
regexp_substr(review_reviewers, '(^|:)(' || :userinput || '\s.+?)(:|$)', 1, 1, null, 2)
from review
where regexp_like(review_reviewers, '(^|:)' || :userinput || '\s');

Using Regex on a variable in a SQL function

I'm working on a SQL developer function that standardizes street suffixes in a table of addresses. Originally I had a series of replace statements with regex like this:
select replace (ADDRESS, '(^|\s)(AVENUE)($|\s)', ' AVE ') into ADDRESS from dual;
But to try to decrease the length of the function, I changed it to a cursor which looks like this:
DECLARE
CURSOR CHANGE_SUFFIX
IS
SELECT *
FROM SUFFIX_LIST;
BEGIN
FOR SUFFIX_LIST
IN CHANGE_SUFFIX
LOOP
select regexp_replace (ADDRESS, SUFFIX_LIST.ORIG_SUFFIX, SUFFIX_LIST.CHANGED_SUFFIX) into ADDRESS from dual;
END LOOP;
END;
Where I have a table (SUFFIX_LIST) with a column of what should be changed (ORIG_SUFFIX) and what it's supposed to change into (CHANGED_SUFFIX).
The issue I'm having is that I can't find a way to apply the original regex to the replace statement in the cursor. I tried including it like
select regexp_replace (ADDRESS, (^|\s)SUFFIX_LIST.ORIG_SUFFIX($|\s), SUFFIX_LIST.CHANGED_SUFFIX) into ADDRESS from dual
But that gives an error (missing expression) and putting it in single quotes treats it like a string. Any help appreciated, please lmk if there are any questions.
You need the anchors to be strings, but then you need to concatenate them with the table values:
select regexp_replace (ADDRESS, '(^|\s)' || SUFFIX_LIST.ORIG_SUFFIX || '($|\s)',
SUFFIX_LIST.CHANGED_SUFFIX) into ADDRESS from dual;
Or you could do that concatenation in the cursor query. As ADDRESS is a PL/SQL variable you also don't need to select from dual to modify that; you can just assign it:
address := regexp_replace (ADDRESS, '(^|\s)' || SUFFIX_LIST.ORIG_SUFFIX || '($|\s)',
SUFFIX_LIST.CHANGED_SUFFIX);
You can do the same concatenation to add the spaces back around the CHANGED_SUFFIX values if the table values don't have those. (Presumably you trim them off at the end...)

Procedure to insert data from one column into two columns in another table

I'm trying to get a procedure that will allow me to get data from a column and insert it into two different columns in a different table. the first table currently has both first and last name in a single column. I have another table with first name and last name in different columns and I need to separate and insert them from Column1/Table1 into the two columns in Table2 preferably using a procedure since I have a lot of names to migrate.
Column1(Name) in Table 1 looks like this
NAME
First_Name1 Last_name1
First_Name2 Last_Name2
First_Name3 Last_Name3
And I need the data to be separated like this in Table2 as FName/LName using the data from the first table:
F_Name | L_Name
First_Name1|Last_Name1
First_Name2|Last_Name2
First_Name3|Last_Name3
I figured out how to get the data from the last and the first name separated using SUBSTR and INSTR, but I can't figure out how to put this inside a Procedure, or how to Loop it since I want to use it for several rows.
select substr(staff.name, 0, instr(staff.name, ' ')-1) as Fname
from staff;
select substr(staff.name, instr(staff.name,' ')+1) as Lname
from Staff;
Any ideas/Help? Thanks guys.
Building a Looping PL/SQL Based DML Cursor For Multiple DML Targets
A PL/SQL Stored Procedure is a great way to accomplish your task. An alternate approach to breaking down your single name field into FIRST NAME and LAST NAME components could be to use an Oracle Regular Expression, as in:
SELECT REGEXP_SUBSTR('MYFIRST MYLAST','[^ ]+', 1, 1) from dual
-- Result: MYFIRST
SELECT REGEXP_SUBSTR('MYFIRST MYLAST','[^ ]+', 1, 2) from dual
-- Result: MYLAST
A procedure based approach is a good idea; first wrap this query into a cursor definition. Integrate the cursor within a complete PL/SQL stored procedure DDL script.
CREATE or REPLACE PROCEDURE PROC_MYNAME_IMPORT IS
-- Queries parsed name values from STAFF (the source) table
CURSOR name_cursor IS
SELECT REGEXP_SUBSTR(staff.name,...) as FirstName,
REGEXP_SUBSTR(... ) as LastName
FROM STAFF;
BEGIN
FOR i IN name_cursor LOOP
--DML Command 1:
INSERT INTO Table_One ( first_name, last_name )
VALUES (i.FirstName, i.LastName);
COMMIT;
--DML Command 2:
INSERT INTO Table_Two ...
COMMIT;
END LOOP;
END proc_myname_import;
As you can see from the example block, a long series of DML statements can take place (not just two) for a given cursor record and its values as it is handled by each loop iteration. Each field may be referenced by the name assigned to them within the cursor SQL statement. There is a '.' (dot) notation where the handle assigned to the cursor call is the prefix, as in:
CURSOR c1 IS
SELECT st.col1, st.col2, st.col3
FROM sample_table st
WHERE ...
Then the cursor call for looping through the main record set:
FOR my_personal_loop IN c1 LOOP
...do this
...do that
INSERT INTO some_other_table (column_one, column_two, column_three)
VALUES (my_personal_loop.col1, my_personal_loop.col2, ...);
COMMIT;
END LOOP;
... and so on.
This should work for you.
insert into newtable(FirstName, LastName)
select substr(staff.name, 0, instr(staff.name, ' ') - 1),
substr(staff.name, instr(staff.name, ' ') + 1)
from staff;

Calling Stored procedure with 'IS IN' statement in Oracle

I have following issue. I need to filter fetching data in stored procedure:
SELECT * FROM tab WHERE post_code IS IN ('pc1', 'pc2', 'pc3');
My question is: how to pass arguments pc1, pc2, pc3... into stored procedure?
As an array or as string?
When I try pass as string I have problem with apostrophes.
Passing array in my opinion is not good because of performance...I will need to create for loop and create string which will be passed like this:
SELECT * FROM tab WHERE post_code IS IN (post_codes);
How to do it right?
Not sure why you think passing an array would lead to a performance issue.
This will work:
declare
post_codes sys.dbms_debug_vc2coll := new sys.dbms_debug_vc2coll ('pc1', 'pc2', 'pc3');
lrec tab%rowtype;
begin
select * into lrec
where post_code in ( select * from table(post_codes));
end;
/
This is proof of concept only. It will hurl TOO_MANY_ROWS exception if the query returns more than one row. As you haven't provided any context for what you are trying to achieve I haven't bothered to invent anything exra.
The easiest way, in my opinion is to use SQL to split up your string:
with id_generator
as
(
SELECT regexp_substr(:txt, '[^,]+', 1, LEVEL) token
FROM dual
CONNECT BY LEVEL <= length(:txt) - length(REPLACE(:txt, ',', '')) + 1
)
select u.id, u.username
from users u, id_generator g
where u.id = g.token;
This query looks a little scary, but you can take the top part and run it in isolation to see what it does:
SELECT regexp_substr(:txt, '[^,]+', 1, LEVEL) token
FROM dual
CONNECT BY LEVEL <= length(:txt) - length(REPLACE(:txt, ',', '')) + 1
This generates a row per item in your comma separated list, which you can then join to the other table to get your results.
More discussion on this problem on my blog - http://betteratoracle.com/posts/20-how-do-i-bind-a-variable-in-list