T-Sql query to clean up varchar column - sql-server-2005

I have a varchar column in one of my tables with data like:
1234abc
1234abcde456757
1234abc Supervisor
1234abc456 Administrator
I want to "clean it" by removing any letters and numbers immediately following them so for the above examples I want to have:
1234
1234
1234 Supervisor
1234 Administrator
In another word, I want to keep the initial number and the last word. I'm using the SUBSTRING and CHARINDEX but those functions remove everything till the end of the string and I don't know the length of the part I need to remove.
Any suggestions?
Thanks

You could search for the first non-digit and the first space in a subquery. That also works if the number of digits isn't exactly four:
declare #t table (col1 varchar(50))
insert into #t select '12abc'
union all select '1234abcde456757'
union all select '1234abc Supervisor'
union all select '1234abc456 Administrator'
union all select '123456abc456 Administrator'
select case when FirstNonDigit = 0 then col1
when FirstSpace = 0 then substring(col1, 1, FirstNonDigit-1)
else substring(col1, 1, FirstNonDigit-1) +
substring(col1, FirstSpace, len(col1) - FirstSpace + 1)
end
from (
select patindex('%[^0-9]%', col1) FirstNonDigit
, patindex('% %', col1) FirstSpace
, col1
from #t
) subqueryalias
-->
12
1234
1234 Supervisor
1234 Administrator
123456 Administrator

try this:
DECLARE #YourTable table (RowValue varchar(50))
INSERT #YourTable VALUES ('1234abc')
INSERT #YourTable VALUES ('1234abcde456757')
INSERT #YourTable VALUES ('1234abc Supervisor')
INSERT #YourTable VALUES ('1234abc456 Administrator')
UPDATE #YourTable
SET RowValue=LEFT(RowValue,4)+RIGHT(RowValue,CHARINDEX(' ',REVERSE(RowValue)))
FROM #YourTable
SELECT * FROM #YourTable
OUTPUT:
RowValue
--------------------------------------------------
1234
1234
1234 Supervisor
1234 Administrator
(4 row(s) affected)
EDIT: set based any number of digits and handles no digits or no words
DECLARE #YourTable table (RowValue varchar(50))
set nocount on
INSERT #YourTable VALUES ('13')
INSERT #YourTable VALUES ('1234abc')
INSERT #YourTable VALUES ('1234abc')
INSERT #YourTable VALUES ('1234abcde456757')
INSERT #YourTable VALUES ('1234abc Supervisor')
INSERT #YourTable VALUES ('1234abc456 Administrator')
INSERT #YourTable VALUES ('1234567abc456 Administrator')
INSERT #YourTable VALUES ('Administrator')
INSERT #YourTable VALUES ('abcde Administrator')
set nocount off
;WITH Digits AS
(SELECT 0 AS Digit UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9
)
,Numbers AS
(SELECT 1 AS Number
UNION ALL
SELECT Number+1 FROM Numbers where Number<1000
)
,FindDigits AS
(
SELECT
y.RowValue,n.Number,SUBSTRING(y.RowValue,n.Number,1) AS CharOf,CASE WHEN SUBSTRING(y.RowValue,n.Number,1) LIKE '[0-9]' THEN 'N' ELSE 'A' END AS TypeOf
FROM #YourTable y
INNER JOIN Numbers n ON 1=1
WHERE n.Number<=LEN(y.RowValue)
)
,LenOf AS
(
SELECT
RowValue,MIN(Number)-1 AS Digits
FROM FindDigits
WHERE TypeOf='A'
GROUP BY RowValue
HAVING MIN(Number)-1>0
UNION
SELECT
f.RowValue,LEN(f.RowValue)
FROM FindDigits f
WHERE NOT EXISTS (SELECT 1 FROM FindDigits f2 WHERE f.RowValue=f2.RowValue AND TypeOf='A')
)
UPDATE y
SET RowValue=CASE WHEN l.Digits IS NOT NULL THEN LEFT(y.RowValue,l.Digits)+RIGHT(y.RowValue,CHARINDEX(' ',REVERSE(y.RowValue)))
WHEN CHARINDEX(' ',REVERSE(y.RowValue))=0 THEN y.RowValue
ELSE RIGHT(y.RowValue,CHARINDEX(' ',REVERSE(y.RowValue))-1) END
FROM #YourTable y
LEFT JOIN LenOf l ON y.RowValue=l.RowValue
OPTION (MAXRECURSION 1000)
SELECT * FROM #YourTable
OUTPUT:
RowValue
--------------------------------------------------
13
1234
1234
1234
1234 Supervisor
1234 Administrator
1234567 Administrator
Administrator
Administrator
(9 row(s) affected)

You actually want two strings, the characters at indices 0-3 and those from the position after the space till the end of the string. I (think) this will work (have not tried it):
UPDATE TableName SET ColumnName = SUBSTRING(ColumnName,1,4) +
SUBSTRING(ColumnName,CHARINDEX(' ',ColumnName)+1,LEN(ColumnName))

The code below uses a "tally table" of values to find the first non-numeric character and the last space. KM's solution using PATINDEX is probably more elegant!
DECLARE #t TABLE
(
c VARCHAR(MAX)
);
INSERT INTO #t VALUES('1234abc');
INSERT INTO #t VALUES('1234abcde456757');
INSERT INTO #t VALUES('1234abc Supervisor');
INSERT INTO #t VALUES('1234abc456 Administrator');
WITH Tally AS
(
SELECT ROW_NUMBER() OVER (ORDER BY s1.[id]) AS i
FROM sys.sysobjects s1 CROSS JOIN sys.sysobjects s2 CROSS JOIN sys.sysobjects s3
),
NumPart AS
(
SELECT c, MIN(i) AS firstNonNumber
FROM #t CROSS JOIN Tally
WHERE i <= LEN(c)
AND SUBSTRING(c, i, 1) < '0' OR SUBSTRING(c, i, 1) > '9'
GROUP BY c
),
SpacePart AS
(
SELECT c, MAX(i) AS spacePos
FROM #t t CROSS JOIN Tally
WHERE i <= LEN(c)
AND SUBSTRING(c, i, 1) = ' '
GROUP BY c
)
UPDATE t
SET t.c = LEFT(n.c, n.firstNonNumber - 1) +
CASE WHEN ISNULL(s.SpacePos, 0) > 0 THEN
RIGHT(n.c, LEN(n.c) - s.SpacePos + 1)
ELSE
''
END
FROM #t t
INNER JOIN NumPart n ON t.c = n.c
LEFT JOIN SpacePart s ON n.c = s.c;
SELECT * FROM #t;

Related

Add character in front and at the end of each character

In SQL I want to add 0 in front and , at the end of each character.
Example: A30F1 -> 0A,03,00,0F,01
I don't want to use cursor if possible.
Thanks!
EIDT:
I apologize for not asking the most appropriate question at the beginning.
In short, I have a table and for each value in the column name I have to convert it to the desired format. For example, we have a #Temp table:
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721')
SELECT * FROM #Temp
One method would be to use a Tally to split the string into it's individual characters, and then use concatenation to add the 0 to the start, and STRING_AGG to comma delimit the results:
DECLARE #YourValue varchar(5) = 'A30F1';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(#YourValue))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2) --Up to 100 characters, add more cross joins for more characters
SELECT STRING_AGG(CONCAT('0',SS.C),',') WITHIN GROUP (ORDER BY T.I) AS NewString
FROM (VALUES(#YourValue))V(YourValue)
CROSS JOIN Tally T
CROSS APPLY (VALUES(SUBSTRING(V.YourValue,T.I,1)))SS(C);
It appears this is meant to be against a table, not a single value. This needs, however, very few changes to work against a table:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (SELECT MAX(LEN(YourColumn)) FROM dbo.YourTable)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2) --Up to 100 characters, add more cross joins for more characters
SELECT STRING_AGG(CONCAT('0',SS.C),',') WITHIN GROUP (ORDER BY T.I) AS NewString
FROM dbo.YourTable YT
JOIN Tally T ON LEN(YT.YourColumn) >= T.I
CROSS APPLY (VALUES(SUBSTRING(YT.YourColumn,T.I,1)))SS(C)
GROUP BY YT.YourColumn;
db<>fiddle
I solved the simplest possible with a few variables, WHILE and SUBSTRING
DECLARE #var VARCHAR(20) = 'A30F1', #i INT = 1, #res NVARCHAR(20)
WHILE (#i <= LEN(#var))
BEGIN
SET #res = #res + '0' + SUBSTRING(#var, #i, 1) + ','
SET #i = #i + 1
END
SELECT LEFT(#res, LEN(#res) - 1) output
Check demo on DB<>FIDDLE.
Original answer:
A recursive CTE and a STRING_AGG() call is also an option (SQL Server 2017+ is needed):
DECLARE #text varchar(max) = 'A30F1';
WITH rCTE AS
(
SELECT 1 AS CharacterPosition, SUBSTRING(#text, 1, 1) AS Character
UNION ALL
SELECT CharacterPosition + 1, SUBSTRING(#text, CharacterPosition + 1, 1)
FROM rCTE
WHERE CharacterPosition < LEN(#text)
)
SELECT STRING_AGG('0' + Character, ',') WITHIN GROUP (ORDER BY CharacterPosition)
FROM rCTE
OPTION (MAXRECURSION 0);
Update:
You need a different statement, if the names are stored in a table, again using recursion and STRING_AGG():
Table:
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721')
Statement:
; WITH rCTE AS (
SELECT
t.id AS id,
LEFT(t.name, 1) AS Character,
STUFF(t.name, 1, 1, '') AS CharactersRemaining,
1 AS CharacterPosition
FROM #Temp t
UNION ALL
SELECT
r.id,
LEFT(r.CharactersRemaining, 1),
STUFF(r.CharactersRemaining, 1, 1, ''),
CharacterPosition + 1
FROM rCTE r
WHERE LEN(r.CharactersRemaining) > 0
)
SELECT
id,
STRING_AGG('0' + Character, ',') WITHIN GROUP (ORDER BY CharacterPosition) AS name
FROM rCTE
GROUP BY id
OPTION (MAXRECURSION 0);
Result:
id name
1 0A,03,00,0F,01
2 0B,05,01,0R,09
3 0L,01,07,02,01
If you are only applying this to English alphabet characters and digits as in your example you could do this.
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721'), (4, 'A')
SELECT SUBSTRING(REPLACE(
0x00 + CAST(CAST(name AS NVARCHAR(25)) AS BINARY(50)), CHAR(0), '0,')
, 3
, LEN(name) * 3 - 1)
FROM #Temp
returns
0A,03,00,0F,01
0B,05,01,0R,09
0L,01,07,02,01
0A
This takes advantage of the fact that the binary representation of the nvarchar and varchar is the same for this limited character set except for padding out with 0x00
'A30F1' -> 0x4133304631
N'A30F1' -> 0x41003300300046003100

SQL Query for Min and Max Values [duplicate]

This question already has answers here:
How to split a comma-separated value to columns
(38 answers)
Closed 8 years ago.
I have the following data in a table. The number of values in each row can vary and the number of rows could also vary.
The table has 1 column with csv formatted values. The values will always be numeric
Data
1,2
4
5,12, 10
6,7,8,9,10
15,17
I would like to end up with a temp table with the following
Data Lowest Highest
1,2 1 2
4 4 4
5,12, 10 5 12
6,7,8,9,10 6 10
15,17 15 17
Can anyone help with writing a sql query or function to achieve this
Instead of function, you can achieve by this
;WITH tmp
AS (SELECT A.rn,split.a.value('.', 'VARCHAR(100)') AS String
FROM (SELECT Row_number() OVER(ORDER BY (SELECT NULL)) AS RN,
Cast ('<M>' + Replace([data], ',', '</M><M>') + '</M>' AS XML) AS String
FROM table1) AS A
CROSS apply string.nodes ('/M') AS Split(a))
SELECT X.data,Tmp.lower,Tmp.higher
FROM (SELECT rn,Min(Cast(string AS INT)) AS Lower,Max(Cast(string AS INT)) AS Higher
FROM tmp
GROUP BY rn) Tmp
JOIN (SELECT Row_number() OVER(ORDER BY (SELECT NULL)) AS RN1,data
FROM table1) X
ON X.rn1 = Tmp.rn
FIDDLE DEMO
Output would be:
Data Lower Higher
1,2 1 2
4 4 4
5,12, 10 5 12
6,7,8,9,10 6 10
15,17 15 17
First create a user defined function to convert each row of 'DATA' column to a intermediate table as:
/****** Object: UserDefinedFunction [dbo].[CSVToTable]******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[CSVToTable] (#InStr VARCHAR(MAX))
RETURNS #TempTab TABLE
(id int not null)
AS
BEGIN
;-- Ensure input ends with comma
SET #InStr = REPLACE(#InStr + ',', ',,', ',')
DECLARE #SP INT
DECLARE #VALUE VARCHAR(1000)
WHILE PATINDEX('%,%', #INSTR ) <> 0
BEGIN
SELECT #SP = PATINDEX('%,%',#INSTR)
SELECT #VALUE = LEFT(#INSTR , #SP - 1)
SELECT #INSTR = STUFF(#INSTR, 1, #SP, '')
INSERT INTO #TempTab(id) VALUES (#VALUE)
END
RETURN
END
GO
Function is explained further here.
Then Using Cross Apply we can get the desired output as:
With CTE as
(
select
T.Data, Min(udf.Id) as [Lowest],Max(udf.Id) as [Highest]
from
Test T
CROSS APPLY dbo.CSVToTable(T.Data) udf
Group By Data
)
Select * from CTE
Sample Code here...
What a Cross Apply does is : it applies the right table expression to each row from the left table and produces a result table with the unified result sets.
Create table #temp1 (name varchar(100),value int )
Declare #len int
Select #len=(select max(LEN(name)-LEN(replace(name,',',''))) from table)
Declare #i int = 1
while (#i<=#len+1)
begin
insert into #temp1
select name,PARSENAME(REPLACE(name,',','.'),#i) from table t
set #i = #i+1
end
Select name,MIN(value) MINV,MAX(value) MAXV from #temp1 group by name
declare #Testdata table ( Data varchar(max))
insert #Testdata select '1,2'
insert #Testdata select '4'
insert #Testdata select '5,12, 10'
insert #Testdata select '6,7,8,9,10'
;with tmp( DataItem, Data, RN1) as (
select LEFT(Data, CHARINDEX(',',Data+',')-1),
STUFF(Data, 1, CHARINDEX(',',Data+','), ''),
ROW_NUMBER()OVER(ORDER BY (SELECT NULL))AS RN1
from #Testdata
union all
select LEFT(Data, CHARINDEX(',',Data+',')-1),
STUFF(Data, 1, CHARINDEX(',',Data+','), ''),RN1
from tmp
where Data > ''
)
Select x.data,t.Low,t.Up FROM
(Select RN1,MIN(Cast(DataItem AS INT)) As Low,
MAX(Cast(DataItem AS INT)) As Up
FROM tmp t GROUP BY t.RN1)t
JOIN (Select ROW_NUMBER()OVER(ORDER BY (SELECT NULL))AS RN,data from #Testdata)X
ON X.RN = t.RN1

Return Distinct Rows That Contain The Same Value/Character In SQL

I have a bit of a tricky situation. I have a column that contains a pipe delimited set of numbers in numerous rows in a table. For example:
Courses
-------------------
1|2
1|2|3
1|2|8
10
11
11|12
What I want to achieve is to return rows where the number only appears once in my output.
Ideally, I want to try and carry this out using SQL rather than having to carry out checks at a web application level. Carrying out a DISTINCT does not achieve what I want.
The desired output would be:
Courses
-------------------
1
2
3
8
10
11
12
I would appreciated if anyone can guide me in the right direction.
Thanks.
Please try:
declare #tbl as table(Courses nvarchar(max))
insert into #tbl values
('1|2'),
('1|2|3'),
('1|2|8'),
('10'),
('11'),
('11|12')
select * from #tbl
SELECT
DISTINCT CAST(Split.a.value('.', 'VARCHAR(100)') AS INT) AS CVS
FROM
(
SELECT CAST ('<M>' + REPLACE(Courses, '|', '</M><M>') + '</M>' AS XML) AS CVS
FROM #tbl
) AS A CROSS APPLY CVS.nodes ('/M') AS Split(a)
ORDER BY 1
Try this one -
SET NOCOUNT ON;
DECLARE #temp TABLE
(
string VARCHAR(500)
)
DECLARE #Separator CHAR(1)
SELECT #Separator = '|'
INSERT INTO #temp (string)
VALUES
('1|2'),
('1|2|3'),
('1|2|8'),
('10'),
('11'),
('11|12')
-- 1. XML
SELECT p.value('(./s)[1]', 'VARCHAR(500)')
FROM (
SELECT field = CAST('<r><s>' + REPLACE(t.string, #Separator, '</s></r><r><s>') + '</s></r>' AS XML)
FROM #temp t
) d
CROSS APPLY field.nodes('/r') t(p)
-- 2. CTE
;WITH a AS
(
SELECT
start_pos = 1
, end_pos = CHARINDEX(#Separator, t.string)
, t.string
FROM #temp t
UNION ALL
SELECT
end_pos + 1
, CHARINDEX(#Separator, string, end_pos + 1)
, string
FROM a
WHERE end_pos > 0
)
SELECT d.name
FROM (
SELECT
name = SUBSTRING(
string
, start_pos
, ABS(end_pos - start_pos)
)
FROM a
) d
WHERE d.name != ''
Try this :
create table course (courses varchar(100))
insert into course values('1|2')
insert into course values('1|2|3')
insert into course values('1|2|8')
insert into course values('10')
insert into course values('11')
insert into course values('11|12')
Declare #col varchar(200)
SELECT
#col=(
SELECT DISTINCT c.courses + '|'
FROM course c
FOR XML PATH('')
);
select * from course
;with demo as(
select cast(substring(#col,1,charindex('|',#col,1)-1) AS INT) cou,charindex('|',#col,1) pos
union all
select cast(substring(#col,pos+1,charindex('|',#col,pos+1)-pos-1)AS INT) cou,charindex('|',#col,pos+1) pos
from demo where pos<LEN(#col))
select distinct cou from demo
Could not manage without recursion :( Something like this could do the trich?
WITH splitNum(num, r)
AS
(
SELECT
SUBSTRING(<field>,1, CHARINDEX('|', <field>)-1) num,
SUBSTRING(<field>,CHARINDEX('|', <field>)+1, len(<field>)) r
FROM <yourtable> as a
UNION ALL
SELECT
SUBSTRING(r,1, CHARINDEX('|', r)-1) num,
SUBSTRING(r,CHARINDEX('|', r)+1, len(r)) r
FROM <yourtable> b
WHERE CHARINDEX('|', r) > 0
inner join splitNum as c on <whatevertheprimarykeyis>
)
SELECT distinct num FROM splitNum
Didn't make it run, but it should do the trick, just replace the and with the correct info
One way would be to use a recursive CTE:
with cte as
(select cast(case charindex('|',courses) when 0 then courses
else left(courses,charindex('|',courses)-1) end as int) course,
case charindex('|',courses) when 0 then ''
else right(courses,len(courses)-charindex('|',courses)) end courses
from courses
union all
select cast(case charindex('|',courses) when 0 then courses
else left(courses,charindex('|',courses)-1) end as int) course,
case charindex('|',courses) when 0 then ''
else right(courses,len(courses)-charindex('|',courses)) end courses
from cte
where len(courses)>0)
select distinct course from cte
SQLFiddle here.

How to check if first five characters of one field match another?

Assuming I have the following table:
AAAAAA
AAAAAB
CCCCCC
How could I craft a query that would let me know that AAAAA and AAAAB are similar (as they share five characters in a row)? Ideally I would like to write this as a query that would check if the two fields shared five characters in a row anywhere in the string but this seems outside the scope of SQL and something I should write into a C# application?
Ideally the query would add another column that displays: Similar to 'AAAAA', 'AAAAB'
I suggest you do not try to violate 1NF by introducing a multi-valued attribute.
Noting that SUBSTRING is highly portable:
WITH T
AS
(
SELECT *
FROM (
VALUES ('AAAAAA'),
('AAAAAB'),
('CCCCCC')
) AS T (data_col)
)
SELECT T1.data_col,
T2.data_col AS data_col_similar_to
FROM T AS T1, T AS T2
WHERE T1.data_col < T2.data_col
AND SUBSTRING(T1.data_col, 1, 5)
= SUBSTRING(T2.data_col, 1, 5);
Alternativvely:
T1.data_col LIKE SUBSTRING(T2.data_col, 1, 5) + '%';
This will find all matches, also those in the middle of the word, it will not perform well on a big table
declare #t table(a varchar(20))
insert #t select 'AAAAAA'
insert #t select 'AAAAAB'
insert #t select 'CCCCCC'
insert #t select 'ABCCCCC'
insert #t select 'DDD'
declare #compare smallint = 5
;with cte as
(
select a, left(a, #compare) suba, 1 h
from #t
union all
select a, substring(a, h + 1, #compare), h+1
from cte where cte.h + #compare <= len(a)
)
select t.a, cte.a match from #t t
-- if you don't want the null matches, remove the 'left' from this join
left join cte on charindex(suba, t.a) > 0 and t.a <> cte.a
group by t.a, cte.a
Result:
a match
-------------------- ------
AAAAAA AAAAAB
AAAAAB AAAAAA
ABCCCCC CCCCCC
CCCCCC ABCCCCC
You can use left to compare the first five characters and you can use for xml path to concatenate the similar strings to one column.
declare #T table
(
ID int identity primary key,
Col varchar(10)
)
insert into #T values
('AAAAAA'),
('AAAAAB'),
('AAAAAC'),
('CCCCCC')
select Col,
stuff((select ','+T2.Col
from #T as T2
where left(T1.Col, 5) = left(T2.Col, 5) and
T1.ID <> T2.ID
for xml path(''), type).value('.', 'varchar(max)'), 1, 1, '') as Similar
from #T as T1
Result:
Col Similar
---------- -------------------------
AAAAAA AAAAAB,AAAAAC
AAAAAB AAAAAA,AAAAAC
AAAAAC AAAAAA,AAAAAB
CCCCCC NULL

Efficient way to string split using CTE

I have a table that looks like
ID Layout
1 hello,world,welcome,to,tsql
2 welcome,to,stackoverflow
The desired output should be
Id Splitdata
1 hello
1 world
1 welcome
1 to
1 tsql
2 welcome
2 to
2 stackoverflow
I have done this by the below query
Declare #t TABLE(
ID INT IDENTITY PRIMARY KEY,
Layout VARCHAR(MAX)
)
INSERT INTO #t(Layout)
SELECT 'hello,world,welcome,to,tsql' union all
SELECT 'welcome,to,stackoverflow'
--SELECT * FROM #t
;With cte AS(
select F1.id
,O.splitdata
from
(
select *,
cast('<X>'+replace(F.Layout,',','</X><X>')+'</X>' as XML) as xmlfilter
from #t F
)F1
cross apply
(
select fdata.D.value('.','varchar(MAX)') as splitdata
from f1.xmlfilter.nodes('X') as fdata(D)) O
)
select * from cte
But performance wise it is very bad. I am looking for a more efficient query but using CTE only.
You seem dead set on using a CTE, so try this:
DECLARE #YourTable table (RowID int, Layout varchar(200))
INSERT #YourTable VALUES (1,'hello,world,welcome,to,tsql')
INSERT #YourTable VALUES (2,'welcome,to,stackoverflow')
;WITH SplitSting AS
(
SELECT
RowID,LEFT(Layout,CHARINDEX(',',Layout)-1) AS Part
,RIGHT(Layout,LEN(Layout)-CHARINDEX(',',Layout)) AS Remainder
FROM #YourTable
WHERE Layout IS NOT NULL AND CHARINDEX(',',Layout)>0
UNION ALL
SELECT
RowID,LEFT(Remainder,CHARINDEX(',',Remainder)-1)
,RIGHT(Remainder,LEN(Remainder)-CHARINDEX(',',Remainder))
FROM SplitSting
WHERE Remainder IS NOT NULL AND CHARINDEX(',',Remainder)>0
UNION ALL
SELECT
RowID,Remainder,null
FROM SplitSting
WHERE Remainder IS NOT NULL AND CHARINDEX(',',Remainder)=0
)
SELECT * FROM SplitSting ORDER BY RowID
OUTPUT:
RowID Part
----------- -----------------------
1 hello
1 world
1 welcome
1 to
1 tsql
2 welcome
2 to
2 stackoverflow
(8 row(s) affected)
here is an excellent article on splitting strings in SQL Server: "Arrays and Lists in SQL Server 2005 and Beyond, When Table Value Parameters Do Not Cut it" by Erland Sommarskog
EDIT here's another version (but you need a numbers table) returns same results as above:
;WITH SplitValues AS
(
SELECT
RowID,ListValue
FROM (SELECT
RowID, LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(',', List2, number+1)-number - 1))) AS ListValue
FROM (
SELECT RowID, ',' + Layout + ',' AS List2
FROM #YourTable
) AS dt
INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
WHERE SUBSTRING(List2, number, 1) = ','
) dt2
WHERE ListValue IS NOT NULL AND ListValue!=''
)
SELECT * FROM SplitValues
see here for a numbers table: What is the best way to create and populate a numbers table?
From NullRef's Answer
Function without set operation will be faster according to my understanding of sql server
so this will be more efficient
CREATE FUNCTION fnSplitString(#str nvarchar(max),#sep nvarchar(max))
RETURNS TABLE
AS
RETURN
WITH a AS(
SELECT CAST(0 AS BIGINT) as idx1,CHARINDEX(#sep,#str) idx2
UNION ALL
SELECT idx2+1,CHARINDEX(#sep,#str,idx2+1)
FROM a
WHERE idx2>0
)
SELECT SUBSTRING(#str,idx1,COALESCE(NULLIF(idx2,0),LEN(#str)+1)-idx1) as value
FROM a
it's my best solution using CTE:
DECLARE #Char VARCHAR(MAX) = '10||3112||||aaaa||'
DECLARE #Separador CHAR(2) = '||'
;WITH Entrada AS(
SELECT
CAST(1 AS Int) As Inicio,
CHARINDEX(#Separador, #Char) As Fim
UNION ALL
SELECT
CAST(Fim + LEN(#Separador) AS Int) As Inicio,
CHARINDEX(#Separador, #Char, Fim + 1) As Fim
FROM Entrada
WHERE CHARINDEX(#Separador, #Char, Fim + 1) > 0
)
SELECT
SUBSTRING(#Char, Inicio, Fim - Inicio)
FROM Entrada
WHERE (Fim - Inicio) > 0