Use replace to update column value from another column - sql

I have database that looks like this
CREATE TABLE code (
id SERIAL,
name VARCHAR(255) NOT NULL
);
INSERT INTO code (name) VALUES ('random_value1_random');
INSERT INTO code (name) VALUES ('random_value123_random');
CREATE TABLE value (
id SERIAL,
name VARCHAR(255) NOT NULL
);
INSERT INTO value (name) VALUES ('value1');
INSERT INTO value (name) VALUES ('value123');
UPDATE code SET name = REPLACE(name, SELECT name from value , '');
I want to update my table code to remove a portion of a code and that code is coming from another table. My goal is to update all values of code and remove the portion of the string that matches another value. My end goal is to make all code.name in the example look like: random_random removing the value from the value table.
When tried using to replace with a query I get an error:
[21000] ERROR: more than one row returned by a subquery used as an expression
What is a cleaner better way to do this?

You can use REGEXP_REPLACE to replace multiple substrings in a string. You can use STRING_AGG to get the search pattern from the single search values.
UPDATE code SET name =
REGEXP_REPLACE( name,
(SELECT '(' || STRING_AGG(name, '|') || ')' from value),
''
);
This will leave you with 'random___random', not 'random_random'. If you only want to look for substrings separated with the underline character, then use
UPDATE code SET name =
TRIM('_' FROM
REGEXP_REPLACE(name,
(SELECT '(' || STRING_AGG('_?' || name || '_?', '|') || ')' from value),
'_'
)
);
Demo: https://dbfiddle.uk/RrOel8Ns

This T-SQL (I don't have Postgres) and isn't elegant, but it works..
;with l as (
-- Match the longest value first
select c.id c_id, v.id v_id, ROW_NUMBER () over (partition by c.id order by len(v.name) desc) r
from code c
join value v on charindex (v.name, c.name) > 0)
, l1 as (
-- Select the longest value first
select c_id, v_id from l where r = 1
)
update c set name = REPLACE(c.name, v.name, '')
from l1
join code c on c.id = l1.c_id
join value v on v.id = l1.v_id

Related

Can SQL STRING_SPLIT use two (or more) separators?

I have a list of drug names that are stored in various upper and lower case combinations. I need to capitalize the first letter of each word in a string, excluding certain words. The string is separated by spaces, but can also be separated by a forward slash.
The following code works:
create table #exclusionlist (word varchar(25))
create table #drugnames (drugname varchar(50))
insert into #exclusionlist values ('ER')
insert into #exclusionlist values ('HCL')
insert into #drugnames values ('DRUGNAME ER')
insert into #drugnames values ('drugname hcl')
insert into #drugnames values ('ONEDRUG/OTHERDRUG')
select 'Product Name' = drugname
, 'Product Name 2' = STUFF((SELECT ' ' +
case when value in (select word from #exclusionlist) then upper(value)
else upper(left(value, 1)) + lower(substring(value, 2, len(value))) end
from STRING_SPLIT(drugname, ' ')
FOR XML PATH('')) ,1,1,'')
from #drugnames
The output looks like this:
Drugname ER
Drugname HCL
Onedrug/otherdrug
How can I get that last one to look like this:
Onedrug/Otherdrug
I did try STRING_SPLIT(replace(drugname, '/', ' '), ' ') but obviously replaces the slash with a space. And if the slash is at the end of the string like ONEDRUG/OTHERDRUG/ then the result looks like Onedrug Otherdrug
It's possible that the string may end in a forward slash due to the field only holding N number of characters. When data gets inserted into the table, only the first N characters of the drug name are inserted. If that Nth character is a slash, the string will end in a slash.
You can use a CASE expression for the separator parameter to the STRING_SPLIT function.
In the code below, a common table expression (CTE) uses STRING_SPLIT to split out all the drugname words and capitalize the first letter of each word as appropriate.
The CTE results are unioned together using STRING_AGG to join the drugnames back together. Note that separator parameter for STRING_AGG cannot be an expression. Using a CASE expression results in this error:
Msg 8733, Level 16, State 1, Line 14 Separator parameter for
STRING_AGG must be a string literal or variable.
For SQL Server, you would need to be on SQL 2017 or greater for the STRING_AGG function. (I added an id/identity column to the #drugnames temp table to assist with grouping.)
DROP TABLE IF EXISTS #exclusionlist;
DROP TABLE IF EXISTS #drugnames;
CREATE TABLE #exclusionlist (word VARCHAR(25))
CREATE TABLE #drugnames (id INT IDENTITY, drugname VARCHAR(50))
INSERT INTO #exclusionlist VALUES ('ER')
INSERT INTO #exclusionlist VALUES ('HCL')
INSERT INTO #drugnames VALUES ('DRUGNAME ER')
INSERT INTO #drugnames VALUES ('drugname hcl')
INSERT INTO #drugnames VALUES ('ONEDRUG/OTHERDRUG')
;WITH SomeStuff AS
(
select d.id, d.drugname,
sp.value AS SplitValue, el.word AS ExclusionListSpaceWord,
COALESCE(el.word,
UPPER(LEFT(sp.value, 1)) + LOWER(RIGHT(sp.value, LEN(sp.value) - 1))
) AS CapitalizedWord
from #drugnames d
CROSS APPLY STRING_SPLIT(d.drugname, CASE WHEN d.drugname LIKE '%/%' THEN '/' ELSE ' ' END ) sp
LEFT JOIN #exclusionlist el
ON el.word = sp.value
)
SELECT ss.id, STRING_AGG(ss.CapitalizedWord, '/') AS ReconstructedDrugname
FROM SomeStuff ss
WHERE ss.drugname LIKE '%/%'
GROUP BY ss.id
UNION
SELECT ss.id, STRING_AGG(ss.CapitalizedWord, ' ') AS ReconstructedDrugname
FROM SomeStuff ss
WHERE ss.drugname LIKE '% %'
GROUP BY ss.id
Output:
id ReconstructedDrugname
----------- ----------------------
1 Drugname ER
2 Drugname HCL
3 Onedrug/Otherdrug
If (hopefully) using SQL 2017+ you can somewhat compact the logic. first a single string split by using translate to create a common separator. THen apply the exclude word criteria forllowed by the upper-case criteria, then re-aggregate, noting which separator to use.
select *
from #drugnames
outer apply (
select case
when max(sep)=' ' then String_Agg(word,' ')
else String_Agg(word,'/') end NewName
from (
select
case
when exists (select * from #exclusionlist x where x.word = value)
then Upper(value)
else Stuff(Lower(value),1,1,Upper(Left(value,1)))
end word, Iif(drugname like '%/%','/',' ') sep
from String_Split(Translate(drugname,' /','**'),'*')
)w
)new;
Example DB<>Fiddle
Output:
Note - using string_split does not, according to the documentation, guarantee the ordering of the values. In practice, I've never seen this be the case and since you're already using the function I'm using here also. There's plenty of ways to split the string while retaining an ordering (using json for example) should it ever prove necessary.
If you are still on 2016 then the string_agg can just be replaced with the for xml implementation you're already using.
If the "/" is replaced to a " /", then the split can still happen on the space.
And the extra space can be removed afterwards.
SELECT
[Product Name] = d.drugname
, [Product Name 2] = ca.drugname2
FROM #drugnames d
CROSS APPLY (
SELECT
REPLACE(LTRIM(x.value('(./text())[1]','VARCHAR(MAX)')),' /','/') AS drugname2
FROM
(
SELECT ' '+
CASE
WHEN e.word IS NOT NULL THEN e.word
WHEN s.value LIKE '/%'
THEN STUFF(LOWER(s.value),1,2,UPPER(LEFT(s.value,2)))
ELSE STUFF(LOWER(s.value),1,1,UPPER(LEFT(s.value,1)))
END
FROM STRING_SPLIT(REPLACE(d.drugname,'/',' /'),' ') s
LEFT JOIN #exclusionlist e
ON e.word = s.value
FOR XML PATH(''), TYPE
) q(x)
) ca;
Product Name
Product Name 2
DRUGNAME ER
Drugname ER
drugname hcl
Drugname HCL
ONEDRUG/OTHERDRUG
Onedrug/Otherdrug
Test on db<>fiddle here

Passing regexp_replace matches into arguments

I've got a set of strings in a database which describe calculations like this:
"#id1# = #id2# + #id3#"
and a table with the ids like this:
ID Human_friendly_name
id1 Name1
id2 Name2
id3 Name3
I'd like to substitute the human-friendly names in for the #id# format, giving me a result of:
Name1 = Name2 + Name3
The calculations do not have a limit on how many variables they can include - some are in the hundreds
One potential way to do this would be to split the equation into multiple rows (using a recursive trim, for example), do a lookup for the names and then use LISTAGG to recombine the strings. But that seems overly complicated.
What I'd really like to do is use REGEXP_REPLACE to pass the matches into the argument for the replacement string, i.e.:
REGEXP_REPLACE('My calculation string',
'#\d+#',
(select max(name) from table where id = REGEX_MATCH)
)
I haven't been able to find any examples of passing the matched value into the replacement_string argument (although the SELECT part works). Can anyone tell me how to do this, or confirm that it's impossible?
Thought about it some more... Perhaps you meant something different?
Do you have a table with strings, like '#id1# = #id2# + #id3#', and you are looking for a query that will substitute 'Name1' in place of '#id1#', etc. - that is, the + sign in the string has NO meaning whatsoever, and you are simply wanting to do a string replacement based on a substitution table? So, for example, if you had another string '#id1# is better than a glass of #id2#' you would want the output 'Name1 is better than a glass of Name2'?
If so, you will need regular expressions AND a recursive process of some sort. Below I show how this can be done in Oracle versions 11.2 and higher (since I use the recursive subquery factoring introduced in 11.2).
Input tables:
Table: INPUT_STRINGS
Columns: INP_STR
INP_STR
------------------------------------------------------------
#id1# = #id2# + #id3# + #id1# / #id3#
Let #id2# be equal to #id4# - 5 + #id1##id2#
Table: HUMAN_READABLE
Columns: ID, HUMAN_READABLE_NAME
ID HUMAN_READABLE_NAME
-------------------- -----------------------------
id1 James Bond
id2 cat$5FISH
id3
id4 star
Query:
with t (input_str, temp_str, ct) as (
select inp_str, inp_str, regexp_count(inp_str, '#') from input_strings
union all
select input_str, regexp_replace(temp_str, '#[^#]*#',
(select human_readable_name from human_readable
where regexp_substr(temp_str, '#[^#]*#') = '#'||id||'#'), 1, 1), ct - 2
from t
where ct != 0
)
select t.input_str, temp_str as human_readable_str from t where ct = 0;
Output:
INPUT_STR HUMAN_READABLE_STR
-------------------------------------------- ------------------------------------------------------------
Let #id2# be equal to #id4# - 5 + #id1##id2# Let cat$5FISH be equal to star - 5 + James Bondcat$5FISH
#id1# = #id2# + #id3# + #id1# / #id3# James Bond = cat$5FISH + + James Bond /
Interesting problem. I think the issue is with when Oracle evaluates the backreferences in regexp_replace (so instead of sending the value of \1 you are actually sending the literal string '\1'). Anyway, here is a solution using SQL modeling (I like mathguy's answer too, this is just a different approach):
First, setup your ref table holding the id=>name translations:
create table my_ref
(
id varchar2(50) not null primary key,
name varchar2(50)
);
insert into my_ref (id, name) values ('id1','name1');
insert into my_ref (id, name) values ('id2','name2');
insert into my_ref (id, name) values ('id3','name3');
insert into my_ref (id, name) values ('id4','name4');
insert into my_ref (id, name) values ('id5','name5');
insert into my_ref (id, name) values ('id6','name6');
commit;
And the main table with a few example entries:
create table my_tab
(
formula varchar2(50)
);
insert into my_tab values ('#id1# = #id2# + #id3#');
insert into my_tab values ('#test# = some val #id4#');
commit;
Next, a basic function to translate a single id to name (lookup function):
create or replace function my_ref_fn(i_id in varchar2)
return varchar2
as
rv my_ref.name%type;
begin
begin
select
-- replace id with name, keeping spaces
regexp_replace(i_id, '( *)#(.+)#( *)', '\1' || name || '\3')
into rv
from my_ref
where id = ltrim(rtrim(trim(i_id), '#'),'#');
exception
when no_data_found then null;
end;
dbms_output.put_line('"' || i_id || '" => "' || rv || '"');
return rv;
end;
And to use it, we need to use SQL modeling:
select formula, new_val
from my_tab
MODEL
PARTITION BY (ROWNUM rn)
DIMENSION BY (0 dim)
MEASURES(formula, CAST('' AS VARCHAR2(255)) word, CAST('' AS VARCHAR(255)) new_val)
RULES ITERATE(99) UNTIL (word[0] IS NULL)
(word[0] = REGEXP_SUBSTR(formula[0], '( *)[^ ]+( *|$)', 1, ITERATION_NUMBER + 1)
, new_val[0] = new_val[0] || nvl(my_ref_fn(word[0]), word[0])
);
Which gives:
FORMULA;NEW_VAL
"#id1# = #id2# + #id3#";"name1 = name2 + name3"
"#test# = some val #id4#";"#test# = some val name4"

Select rows using in with comma-separated string parameter

I'm converting a stored procedure from MySql to SQL Server. The procedure has one input parameter nvarchar/varchar which is a comma-separated string, e.g.
'1,2,5,456,454,343,3464'
I need to write a query that will retrieve the relevant rows, in MySql I'm using FIND_IN_SET and I wonder what the equivalent is in SQL Server.
I also need to order the ids as in the string.
The original query is:
SELECT *
FROM table_name t
WHERE FIND_IN_SET(id,p_ids)
ORDER BY FIND_IN_SET(id,p_ids);
The equivalent is like for the where and then charindex() for the order by:
select *
from table_name t
where ','+p_ids+',' like '%,'+cast(id as varchar(255))+',%'
order by charindex(',' + cast(id as varchar(255)) + ',', ',' + p_ids + ',');
Well, you could use charindex() for both, but the like will work in most databases.
Note that I've added delimiters to the beginning and end of the string, so 464 will not accidentally match 3464.
You would need to write a FIND_IN_SET function as it does not exist. The closet mechanism I can think of to convert a delimited string into a joinable object would be a to create a table-valued function and use the result in a standard in statement. It would need to be similar to:
DECLARE #MyParam NVARCHAR(3000)
SET #MyParam='1,2,5,456,454,343,3464'
SELECT
*
FROM
MyTable
WHERE
MyTableID IN (SELECT ID FROM dbo.MySplitDelimitedString(#MyParam,','))
And you would need to create a MySplitDelimitedString type table-valued function that would split a string and return a TABLE (ID INT) object.
A set based solution that splits the id's into ints and join with the base table which will make use of index on the base table id. I assumed the id would be an int, otherwise just remove the cast.
declare #ids nvarchar(100) = N'1,2,5,456,454,343,3464';
with nums as ( -- Generate numbers
select top (len(#ids)) row_number() over (order by (select 0)) n
from sys.messages
)
, pos1 as ( -- Get comma positions
select c.ci
from nums n
cross apply (select charindex(',', #ids, n.n) as ci) c
group by c.ci
)
, pos2 as ( -- Distinct posistions plus start and end
select ci
from pos1
union select 0
union select len(#ids) + 1
)
, pos3 as ( -- add row number for join
select ci, row_number() over (order by ci) as r
from pos2
)
, ids as ( -- id's and row id for ordering
select cast(substring(#ids, p1.ci + 1, p2.ci - p1.ci - 1) as int) id, row_number() over (order by p1.ci) r
from pos3 p1
inner join pos3 p2 on p2.r = p1.r + 1
)
select *
from ids i
inner join table_name t on t.id = i.id
order by i.r;
You can also try this by using regex to get the input values from comma separated string :
select * from table_name where id in (
select regexp_substr(p_ids,'[^,]+', 1, level) from dual
connect by regexp_substr(p_ids, '[^,]+', 1, level) is not null );

Select statement that concatenates the first character after every '/' character in a column

So I am trying to write a query which, among other things, brings back the first character in a Varchar field, then returns the first character which appears after each / character throughout the rest of the field.
The field I am refrering too will contain a group of last names, separated by a '/'. For example: Fischer-Costello/Korbell/Morrison/Pearson
For the above example, I would want my select statement to return: FKMP.
So far, I have only been able to get my code to return the first character + the first character after the FIRST (and only the first) '/' character.
So for the above example input, my select statement would return: FK
Here is the code that I have written so far:
select rp.CONTACT_ID, ra.TRADE_REP, c.FIRST_NAME, c.LAST_NAME,
UPPER(LEFT(FIRST_NAME, 1)) + SUBSTRING(c.first_name,CHARINDEX('/',c.first_name)+1,1) as al_1,
UPPER(LEFT(LAST_NAME, 1)) + SUBSTRING(c.LAST_name,CHARINDEX('/',c.LAST_name)+1,1) as al_2
from dbo.REP_ALIAS ra
inner join dbo.REP_PROFILE rp on rp.CONTACT_ID = ra.CONTACT_ID
inner join dbo.CONTACT c on rp.CONTACT_ID = c.CONTACT_ID
where
rp.CRD_NUMBER is null and
ra.TRADE_REP like '%DNK%' and
(c.LAST_NAME like '%/%' or c.FIRST_NAME like '%/%') and
ra.TRADE_FIRM in
(
'xxxxxxx',
'xxxxxxx'
)
If you read the code, it's obvious that I am attempting to perform the same concatenation on the first_name column as well. However, I realize that a solution which will work for the Last_name column (used in my example), will also work for the first_name column.
Thank you.
Some default values
DECLARE #List VARCHAR(50) = 'Fischer-Costello/Korbell/Morrison/Pearson'
DECLARE #SplitOn CHAR(1) = '/'
This area just splits the string into a list
DECLARE #RtnValue table
(
Id int identity(1,1),
Value nvarchar(4000)
)
While (Charindex(#SplitOn, #List)>0)
Begin
Insert Into #RtnValue (value)
Select
Value = ltrim(rtrim(Substring(#List,1,Charindex(#SplitOn,#List)-1)))
Set #List = Substring(#List,Charindex(#SplitOn,#List)+len(#SplitOn+',')-1,len(#List))
End
Insert Into #RtnValue (Value)
Select Value = ltrim(rtrim(#List))
Now lets grab the first character of each name and stuff it back into a single variable
SELECT STUFF((SELECT SUBSTRING(VALUE,1,1) FROM #RtnValue FOR XML PATH('')),1,0,'') AS Value
Outputs:
Value
FKMP
Here is another way to do this would be a lot faster than looping. What you need is a set based splitter. Jeff Moden at sql server central has one that is awesome. Here is a link to the article. http://www.sqlservercentral.com/articles/Tally+Table/72993/
Now I know you have to signup for an account to view this but it is free and the logic in that article will change the way you look at data. You might also be able to find his code posted if you search for DelimitedSplit8K.
At any rate, here is how you could implement this type of splitter.
declare #Table table(ID int identity, SomeValue varchar(50))
insert #Table
select 'Fischer-Costello/Korbell/Morrison/Pearson'
select ID, STUFF((select '' + left(x.Item, 1)
from #Table t2
cross apply dbo.DelimitedSplit8K(SomeValue, '/') x
where t2.ID = t1.ID
for xml path('')), 1, 0 , '') as MyResult
from #Table t1
group by t1.ID

SQL How do I find values from a list that are not in a table

I have a table with values in a field called 'code'.
ABC
DFG
CDF
How would I select all codes that are not in the table from a list I have?
Eg:
SELECT * from [my list] where table1.code not in [my list]
the list is not in a table.
The list would be something like "ABC","BBB","TTT" (As strings)
Try this:
SELECT code
FROM Table1
WHERE code NOT IN ('ABC','CCC','DEF') --values from your list
It will result:
DFG
CDF
If the list is in another table, try this:
SELECT code
FROM Table1
WHERE code NOT IN (SELECT code FROM Table2)
As per your requirement, try this:
SELECT list
FROM Table2
WHERE list NOT IN (SELECT code from table1)
It will select the list values that are not in code.
See an example in SQL Fiddle
The question key point need to set "ABC","BBB","TTT" source data trun to a table.
that table will look like
|---+
|val|
|---+
|ABC|
|BBB|
|TTT|
Sqlite didn't support sqlite function. so that will be a little hard to sqlite your list to be a table.
You can use a CTE Recursive to make like sqlite function
You need to use replace function to remove " double quotes from your
source data.
There are two column in the CTE
val column carry your List data
rest column to remember current splite string
You will get a table from CTE like this.
|---+
|val|
|---+
|ABC|
|BBB|
|TTT|
Then you can compare the data with table1.
Not IN
WITH RECURSIVE split(val, rest) AS (
SELECT '', replace('"ABC","BBB","TTT"','"','') || ','
UNION ALL
SELECT
substr(rest, 0, instr(rest, ',')),
substr(rest, instr(rest, ',')+1)
FROM split
WHERE rest <> '')
SELECT * from (
SELECT val
FROM split
WHERE val <> ''
) t where t.val not IN (
select t1.code
from table1 t1
)
sqlfiddle:https://sqliteonline.com/#fiddle-5adeba5dfcc2fks5jgd7ernq
Outut Result:
+---+
|val|
+---+
|BBB|
|TTT|
If you want to show it in a line,use GROUP_CONCAT function.
WITH RECURSIVE split(val, rest) AS (
SELECT '', replace('"ABC","BBB","TTT"','"','') || ','
UNION ALL
SELECT
substr(rest, 0, instr(rest, ',')),
substr(rest, instr(rest, ',')+1)
FROM split
WHERE rest <> '')
SELECT GROUP_CONCAT(val,',') val from (
SELECT val
FROM split
WHERE val <> ''
) t where t.val not IN (
select t1.code
from table1 t1
)
Outut Result:
BBB, TTT
sqlfiddle:https://sqliteonline.com/#fiddle-5adecb92fcc36ks5jgda15yq
Note:That is unreasonable on SELECT * from [my list] where table1.code not in [my list],because This query has no place to find table1 so you couldn't get table1.code column
You can use not exists or JOIN to make your expect.
sqlfiddle:https://sqliteonline.com/#fiddle-5adeba5dfcc2fks5jgd7ernq
Can you use common table expressions?
WITH temp(code) AS (VALUES('ABC'),('BBB'),('TTT'),(ETC...))
SELECT temp.code FROM temp WHERE temp.code NOT IN
(SELECT DISTINCT table1.code FROM table1);
This would allow you to create a temporary table defined with your list of strings within the VALUES statement. Then use standard SQL to select values NOT IN your table1.code column.
Is this solution good, or am I missing something?
create table table10 (code varchar(20));
insert into table10 (code) values ('ABC');
insert into table10 (code) values ('DFG');
insert into table10 (code) values ('CDF');
select * from (
select 'ABC' as x
union all select 'BBB'
union all select 'TTT'
) t where t.x not in (select code from table10);
-- returns: BBB
-- TTT
See SQL Fiddle.
This can also be achieved using a stored procedure:
DELIMITER //
drop function if exists testcsv
//
create function testcsv(csv varchar(255)) returns varchar(255)
deterministic
begin
declare pos, found int default 0;
declare this, notin varchar(255);
declare continue handler for not found set found = 0;
set notin = '';
repeat
set pos = instr(csv, ',');
if (pos = 0) then
set this = trim('"' from csv);
set csv = '';
else
set this = trim('"' from trim(substring(csv, 1, pos-1)));
set csv = substring(csv, pos+1);
end if;
select 1 into found from table1 where code = this;
if (not found) then
if (notin = '') then
set notin = this;
else
set notin = concat(notin, ',', this);
end if;
end if;
until csv = ''
end repeat;
return (notin);
end
//
select testcsv('"ABC","BBB","TTT","DFG"')
Output:
BBB, TTT