Replace placeholder content in result set - sql-server

I have this table as a simple example, with food items:
Table: FoodItem
1 Burgers
2 French Fries
3 Pizzas
and I have another table with phrases like these:
Table: Phrase
1 I want {1} and {2}!
2 I just want {3}.
I want to create an sp that grabs all the phrases from the phrase table and replaces the placeholder parts with content from the food table, like this:
I want Burgers and French Fries!
I just want Pizzas.
How can I accomplish this? I already tried "like" and "patindex", but I'm unsure of whether these are even suited for this task.

For a little number of replacements and small amount of data you can use recursive CTE (I have seen bad performance when a lot of replacements are performed). Something like this:
Declare #Phrase table (ID int,Phrase varchar(100))
Insert into #Phrase values
(1,'I want {1} and {2}!')
,(2,'I just want {3}.')
,(3,'i just don not like {1} and {3}');
Declare #FoodItem table (ID int, MapTo varchar(100))
Insert Into #FoodItem values
(1 ,'Burgers')
,(2 ,'French Fries')
,(3 ,'Pizza');
With DataSource AS
(
SELECT ID
,Phrase
,1 as level
FROM #Phrase
UNION ALL
SELECT DS.[ID]
,cast(REPLACE(ds.Phrase, '{'+ CAST(DS.[Level] AS VARCHAR(8)) +'}', FI.[MapTo]) as varchar(100))
,level + 1 as level
FROM DataSource DS
INNER JOIN #FoodItem FI
ON DS.[level] = FI.[ID]
)
SELECT *
FROM DataSource
WHERE level = (SELECT max(id) from #FoodItem) + 1;
I am sure this can be improved further.
If you are going to work with huge amount of data it will be good to implement SQL CLR function for replacing multiple strings and concatenating strings.
So, for each row you will have something like this:
(1,'I want {1} and {2}!', '{1}|{2}','Burgers|French Fries')
,(2,'I just want {3}.', '{3}', 'Pizza')
,(3,'i just don not like {1} and {3}', '{1}|{3}','Burgers|Pizza');
Then your function with accept the three columns and perform the replace internally.

Example
Declare #Phrase table (ID int,Phrase varchar(100))
Insert into #Phrase values
(1,'I want {1} and {2}!')
,(2,'I just want {3}.')
Declare #FoodItem table (ID int, MapTo varchar(100))
Insert Into #FoodItem values
(1 ,'Burgers')
,(2 ,'French Fries')
,(3 ,'Pizza')
Select A.ID
,NewStr = replace(replace(B.S,' ||',''),'|| ','')
From #Phrase A
Cross Apply (
Select S = Stuff((Select ' ' +coalesce(MapTo,RetVal)
From (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(replace(replace(A.Phrase,'{','|| {'),'}','} ||'),' ','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B1
Left Join #FoodItem B2 on B1.RetVal = concat('{',B2.ID,'}')
Order by RetSeq
For XML Path ('')),1,1,'')
) B
Returns
ID NewStr
1 I want Burgers and French Fries!
2 I just want Pizza.
Edit - It may be more performat to create a UDF which does something like the
following
Declare #S varchar(max) = 'I want {1} and {2}!'
Select #S = replace(#S,concat('{',ID,'}'),MapTo)
From FoodItem
Select #S
Returns
I want Burgers and French Fries!

Related

Select rows with any member of list of substrings in string

In a Micrososft SQL Server table I have a column with a string.
Example:
'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3'
I also have a dynamic list of substrings
Example:
('icouldnt', 'stuff', 'banana')
I don't care for string manipulation. The substrings could also be called:
('%icouldnt%', '%stuff%', '%banana%')
What's the best way to find all rows where the string contains one of the substrings?
Solutions that are not possible:
multiple OR Statements in the WHERE clause, the list is dynamic
external Code to do a "for each", its a multi value parameter from the reportbuilder, so nothing useful here
changing the database, its the database of a tool a costumer is using and we can't change it, even if we would like... so much
I really cant believe how hard such a simple problem can turn out. It would need a "LIKE IN" command to do it in a way that looks ok. Right now I cant think of anything but a messy temp table.
One option is to use CHARINDEX
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3' )
;WITH cteX
AS(
SELECT 'icouldnt' Strings
UNION ALL
SELECT 'stuff'
UNION ALL
SELECT 'banana'
)
SELECT
T.*, X.Strings
FROM #tab T
CROSS APPLY (SELECT X.Strings FROM cteX X) X
WHERE CHARINDEX(X.Strings, T.Col1) > 1
Output
EDIT - using an unknown dynamic string variable - #substrings
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3' )
DECLARE #substrings NVARCHAR(200) = 'icouldnt,stuff,banana'
SELECT
T.*, X.Strings
FROM #tab T
CROSS APPLY
( --dynamically split the string
SELECT Strings = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#substrings, ',', '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
) X
WHERE CHARINDEX(X.Strings, T.Col1) > 1

MS SQL Split a column value using delimiters and update in other field

Ex, I have a table like this,
ID Name
1 Apple,banana
2 Grape,Orange
3 Papaya,Jackfruit
I need to split (,) and save like this in SQL
ID Name Name2
1 Apple banana
2 Grape Orange
3 Papaya Jackfruit
The fastest, most scaleable way to split strings before SQL Server 2016 is to write a SQLCLR method that splits strings, like this one. SQL Server 2016 introduced the STRING_SPLIT function which is even faster.
The second fastest way to split strings for versions before SQL Server 2016 is to convert the separated text into XML and use XML operators to retrieve the individual items. The typical usage, as shown in Aaron Bertrand's articles returns items as rows. It can be adapted easily to return items as columns:
declare #table table (ID int, Name nvarchar(200))
insert into #table
values
(1,'Apple,banana'),
(2,'Grape,Orange'),
(3,'Papaya,Jackfruit');
with items as (
select
ID,
xmlField= cast('<item><tag>'
+ replace(Name,',','</tag><tag>')
+ '</tag></item>' as xml)
from #table
)
-- Step 2: Select different tags and display them as fields
select
y.item.value('(tag/text())[1]','nvarchar(20)') As Name1,
y.item.value('(tag/text())[2]','nvarchar(20)') as Name2
from items outer apply xmlField.nodes('item') as y(item)
This returns :
1 Apple banana
2 Grape Orange
3 Papaya Jackfruit
This works by first converting Name1,Name2 to <item><tag>Name1</tag><tag>Name2</tag><item> which can be cast to XML and returned as xmlField.
outer apply xmlField.nodes('item') as y(item) converts this field to a table of items named y. Only one item row exists in each field.
Finally, y.item.value('(tag/text())[1]','nvarchar(20)') extracts the text of the first tag element as Name1.
This can be extended easily to multiple entries, or to return entries as different elements.
The number of columns has to be known in advance. SQL, the language, doesn't allow an arbitrary number of columns. If different fields contain a different number of tokens, they'll have to be returned as rows.
In this case, you should use STRING_SPLIT if you target SQL Server 2016 or the original version of the XML splitting technique :
CREATE FUNCTION dbo.SplitStrings_XML
(
#List nvarchar(max),
#Delimiter nvarchar(10)
)
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN (SELECT [value] = y.i.value('(./text())[1]', 'varchar(8000)')
FROM (SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i));
It's worth checking Performance Surprises and Assumptions : STRING_SPLIT() which compares all available string splitting techniques to find the fastest and most scaleable
We get the Result by using Lead and Lag Function along with Split String And insert the Result set in to your table as You Required
IF OBJECT_ID('tempdb..#InsertTable') IS NOT NULL
DROP TABLE #InsertTable
DECLARE #table TABLE (ID INT, Name VARCHAR(50))
CREATE TABLE #InsertTable (ID INT,Name1 VARCHAR(100),Name2 VARCHAR(100))
INSERT INTO #table
SELECT 1,'Apple,Banana' UNION ALL
SELECT 2,'Grape,Orange' UNION ALL
SELECT 3,'Papaya,Jackfruit'
INSERT INTO #InsertTable(ID,Name1,Name2)
SELECT DISTINCT ID,
ISNULL(Name1,LagName1) AS Name1 ,
ISNULL(Name2,LeadName2) AS Name2
FROM
(
SELECT ID,
Name1,
LAG(NAme1,1)OVER(ORDER BY ID) LagName1,
Name2,
LEAD(Name2,1)OVER(ORDER BY ID)LeadName2
FROM
(
SELECT ID, CASE WHEN Seq%2=1 THEN Name END AS Name1,
CASE WHEN Seq%2=0 THEN Name END AS Name2
FROM
(
SELECT Row_NUmber ()OVER(ORDER BY ID )AS Seq,ID, Split.a.value('.', 'VARCHAR(1000)') AS Name
FROM (
SELECT ID, CAST('<S>' + REPLACE(Name, ',', '</S><S>') + '</S>' AS XML) AS Name
FROM #table
) AS A
CROSS APPLY Name.nodes('/S') AS Split(a)
)Dt
)DT2
)Final
SELECT * FROM #InsertTable
Result
ID Name1 Name2
----------------------
1 Apple Banana
2 Grape Orange
3 Papaya Jackfruit

Get multiple rows using FOR JSON clause

Using PostgreSQL I can have multiple rows of json objects.
select (select ROW_TO_JSON(_) from (select c.name, c.age) as _) as jsonresult from employee as c
This gives me this result:
{"age":65,"name":"NAME"}
{"age":21,"name":"SURNAME"}
But in SqlServer when I use the FOR JSON AUTO clause it gives me an array of json objects instead of multiple rows.
select c.name, c.age from customer c FOR JSON AUTO
[{"age":65,"name":"NAME"},{"age":21,"name":"SURNAME"}]
How to get the same result format in SqlServer ?
By constructing separate JSON in each individual row:
SELECT (SELECT [age], [name] FOR JSON PATH, WITHOUT_ARRAY_WRAPPER)
FROM customer
There is an alternative form that doesn't require you to know the table structure (but likely has worse performance because it may generate a large intermediate JSON):
SELECT [value] FROM OPENJSON(
(SELECT * FROM customer FOR JSON PATH)
)
no structure better performance
SELECT c.id, jdata.*
FROM customer c
cross apply
(SELECT * FROM customer jc where jc.id = c.id FOR JSON PATH , WITHOUT_ARRAY_WRAPPER) jdata (jdata)
Same as Barak Yellin but more lazy:
1-Create this proc
CREATE PROC PRC_SELECT_JSON(#TBL VARCHAR(100), #COLS VARCHAR(1000)='D.*') AS BEGIN
EXEC('
SELECT X.O FROM ' + #TBL + ' D
CROSS APPLY (
SELECT ' + #COLS + '
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER
) X (O)
')
END
2-Can use either all columns or specific columns:
CREATE TABLE #TEST ( X INT, Y VARCHAR(10), Z DATE )
INSERT #TEST VALUES (123, 'TEST1', GETDATE())
INSERT #TEST VALUES (124, 'TEST2', GETDATE())
EXEC PRC_SELECT_JSON #TEST
EXEC PRC_SELECT_JSON #TEST, 'X, Y'
If you're using PHP add SET NOCOUNT ON; in the first row (why?).

Insert Substring Values into SQL Temp Table Using While Loop?

I have a table called accountNumbers. An example of values in the table are:
01-005-000-000-001-000
01-005-311-097-000
001-005-105-545
What I want to do is split the column (accountNum) at the dash, and then insert that value into a temp table, #test. When printing out #test, it should look like:
01
005
000
001
311
097
and so on. I cannot use store procedures or functions. I can get the first value, but any while loop I try just prints that first row over and over again.
WHILE ##ROWCOUNT > (select count(*) from dbo.accountNumbers
BEGIN
insert into #test (split, accountNum)
select SUBSTRING(accountNum, 1, CHARINDEX('-', accountNum) -1), accountNum
from dbo.accountNumbers
END
The restriction of no functions or procedures seems a little strange but you don't have to use a function to do this. A VERY minor tweak to the XML function found here http://sqlperformance.com/2012/07/t-sql-queries/split-strings can be utilized so you don't need a function to do this.
if OBJECT_ID('tempdb..#something') is not null
drop table #something
create table #something
(
AccountNumbers varchar(100)
)
insert #something
select '01-005-000-000-001-000' union all
select '01-005-311-097-000' union all
select '001-005-105-545'
select *
from #something s
cross apply
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(s.AccountNumbers, '-', '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
)MySplit

How to select delimited texts separately and completely SQL Server table column

I have a SQL Server table with a column p_author containing semi-colon (;) delimited text value. I used this query to split and select the respective id's from another table but it only splits once whereas I need all the value id's after splitting either p_author contains one value, two values, three values or whatever number of values. Under is the used query for splitting and selecting respective id's from another table.
select aid as [CountedID]
from sub_aminer_author
where name like (select RIGHT(p_author, LEN(p_author) - CHARINDEX(';', p_author))
from sub_aminer_paper
where pid = 4)
Sample data is shown here in this image.
#DarkKnight--This is my output in SqlServer2014
Try this..
DECLARE #X XML
DECLARE #STR VARCHAR(MAX)=''
Select #STR = #STR+';'+P_AUTHOR
From sub_aminer_paper
WHERE PID = 4
ORDER BY PID
select #STR = substring(#STR,2,len(#STR))
SELECT #X = CONVERT(xml,' <root> <s>' + REPLACE(#STR,';','</s> <s>') + '</s> </root> ')
select aid as [CountedID], name
from sub_aminer_author s
inner join (
SELECT row_number() over(order by (select null)) as rn, T.c.value('.','varchar(max)') as value
FROM #X.nodes('/root/s') T(c)) t on t.value = s.name
order by rn
Example fiddle : http://sqlfiddle.com/#!3/34b6c/10
As it has been said in the comments, you really need to remove this bad design.
It will only lead to bigger problems and performance issues. (I know that sometime you have to deal will terrible design for a little while)
In the meantime, if you really need to split your rows, this type of query with a recursive CTE (SQL Fiddle)can be used:
create table sub_aminer_author(pid int, p_author varchar(max), name varchar(max));
go
Insert into sub_aminer_author(pid, p_author, name) values
(1, 'AAAA;BBBBB;CCCCC', 'AAAA'), (2, 'DDDDD;EEEEE;FFFF;GGGGGGGG', 'GGGGGGGG'), (3, 'HHH', 'GGGGGGGG');
go
with split(pid, first, pos) as(
Select pid, cast(1 as bigint)
, Case When CHARINDEX(';', p_author) > 0
Then CHARINDEX(';', p_author)-1 Else len(p_author) End
From sub_aminer_author
Union All
Select d.pid, s.pos+2
, Case When CHARINDEX(';', d.p_author, s.pos+2) > 0 Then CHARINDEX(';', d.p_author, s.pos+2)-1 Else len(p_author) End
From split s
Inner Join sub_aminer_author d on d.pid = s.pid
Where s.pos < len(d.p_author)-1
)
Select d.pid, s.first, s.pos , SUBSTRING(d.p_author, s.first, s.pos - s.first +1)
From split s
Inner Join sub_aminer_author d on d.pid = s.pid
order by d.pid, s.first
You have to understand that it is neither good nor efficient.
It should be ok as long as you only temporary use it to fix your current design issue and move splited data to a better design.

Resources