SQL splitting a word in separate characters - sql

I need to change an application and the first thing I need is to change a field in a database table.
In this table I now have 1 to 6 single characters, i.e. 'abcdef'
I need to change this to '[a][b][c][d][e][f]'
[edit] It is meant to stay in the same field. So before field = 'abcdef' and after field = '[a][b][c][d][e][f]'.
What would be a good way to do this?
rg.
Eric

You can split string to separate characters using following function:
create function ftStringCharacters
(
#str varchar(100)
)
returns table as
return
with v1(N) as (
select 1 union all select 1 union all select 1 union all select 1 union all select 1
union all
select 1 union all select 1 union all select 1 union all select 1 union all select 1
),
v2(N) as (select 1 from v1 a, v1 b),
v3(N) as (select top (isnull(datalength(#str), 0)) row_number() over (order by ##spid) from v2)
select N, substring(#str, N, 1) as C
from v3
GO
And then apply it as:
update t
set t.FieldName = p.FieldModified
from TableName t
cross apply (
select (select quotename(s.C)
from ftStringCharacters(t.FieldName) s
order by s.N
for xml path(''), type).value('text()[1]', 'varchar(20)')
) p(FieldModified)
SQLFiddle sample

DECLARE #text NVARCHAR(50)
SET #text = 'abcdef'
DECLARE #texttable TABLE (value NVARCHAR(1))
WHILE (len(#text) > 0)
BEGIN
INSERT INTO #texttable
SELECT substring(#text, 1, 1)
SET #text = stuff(#text, 1, 1, '')
END
select * from #texttable

Without using a function:
declare #t table(C varchar(18))
insert #t values('abc'), ('1234'), (' 1234a')
;with CTE as
(
select C, '[' + substring(c, a.n, 1) + ']' v, rn from
(select 1 n union all
select 2 union all
select 3 union all
select 4 union all
select 5 union all
select 6) a
cross apply
(select c, row_number() over (order by C) rn from #t group by c) b
where a.n <= len(C)
)
update t3
set C = t4.[value]
FROM #t t3
JOIN
(
select C,
(
select v
from CTE t1
where t1.rn = t2.rn
for xml path(''), type
).value('.', 'varchar(18)') [value]
from CTE t2
group by t2.rn, C
) t4
ON t3.C = t4.C
SELECT * FROM #t

Related

T-SQL query to check if given set of data is available are not

I have a SQL Server table and my java application is sending a list of descriptions.
Now I need to verify if all the description are available in the table or not, if all the description is not available in the table then an error has to be raised with the missing description.
For example: Java application is sending message as 'tree', 'flower', 'plant'.
In SQL Server, there is a column description - I need to check if 'tree', 'flower', 'plant' are available or not.
If anyone is unavailable like 'plant' is not there in the table then raise an error that 'plant' is unavailable.
Could you please help me with this?
You can try to left join the table to the descriptions. If there is no match in the table, the columns from that table remain NULL, so you can filter only for NULLs. Like that you have a list of descriptions, that don't exist.
SELECT i.description
FROM (SELECT 'tree'
UNION ALL
SELECT 'flower'
UNION ALL
SELECT 'plant') i
LEFT JOIN elbat t
ON t.description = i.description
WHERE t.description IS NULL;
u meant somethin like this?
if not exists (select 1 from table1 where description='plant')
begin
RAISERROR('your custom error',16,1)
end
ALTER FUNCTION [dbo].[SplitToItems] (
#pString NVARCHAR(3999), --!! DO NOT USE MAX DATA-TYPES
#pDelimiter CHAR(1)
)
RETURNS #Items TABLE (ItemNumber INT, Item NVARCHAR(100))
BEGIN
IF Replace(#pString, '''', '') = ''
SET #pString = '';
WITH E1 (N)
AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), E2 (N)
AS (
SELECT 1
FROM E1 a, E1 b
), E4 (N)
AS (
SELECT 1
FROM E2 a, E2 b
), cteTally (N)
AS (
SELECT TOP (ISNULL(DATALENGTH(#pString), 0)) ROW_NUMBER() OVER (
ORDER BY (
SELECT NULL
)
)
FROM E4
), cteStart (N1)
AS (
SELECT 1
UNION ALL
SELECT t.N + 1
FROM cteTally t
WHERE SUBSTRING(#pString, t.N, 1) = #pDelimiter
), cteLen (N1, L1)
AS (
SELECT s.N1, ISNULL(NULLIF(CHARINDEX(#pDelimiter, #pString, s.N1), 0) - s.N1, 8000)
FROM cteStart s
)
INSERT INTO #Items
SELECT ItemNumber = ROW_NUMBER() OVER (
ORDER BY l.N1
), Item = SUBSTRING(SUBSTRING(#pString, l.N1, l.L1), 1, 100)
FROM cteLen l
RETURN
END
SELECT *
FROM dbo.SplitToItems('''tree'',''flower'',''plant''', ',')

SQL String join to table

Given few strings as
SET #Codes1 = 3,4
SET #Codes2 = 1
SET #Codes3 = --empty
Table -- TblCode
Id Code
1 A
2 B
3 C
4 D
How to convert the #Codes1, #Codes2, #Codes3 with join to the table TblCode so it returns the following output :
1. #Codes1 = CD
2. #Codes2 = A
3. #Codes3 = --empty
Note that the concatenation for the output is without the comma.
PS - This is a small example to a much larger and complex data set. Kindly ignore any wrongful design pattern here.
You can try this. I added the answer just for #Codes1, but it works with #Codes2 and #Codes3 too.
DECLARE #TblCode TABLE (Id INT, Code VARCHAR(2))
INSERT INTO #TblCode
VALUES(1, 'A'),
(2,'B'),
(3,'C'),
(4,'D')
DECLARE #Codes1 VARCHAR(10) = '3,4'
DECLARE #Codes2 VARCHAR(10) = '1'
DECLARE #Codes3 VARCHAR(10) = NULL
DECLARE #CodesOut VARCHAR(10) = ''
;WITH CTE_1 AS (
SELECT CODE= #Codes1 + ','
)
, CTE_2 AS -- It silit text to rows
(
SELECT RIGHT(CTE_1.CODE, LEN(CTE_1.CODE) - CHARINDEX(',',CTE_1.CODE)) CODE , SUBSTRING(CTE_1.CODE, 0, CHARINDEX(',',CTE_1.CODE)) ID, CHARINDEX(',',CTE_1.CODE) AS CI
FROM CTE_1
UNION ALL
SELECT RIGHT(CTE_2.CODE, LEN(CTE_2.CODE) - CHARINDEX(',',CTE_2.CODE)) CODE , SUBSTRING(CTE_2.CODE, 0, CHARINDEX(',',CTE_2.CODE)) ID, CHARINDEX(',',CTE_2.CODE) AS CI
FROM CTE_2 WHERE LEN(CTE_2.CODE) > 0
)
SELECT #CodesOut = #CodesOut + C.Code FROM CTE_2 INNER JOIN #TblCode C ON CTE_2.ID = C.Id
SELECT #CodesOut
Result:
CD
You can use a recursive CTE. Here is one method:
with c as (
select c.*, row_number() over (partition by id) as seqnum
from c
),
cte as (
select cast(#codes as varchar(max)) as str,
replace(#codes, id, code) as newstr,
1 as lev
from c
where seqnum = 1
union all
select str, replace(newstr, id, code), lev + 1
from cte join
c
on c.seqnum = cte.lev + 1
)
select top (1) newstr
from cte
order by lev desc;
If there is an error in the syntax, set up a SQL Fiddle or Rextester or something similar so it can be fixed.

SQL split-string as (key-identity,value)

I've added a function to my DB that splits a comma separated string into separate rows.
Now in my string I have: 1,55,2,56,3,57,etc... where (1) is the rowID and (55) the value I want to enter into row 1 of my table.
How can I modify this function to pull the 1st,3rd,5th,etc... values and 2nd,4th,6th,etc... values into two different columns?
CREATE FUNCTION dbo.SplitStringToValues
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
WITH E1(N) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
E2(N) AS (SELECT 1 FROM E1 a, E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b),
E42(N) AS (SELECT 1 FROM E4 a, E2 b),
cteTally(N) AS (SELECT 0 UNION ALL SELECT TOP (DATALENGTH(ISNULL(#List,1)))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E42),
cteStart(N1) AS (SELECT t.N+1 FROM cteTally t
WHERE (SUBSTRING(#List,t.N,1) = #Delimiter OR t.N = 0))
SELECT Item = SUBSTRING(#List, s.N1, ISNULL(NULLIF(CHARINDEX(#Delimiter,#List,s.N1),0)-s.N1,8000))
FROM cteStart s;
go
-------------- Update
Thanks everyone for your examples. I'm going to try out each of these until I get something working. I will accept once i figure out which on I can make work.
Thank you,
Alexp
An attempt to help with batch script; please try it out:
DECLARE #List NVARCHAR(MAX) = '1,55,2,56,3,57,10,65,11,88';
DECLARE #Delimiter NVARCHAR(255) = ',';
DECLARE #ListDataTable TABLE
(
ID INT IDENTITY (1, 1)
,DataKey INT
,DataValue INT
)
INSERT INTO #ListDataTable (DataKey, DataValue)
SELECT
value
,LEAD(value, 1, 0) OVER(ORDER BY (SELECT 1))
FROM STRING_SPLIT(#List, #Delimiter) WHERE RTRIM(value) <> '';
-- To get odd key values
SELECT * FROM
(
SELECT DataKey, DataValue FROM #ListDataTable WHERE ID % 2 = 1
) Temp WHERE DataKey % 2 = 1;
-- To get even key values
SELECT * FROM
(
SELECT DataKey, DataValue FROM #ListDataTable WHERE ID % 2 = 1
) Temp WHERE DataKey % 2 = 0;
Modify your function to return two columns: the position and the value. This is easy enough and keeps the function general purpose. Just change the select to:
SELECT Item = SUBSTRING(#List, s.N1, ISNULL(NULLIF(CHARINDEX(#Delimiter, #List, s.N1), 0) - s.N1, 8000)),
ItemNum = row_number() over (order by s.N1)
FROM cteStart s;
Then you can use to get the information you want. Here is one method:
select max(case when ItemNum % 2 = 1 then Item end) as rownum,
max(case when ItemNum % 2 = 0 then Item end) as value
from dbo.SplitStringToValues('1,55,2,56,3,57', ',')
group by (ItemNum - 1) / 2
#Macwise was on to something with LEAD - you could do this:
SELECT rownum = item, value
FROM
(
SELECT itemnumber, item, value = LEAD(item,1) OVER (ORDER BY itemnumber)
FROM dbo.SplitStringToValues('1,44,2,55,3,456,4,123,5,0', ',')
) split
WHERE 1 = itemnumber%2;
Gordon's solution is the best, most elegant pre-2012 solution. Here's another pre-2012 solution that does not require a sort in the execution plan:
SELECT rownum = s1.Item, value = s2.Item
FROM DelimitedSplit8K(#string, ',') s1
INNER MERGE JOIN SplitStringToValues('1,44,2,55,3,456,4,123,5,0', ',') s2
ON 1 = s1.itemNumber % 2 AND s1.ItemNumber = s2.ItemNumber-1;
Instead of changing that function, to get the next row's value next to the id use the LEAD function introduced in SQL SERVER 2012:
SELECT Id, Value
FROM (SELECT
ROW_NUMBER() over (order by(select 1)) as cnt,
t.item AS Id,
Lead(t.item)
OVER (
ORDER BY (SELECT 1)) Value
FROM dbo.Splitstringtovalues('10,20,30,40,50,10,20,30,40,50,60,70', ',')
t)
keyValue
WHERE keyValue.value IS NOT NULL
and cnt % 2 = 1

SQL replace from list

I'm trying to figure our how I can replace a string using data from another table
I have a table that looks like this:
Id Translation
1 Peter
2 Sandra
3 Olga
Now I want to select all and replace the translations using a list that looks like this:
Original New
e #
r ?
lg *%
So that the select list looks like this:
Id Translation
1 P#t#?
2 Sand?a
3 O*%a
So, for each translation, I need to have a REPLACE(Translation,Original,New).
Or in other words: I need to go through every "Translation" in my first list and make another loop in my replacement table to see what to replace
Bare in mind that the first list has 25'000 rows and the second has 50'000, so I can't just type it by hand :)
EDIT
Just to clarify:
The Original and New from my look up table can be both letters and words so the table can looks like this:
Original New
one two
three fifty
sun moon
To do this in one query, you need to use a recursive CTE. Something like:
with trans as (
select t.original, t.new, row_number() over (order by t.original) as seqnum,
count(*) over () as cnt
from translations
),
t as (
select tt.id, tt.string, replace(tt.string, trans.original, trans.new) as replaced,
seqnum + 1 as seqnum, cnt
from totranslate tt join
trans
on trans.id = 1
union all
select t.id, t.string, replace(t.string, trans.original, trans.new),
seqnum + 1 as seqnum, cnt
from t join
trans
on t.seqnum = trans.id
where t.seqnum <= t.cnt
)
select t.id, t.string, t.replaced
from t
where seqnum = cnt;
You can use a UDF:
CREATE FUNCTION [dbo].[Translate]
(
-- Add the parameters for the function here
#Str nvarchar(max)
)
RETURNS nvarchar(max)
AS
BEGIN
DECLARE #Result nvarchar(max) = #Str;
SELECT #Result = replace(#Result,Original,New) from dbo.Mappings order BY Pos;
RETURN #Result;
END
Here I assumed the table containing translations is called dbo.Mappings and beside the Original and New columns you need another column Pos int which will be used to determine the order in which the translations are applied (to address the problems mentioned by #Thorsten Kettner in comments)
Also with recursive cte:
DECLARE #translations TABLE
(
Id INT ,
Translation NVARCHAR(20)
)
INSERT INTO #translations
VALUES ( 1, 'Peter' ),
( 2, 'Sandra' ),
( 3, 'Olga' )
DECLARE #replacements TABLE
(
Original VARCHAR(2) ,
New VARCHAR(2)
)
INSERT INTO #replacements
VALUES ( 'e', '#' ),
( 'r', '?' ),
( 'lg', '*%' );
WITH cte1 AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY id ORDER BY (SELECT 1)) rn
FROM #translations CROSS JOIN #replacements),
cte2 AS (SELECT Id, rn, REPLACE(Translation, Original, New) AS NTranslation
FROM cte1
WHERE rn = 1
UNION ALL
SELECT c2.Id, c2.rn + 1, REPLACE(c2.NTranslation, c1.Original, c1.New)
FROM cte1 c1
JOIN cte2 c2 ON c2.Id = c1.Id AND c2.rn + 1 = c1.rn)
SELECT * FROM cte2
WHERE rn = (SELECT COUNT(*) FROM #replacements)
ORDER BY Id
EDIT:
WITH cte1 AS (SELECT t.*, p.Id AS Old, p.Code, ROW_NUMBER() OVER (PARTITION BY t.id ORDER BY (SELECT 1)) rn
FROM translations t CROSS JOIN Property p),
cte2 AS (SELECT Id, rn, REPLACE(Trans, Old, Code) AS NTranslation
FROM cte1
WHERE rn = 1
UNION ALL
SELECT c2.Id, c2.rn + 1, REPLACE(c2.NTranslation, c1.Old, c1.Code)
FROM cte1 c1
JOIN cte2 c2 ON c2.Id = c1.Id AND c2.rn + 1 = c1.rn)
SELECT * FROM cte2
WHERE rn = (SELECT COUNT(*) FROM Property)
ORDER BY Id
Here is something I worked out that will allow you to replace multiple characters with one specified string.
[Split2] is stolen from https://blogs.msdn.microsoft.com/amitjet/2009/12/11/convert-comma-separated-string-to-table-4-different-approaches/
USE <Your Database>
GO
CREATE FUNCTION [dbo].[Split2]
(
#strString varchar(4000)
)
RETURNS #Result TABLE
(
RID INT IDENTITY(0,1) Primary Key
,Value varchar(4000)
)
AS
BEGIN
WITH StrCTE(start, stop) AS
(
SELECT 1, CHARINDEX(',' , #strString )
UNION ALL
SELECT stop + 1, CHARINDEX(',' ,#strString , stop + 1)
FROM StrCTE
WHERE stop > 0
)
INSERT INTO #Result
SELECT SUBSTRING(#strString , start, CASE WHEN stop > 0 THEN stop - start ELSE 4000 END) AS stringValue
FROM StrCTE
RETURN
END
GO
USE <Your Database>
GO
CREATE FUNCTION [dbo].[MultiReplace]
(
#MyString varchar(MAX)
,#RepChars varchar(4000)
,#NewChars varchar(4000)
)
RETURNS varchar(MAX)
AS
BEGIN
DECLARE #CurRow int = 0
DECLARE #MaxRow int
SELECT #MaxRow = MAX(RID)
FROM dbo.split2 ( #RepChars )
WHILE #CurRow <= #MaxRow
BEGIN
SELECT #MyString = REPLACE(#MyString,VALUE,#NewChars)
FROM dbo.split2 ( #RepChars )
WHERE RID = #CurRow
SET #CurRow = #CurRow + 1
END
RETURN (#MyString);
END
GO
In this example I replace each character with no space
SELECT [dbo].[MultiReplace]('6th month 2016-06 (test / requested)',',1st,2nd,3rd,4th,5th,6th,0,1,2,3,4,5,6,7,8,9,(,),/,-,+, ','')
Result:
monthtestrequested
I hope this is useful for you.

How to compare string type columns character by character without looping?

I have two columns: One column holds actual answers, the other one is the answer_key.
I want to compare answers to answer_key and have scores in the third column:
ID Answers Answer_key Score
1 ABCD ABCC 1110
2 ACD DCA 010
Of course, I can check the length, loop through each character to compare them individually, and get the score.
However, is there an alternative? Possibly based on XML path?
You might try binary values rather than letters.
A=0001 B=0010 C=0100 D=1000
ABCD = 0001001001001000 (0x1248)
ABCC = 0001001001000100 (0x1244)
Score = (Answers XOR Answer_key) XOR 11111111
The XOR 11111111 is optional
What you want to do is to split each char in Answers and Answers_Key into separate rows and then compare them. This can be done using a Recursive CTE. The concatenation is done using the FOR XML PATH function.
CREATE TABLE temp(
Answers VARCHAR(10),
Answer_Key VARCHAR(10)
)
INSERT INTO temp VALUES ('ABCD', 'ABCC'), ('ACD', 'DCA');
;WITH temp_numbered AS(
SELECT
ID = ROW_NUMBER() OVER(ORDER BY Answer_Key),
*
FROM temp
),
cte AS(
SELECT
ID,
Answer_Key_Char = SUBSTRING(Answer_Key, 1, 1),
Answer_Key = STUFF(Answer_Key, 1, 1, ''),
Answers_Char = SUBSTRING(Answers, 1, 1),
Answers = STUFF(Answers, 1, 1, ''),
RowID = 1
FROM temp_numbered t
UNION ALL
SELECT
ID,
Answer_Key_Char = SUBSTRING(Answer_Key, 1, 1),
Answers = STUFF(Answer_Key, 1, 1, ''),
Answers_Char = SUBSTRING(Answers, 1, 1),
Answers = STUFF(Answers, 1, 1, ''),
RowID = RowID + 1
FROM cte
WHERE LEN(Answer_Key) > 0
)
SELECT
Answers,
Answer_Key,
Score = (SELECT
CASE WHEN Answer_Key_Char = Answers_Char THEN '1' ELSE '0' END
FROM cte
WHERE ID = t.ID
ORDER BY ID, RowID
FOR XML PATH(''))
FROM temp_numbered t
DROP TABLE temp
Here is another way using a Tally Table:
;WITH tally(N) AS(
SELECT TOP 11000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM sys.columns
)
,temp_numbered AS(
SELECT
ID = ROW_NUMBER() OVER(ORDER BY Answer_Key),
*
FROM temp
)
,cte AS(
SELECT
ID,
Answer_Key_Char = SUBSTRING(Answer_Key, N, 1),
Answers_Char = SUBSTRING(Answers, N, 1),
RowID = N
FROM temp_numbered tn
CROSS JOIN Tally t
WHERE t.N <= LEN(tn.Answer_Key)
)
SELECT
Answers,
Answer_Key,
Score = (SELECT
CASE WHEN Answer_Key_Char = Answers_Char THEN '1' ELSE '0' END
FROM cte
WHERE ID = t.ID
ORDER BY ID, RowID
FOR XML PATH(''))
FROM temp_numbered t
I seems easiest to loop through each character for the whole set at once:
-- get max Answer length
declare #len int,#max_len int
select #max_len = max(len(Answers)),
#len = 1
from Answers
-- update scores
while #len <= #max_len
begin
update Answers
set Score = isnull(Score,'') + '1'
where substring(Answers,#len,1) = substring(Answer_Key,#len,1)
and len(Answers) >= #len
update Answers
set Score = isnull(Score,'') + '0'
where substring(Answers,#len,1) != substring(Answer_Key,#len,1)
and len(Answers) >= #len
set #len = #len + 1
end
-- return Scores
select * from Answers
SQL FIDDLE
Expanding on the answer from #weswesthemenace to get around cte limit.
DECLARE #Answers TABLE
(
Id INT IDENTITY(1, 1) not null,
Answers VARCHAR(MAX) not null,
Answer_Key VARCHAR(MAX) not null
)
INSERT INTO #Answers (Answers, Answer_Key) VALUES ('ABCD', 'ABCC')
INSERT INTO #Answers (Answers, Answer_Key) VALUES ('ACD', 'DCA')
INSERT INTO #Answers (Answers, Answer_Key) VALUES ('ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXYZ', 'ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEGGHIJKLMNOPQRSTUVXXYZABCDEFGHIIKKMNOPQRSTUVWXYZABCDEFGHIJKLMNOPQRSTUVWXZZ');
WITH
E01(N) AS (SELECT 1 UNION ALL SELECT 1),
E02(N) AS (SELECT 1 FROM E01 a CROSS JOIN E01 b),
E04(N) AS (SELECT 1 FROM E02 a CROSS JOIN E02 b),
E08(N) AS (SELECT 1 FROM E04 a CROSS JOIN E04 b),
E16(N) AS (SELECT 1 FROM E08 a CROSS JOIN E08 b),
E32(N) AS (SELECT 1 FROM E16 a CROSS JOIN E16 b),
cteTally(N) AS (SELECT row_number() OVER (ORDER BY N) FROM E32)
SELECT b.Answers, b.Answer_Key,
(
SELECT CASE when SUBSTRING(a.Answer_Key, n.N, 1) = SUBSTRING(a.Answers, n.N, 1) then '1' else '0' end
FROM #Answers a
CROSS APPLY cteTally n
WHERE b.Id = a.Id AND n.N <= DATALENGTH(b.Answers)
ORDER BY ID, n.N
FOR XML PATH('')
) Score
FROM #Answers b
this can be simplified by a utility Number function in the database. Mine is called dbo.Number(start, end)
SELECT b.Answers, b.Answer_Key,
(
SELECT CASE WHEN SUBSTRING(a.Answer_Key, n.N, 1) = SUBSTRING(a.Answers, n.N, 1) THEN '1' ELSE '0' END
FROM #Answers a
CROSS APPLY dbo.Number(1, DATALENGTH(b.Answers)) n
WHERE b.Id = a.Id
ORDER BY ID, n.N
FOR XML PATH('')
) Score
FROM #Answers b