Split string in SQL server 2012 in a row - sql-server

The string is in
#txt nvarchar(max)='2450,10,54,kb2344,kd5433;87766,500,100,ki5332108,ow092827'
And I want output like this:
Id. Val1. Val2. Val3. Val4. Val5.
1. 2450 10 54 kb2344 kd5433.
2. 87766 500 100 ki5332108 ow09287
Can anybody suggest how to do that?
I google it , and found this solution. But it is for two comma separated values but in my case there are five:
DECLARE #Var NVARCHAR(100) = '2450,10,54,kb2344,kd5433;87766,500,100,ki5332108,ow092827'
SELECT LEFT(#Var, CHARINDEX(';', #Var) - 1) ,SUBSTRING(#Var, CHARINDEX(';', #Var) + 1, LEN(#Var)- LEN(LEFT(#Var, CHARINDEX(';', #Var)))- LEN(RIGHT(#Var, CHARINDEX(';', REVERSE(#Var))))) AS [Job] , RIGHT(#Var, CHARINDEX(';', REVERSE(#Var))-1)

Assuming values 1-5. This can easily be done with a little XML in concert with a CROSS APPLY
If the number columns are variable, you would have to go DYNAMIC.
EDIT - Changed to nvarchar
Example
Declare #txt nvarchar(max)='2450,10,54,kb2344,kd5433;87766,500,100,ki5332108,ow092827'
Select ID=A.RetSeq
,B.*
From (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'nvarchar(max)')))
From (Select x = Cast('<x>' + replace(#txt ,';','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) A
Cross Apply (
Select Val1 = ltrim(rtrim(xDim.value('/x[1]','nvarchar(max)')))
,Val2 = ltrim(rtrim(xDim.value('/x[2]','nvarchar(max)')))
,Val3 = ltrim(rtrim(xDim.value('/x[3]','nvarchar(max)')))
,Val4 = ltrim(rtrim(xDim.value('/x[4]','nvarchar(max)')))
,Val5 = ltrim(rtrim(xDim.value('/x[5]','nvarchar(max)')))
From (Select Cast('<x>' + replace(A.RetVal,',','</x><x>')+'</x>' as xml) as xDim) as B1
) B
Returns
ID Val1 Val2 Val3 Val4 Val5
1 2450 10 54 kb2344 kd5433
2 87766 500 100 ki5332108 ow092827

Related

Split a string and retain character position

I have a large character string (nvarchar(MAX)) that I am trying to split 2 and identify which part of the original string they are.
Example
String:
5;718;0;1071;1.23|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25
I would first need to split the strings based on the '|' character so something like this:
5;718;0;1071;1.23
0;750;0;997;1.25
0;750;0;997;1.25
0;750;0;997;1.25
0;750;0;997;1.25
0;750;0;997;1.25
I would then to split each of those based on the ';' character:
So 5;718;0;1071;1.23 would then split into:
5
718
0
1071
1.23
I know I could do a string_split on the '|' then another string_split on the ';' but that does not maintain an order or identify from which portion of the string the result is split from and I am unfortunately not quite able to get the results I am looking for when trying to use OPENJSON():
Based on the example above I would need a result that could identify that the 718 was from the first group and the 2nd item in said group.
Here is an option that will parse your string and maintain the sequence
Example
Declare #S varchar(max) = '5;718;0;1071;1.23|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25'
Select Seq1 = A.RetSeq
,Val1 = A.RetVal
,Seq2 = B.RetSeq
,Val2 = B.RetVal
From [dbo].[tvf-Str-Parse](#S,'|') A
Cross Apply [dbo].[tvf-Str-Parse](A.RetVal,';') B
Returns
The Function if Interested
CREATE FUNCTION [dbo].[tvf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = row_number() over (order by 1/0)
,RetVal = ltrim(rtrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
EDIT - Update for TABLE
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'5;718;0;1071;1.23|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25')
Select A.ID
,Seq1 = B.RetSeq
,Val1 = B.RetVal
,Seq2 = C.RetSeq
,Val2 = C.RetVal
From #YourTable A
Cross Apply [dbo].[tvf-Str-Parse](SomeCol,'|') B
Cross Apply [dbo].[tvf-Str-Parse](B.RetVal,';') C
here's my old school take:
declare #v nvarchar(max) = N'5;718;0;1071;1.23|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25'
select a.value('.', 'nvarchar(max)') [value], v2.col, rnInternal,ROW_NUMBER() over(partition by rnInternal order by rnInternal, Split.a) rnExternal
from
(
select cast('<M>' + REPLACE(v.[value], ';', '</M><M>') + '</M>' AS XML) as col, rnInternal
from
(
select
a.value('.', 'nvarchar(max)') [value],
ROW_NUMBER() over(order by Split.a) rnInternal
from
(select cast('<M>' + REPLACE(#v, '|', '</M><M>') + '</M>' AS XML) as col) as A
CROSS APPLY A.col.nodes ('/M') AS Split(a)
where
a.value('.', 'nvarchar(max)') <> ''
) v
) v2
CROSS APPLY v2.col.nodes ('/M') AS Split(a)
order by rnInternal, rnExternal
You are right about the STRING_SPLIT() (as is mentioned in the documentation the output rows might be in any order), but you may use a JSON-based approach to get the expected results.
You need to transform the input text into a valid nested JSON array (5;718;0;1071;1.23|0;750;0;997;1.25| into [[5,718,0,1071,1.23],[0,750,0,997,1.25]]) and parse this array with two OPENJSON() calls using a default schema. The result from the OPENJSON() call is a table with columns key, value and type and in case of JSON array the key column returns the index of the element in the specified array:
DECLARE #text nvarchar(max) = N'5;718;0;1071;1.23|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25|0;750;0;997;1.25'
SELECT
CONVERT(int, j1.[key]) + 1 AS id1,
CONVERT(int, j2.[key]) + 1 AS id2,
j2.[value]
FROM OPENJSON(CONCAT('[[', REPLACE(REPLACE(#text, '|', '],['), ';', ','), ']]')) j1
CROSS APPLY OPENJSON(j1.[value]) j2
ORDER BY CONVERT(int, j1.[key]), CONVERT(int, j2.[key])
Result:
id1 id2 value
1 1 5
1 2 718
1 3 0
1 4 1071
1 5 1.23
2 1 0
2 2 750
...
6 1 0
6 2 750
6 3 0
6 4 997
6 5 1.25
If you want to identify the id's of each group and subgroup, you need to add an appropriate WHERE clause:
SELECT
CONVERT(int, j1.[key]) + 1 AS id1,
CONVERT(int, j2.[key]) + 1 AS id2
FROM OPENJSON(CONCAT('[[', REPLACE(REPLACE(#text, '|', '],['), ';', ','), ']]')) j1
CROSS APPLY OPENJSON(j1.[value]) j2
WHERE j2.[value] = '718'
ORDER BY CONVERT(int, j1.[key]), CONVERT(int, j2.[key])
Result:
id1 id2
1 2

Select string between "\" and "." in SQL Server

I have the following table column named file and it contains directories.
Sample data is:
C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML
C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML
Note that I only want to get 6860_f11.xlxb for #1 and 1191_f12.xlxb for #2.
For #1, the directory only contains 1 folder filedata but for #2 it contains 2 folders cloud\files
Below is my code:
select
(SUBSTRING((file), 0, CHARINDEX ('.xlxb', (file)) + 4)) as xlsb_file
from
[Projects].[dbo].[ProjFiles]
Is there any way I could get the string after the folder and until the underscore after .xlxb?
No CLR needed. No Regex required. The easiest and best performing way to solve this using NGrams8K. It's 2AM where I live so I'll make this quick.
Note this query:
DECLARE #string VARCHAR(150) = 'C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML';
SELECT RetPos = f.p, RetVal = e.s
FROM (SELECT MAX(position)+1 FROM samd.NGrams8k(#string,1) WHERE token = '\') AS f(p)
CROSS APPLY (VALUES(SUBSTRING(#string,f.p,CHARINDEX('.',#string,f.p)-f.p+5))) AS e(s);
Results:
RetPos RetVal
------ ---------------
16 1191_f12.xlxb
Now Against a table:
CREATE TABLE #yourtable ([file] VARCHAR(150));
INSERT INTO #yourtable
VALUES ('C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML'),
('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML');
SELECT *
FROM #yourtable AS t
CROSS APPLY
(
SELECT newstring = e.s
FROM (SELECT MAX(position) FROM samd.NGrams8k(t.[file],1) WHERE token = '\') AS f(p)
CROSS
APPLY (VALUES(SUBSTRING(t.[file],f.p+1,CHARINDEX('.',t.[file],f.p+1)-f.p+4))) AS e(s)
) AS itvf_str_extract;
It's really that easy. The performance will beats the pants of any CLR/Regex Based solution too - so there's that.
On a side note: John Cappelletti's solution is excellent (as per usual). Under the hood it's very similar to my NGrams solution but not exactly; compare these two queries:
DECLARE #string VARCHAR(150) = 'C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML';
DECLARE #Delimiter1 varchar(100) = '\', #Delimiter2 varchar(100) = '.';
-- Alan B
SELECT
RetSeq = 1,
RetPos = f.p,
RetVal = e.s
FROM (SELECT MAX(position)+1 FROM samd.NGrams8k(#string,1) WHERE token = '\') AS f(p)
CROSS
APPLY (VALUES(SUBSTRING(#string,f.p,CHARINDEX('.',#string,f.p)-f.p+5))) AS e(s);
-- John C
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 N1,cte1 N2,cte1 N3,cte1 N4,cte1 N5,cte1 N6) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter1) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter1)) = #Delimiter1),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter1,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By N)
,RetPos = N
,RetVal = left(RetVal,charindex(#Delimiter2,RetVal)-1)
From (
Select *,RetVal = Substring(#String, N, L)
From cte4
) A
Where charindex(#Delimiter2,RetVal)>1;
Now the execution plans:
There are several ways to do it. This is one method
select substring(reverse(substring(reverse('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML'),1,charindex('\',reverse('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML'))-1)),1,13)
And if your filename is not always the same, you could do the same logic with _
This one will work with underscore - given that there is no other underscore than xlxb_
select
reverse(substring(reverse(reverse(substring(reverse('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML'),1,charindex('\',reverse('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML'))-1))),charindex('_',reverse('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML'))+1,len('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML')))
For such tasks it will be better to implement SQL CLR function. Here is example of how to create such. Having regex support, you can do things like the following:
DECLARE #DataSource TABLE
(
[value] VARCHAR(256)
);
INSERT INTO #DataSource ([value])
VALUES ('C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML')
,('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML');
SELECT *
FROM #DataSource DS
CROSS APPLY [dbo].[fn_Utils_RegexMatches] ([value], '[^\\]+\.xlxb');
Of course, you can create more complicate patterns to match using regular expressions.
If open to a Table Valued Function, consider the following.
Tired of extracting strings (left,right,charindex,patindex,etc.), I modified a parse function to accept two non-like delimiters. In this case '\' and '.xlxb'
Also being a TVF, it is easy to incorporate into a CROSS APPLY.
Example
Declare #YourTable table ([file] varchar(150))
Insert Into #YourTable values
('C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML')
,('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML')
Select A.*
,B.*
From #YourTable A
Cross Apply [dbo].[tvf-Str-Extract](A.[File],'\','.xlxb') B
Returns
file RetSeq RetPos RetVal
C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML 1 13 6860_f11
C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML 1 16 1191_f12
The TVF if Interested
CREATE FUNCTION [dbo].[tvf-Str-Extract] (#String varchar(max),#Delimiter1 varchar(100),#Delimiter2 varchar(100))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 N1,cte1 N2,cte1 N3,cte1 N4,cte1 N5,cte1 N6) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter1) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter1)) = #Delimiter1),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter1,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By N)
,RetPos = N
,RetVal = left(RetVal,charindex(#Delimiter2,RetVal)-1)
From (
Select *,RetVal = Substring(#String, N, L)
From cte4
) A
Where charindex(#Delimiter2,RetVal)>1
)
/*
Max Length of String 1MM characters
Declare #String varchar(max) = 'Dear [[FirstName]] [[LastName]], ...'
Select * From [dbo].[tvf-Str-Extract] (#String,'[[',']]')
*/
For SQL Server, starting with 2016, you may use STRING_SPLIT():
CREATE TABLE #FileTable (
FullName varchar(150)
)
INSERT INTO #FileTable (FullName)
VALUES
('C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML'),
('C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML')
SELECT
f.FullName,
SUBSTRING(s.[value], 1, CHARINDEX('.xlxb', s.[value]) + LEN('xlxb')) AS FileName
FROM #FileTable f
CROSS APPLY STRING_SPLIT(f.FullName, '\') s
WHERE s.[value] LIKE '%.xlxb%'
Output:
FullName FileName
C:\filedata\6860_f11.xlxb_3.30 test - 0.3 ML 6860_f11.xlxb
C:\cloud\files\1191_f12.xlxb_12.16 test - 0.3 ML 1191_f12.xlxb

T-SQL Put in a field the average of hyphen separated values present in another field

I have a table with like this:
ID | FIX_1 | FTO | FIX_2 |
_____________________________________________________
1 | | 15452 |1.3-1.7-1.8-2.4-2.0 |
2 | | 15454 |1.4-1.1-1.4-2.7-2.6-1.8-2.4 |
3 | | 15454 |1.9-1.3-1.3 |
.... ...... .... .................
.... ...... .... .................
100 | | 15552 |0.4-1.7-1.2-2.1-2.6-1.6 |
I need do a select with FIX_1 field equal to the average of the hyphen separated values in FIX_2 field.
Is it possible with T-SQL without use of temporary table?
Thanks in advance
Option with a UDF
Declare #YourTable table (ID int,FIX_1 money,FTO int,FIX_2 varchar(max))
Insert Into #YourTable values
(1,null,15452,'1.3-1.7-1.8-2.4-2.0'),
(2,null,15454,'1.4-1.1-1.4-2.7-2.6-1.8-2.4'),
(3,null,15454,'1.9-1.3-1.3')
Update #YourTable Set FIX_1=B.Value
From #YourTable A
Cross Apply (
Select Value = Avg(cast(RetVal as money))
From (Select * from [dbo].[udf-Str-Parse](A.FIX_2,'-')) B1
) B
Select * From #YourTable
Option without a UDF
Update #YourTable Set FIX_1=B.Value
From #YourTable A
Cross Apply (
Select Value = Avg(cast(RetVal as money))
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(A.FIX_2,'-','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B1
) B
Both would Return
ID FIX_1 FTO FIX_2
1 1.84 15452 1.3-1.7-1.8-2.4-2.0
2 1.9142 15454 1.4-1.1-1.4-2.7-2.6-1.8-2.4
3 1.50 15454 1.9-1.3-1.3
The UDF if Needed
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
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(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Thanks Shnugo for making this XML safe
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
--Select * from [dbo].[udf-Str-Parse]('this,is,<test>,for,< & >',',')
with a user defined function...
create FUNCTION dbo.AvgOfDashSepVals ( #vals varchar(500))
returns float as
BEGIN
declare #avg decimal
declare #cnt int = 0
declare #sum float = 0.0
While charIndex('-', #vals) > 0 Begin
if isnumeric(left(#vals, charIndex('-', #vals)-1)) = 0
return null
set #cnt+= 1
set #sum += cast(left(#vals, charIndex('-', #vals)-1) as float)
set #vals = substring(#vals, charIndex('-', #vals)+1, len(#vals))
End
RETURN case #cnt when 0 then null else #sum / #cnt end
then alter your table to add a computed column.
alter table myTable
add Fix_1 as ([dbo].[AvgOfDashSepVals]([Fix_2]))
Declare #YourTable table (ID int,FIX_1 money,FTO int,FIX_2 varchar(max))
Insert Into #YourTable values
(1,null,15452,'1.3-1.7-1.8-2.4-2.0'),
(2,null,15454,'1.4-1.1-1.4-2.7-2.6-1.8-2.4'),
(3,null,15454,'1.9-1.3-1.3'),
(4,null,15454,'1.5')
;WITH cte AS
(
SELECT ID, SUBSTRING(FIX_2, 1, CHARINDEX('-',FIX_2) - 1) AS VALUE, SUBSTRING(FIX_2, CHARINDEX('-',FIX_2) + 1, LEN(FIX_2)) AS NEW_FIX_2
FROM #YourTable
WHERE CHARINDEX('-',FIX_2) > 0
UNION ALL
SELECT cte.ID, SUBSTRING(NEW_FIX_2, 1, CHARINDEX('-',NEW_FIX_2) - 1) AS VALUE, SUBSTRING(NEW_FIX_2, CHARINDEX('-',NEW_FIX_2) + 1, LEN(NEW_FIX_2)) AS NEW_FIX_2
FROM #YourTable y
JOIN cte ON cte.ID = y.ID
WHERE CHARINDEX('-', NEW_FIX_2) > 0
UNION ALL
SELECT ID, NEW_FIX_2, NULL
FROM cte
WHERE CHARINDEX('-', NEW_FIX_2) = 0
)
SELECT t.ID, ISNULL(v.VALUE, t.FIX_2) AS FIX_1, t.FTO, t.FIX_2
FROM #YourTable t
LEFT JOIN (
SELECT cte.ID, AVG(CAST(cte.VALUE AS MONEY)) AS VALUE
FROM cte
GROUP BY cte.ID
) v ON v.ID = t.ID

T-SQL split on delimiter

I am working with an employee hierarchy string that is in the format of the following. These number represent employeeID numbers and how the are structured within the company, thus being able to follow the chain of management.
123|456|789|012|345|320
I am trying to take this string of data and turn it into a temp table so I can work with each of the ID's as their own value.
I tried making a function to split the string:
ALTER FUNCTION [dbo].[SplitString]
(#String NVARCHAR(4000),
#Delimiter NCHAR(1))
RETURNS TABLE
AS
RETURN
(WITH Split(stpos, endpos) AS
(
SELECT 0 AS stpos, CHARINDEX(#Delimiter, #String) AS endpos
UNION ALL
SELECT endpos + 1, CHARINDEX(#Delimiter, #String, endpos+1)
FROM Split
WHERE endpos > 0
)
SELECT
'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
'Data' = SUBSTRING(#String, stpos, COALESCE(NULLIF(endpos, 0), LEN(#String) + 1))
FROM
Split
)
This however resulted in the following:
Id Data
-------------------
1 123
2 456|7893
3 7893|012|345|
4 012|345|320
5 345|320
6 320
Is there a better way to approach this, maybe not needing a function at all or will it be required to achieve this?
Without a Parse Function
Declare #YourTable table (ID int,IDList varchar(Max))
Insert Into #YourTable values
(1,'123|456|789|012|345|320'),
(2,'123|456')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
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 A.IDList as [*] For XML Path('')),'|','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B
Returns
ID RetSeq RetVal
1 1 123
1 2 456
1 3 789
1 4 012
1 5 345
1 6 320
2 1 123
2 2 456
OR with the SUPER DUPER Parse (orig source listed below / couple of tweaks)
Select A.ID
,B.*
From #YourTable A
Cross Apply [dbo].[udf-Str-Parse-8K](A.IDList,'|') B
Would Return the same as above
CREATE FUNCTION [dbo].[udf-Str-Parse-8K] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 a,cte1 b,cte1 c,cte1 d) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter)) = #Delimiter),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By A.N)
,RetVal = Substring(#String, A.N, A.L)
From cte4 A
);
--Orginal Source http://www.sqlservercentral.com/articles/Tally+Table/72993/
--Much faster than str-Parse, but limited to 8K
--Select * from [dbo].[udf-Str-Parse-8K]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse-8K]('John||Cappelletti||was||here','||')
Edit - Stand Alone
Declare #String varchar(max) = '123|456|789|012|345|320'
Declare #Delim varchar(10) = '|'
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 #String as [*] For XML Path('')),#Delim,'</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
If you need a string "splitter" the fastest one available for 2012 (pre- 2016) is going to be found here. This will blow the doors off of anything posted thusfar. If your items/tokens are all the same size then an even faster method would be this:
DECLARE #yourstring varchar(8000) = '123|456|789|012|345|320';
WITH E(N) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))t(v)),
iTally(N) AS (SELECT TOP ((LEN(#yourstring)/4)+1) ROW_NUMBER() OVER (ORDER BY (SELECT 1))
FROM e a, e b, e c, e d)
SELECT itemNumber = ROW_NUMBER() OVER (ORDER BY N), item = SUBSTRING(#yourstring, ((N*4)-3), 3)
FROM iTally;
Results:
itemNumber item
-------------------- ----
1 123
2 456
3 789
4 012
5 345
6 320
I write more about this and provide examples of how to put this logic into a function here.
I use this version of the Split function.
CREATE FUNCTION [dbo].[Split]
(
#delimited nvarchar(max),
#delimiter nvarchar(100)
) RETURNS #t TABLE
(
-- Id column can be commented out, not required for sql splitting string
id int identity(1,1), -- I use this column for numbering splitted parts
val nvarchar(max)
)
AS
BEGIN
declare #xml xml
set #xml = N'<root><r>' + replace(#delimited,#delimiter,'</r><r>') + '</r></root>'
insert into #t(val)
select
r.value('.','varchar(max)') as item
from #xml.nodes('//root/r') as records(r)
RETURN
END
You Query would look something like....
SELECT *
FROM TableName t
CROSS APPLY [dbo].[Split](t.EmpIDs, '|')

How to create a View showing rows from splitted data

With a table as:
Name Num Value
----------------------
Peter 10 10
Mary 10,15 5,10
John 5,20 10,20
How can I get a result as the follow table with a View ?
Name Num Value
------------------
Peter 10 10
Mary 10 5
Mary 15 10
John 5 10
John 20 20
Notice Mary and John have multiple data (comma-delimited).
I've a function to do the split of a cell and returns a table but just for a specific row and cell, hos to iterate from all table ?
Aditional Info:
SELECT NAM.NAME, Data AS NUM, VAL.VALUE
FROM dbo.Split((SELECT NUM FROM t1 WHERE LineNum = 2), ','))
CROSS APPLY (
SELECT Id As Id1, Data AS VALUE
FROM dbo.Split((SELECT VALUE FROM t1 WHERE LineNum = 2), ','))
) AS VAL
CROSS APPLY (
SELECT NAME FROM t1 WHERE LineNum = 2
) AS NAM
WHERE Id = Id1
Note: LineNum is row number from t1
From Second Row of t1 (Mary | 10,15 | 5,10)
Previous Function returns table as:
Name Num Value
------------------
Mary 10 5
Mary 15 10
Split Function from a cell (10,15) returns:
Id Data
-----------
1 10
2 15
===============================================
My Split Function:
CREATE FUNCTION [dbo].[Split]
(
#String NVARCHAR(4000),
#Delimiter NCHAR(1)
)
RETURNS TABLE
AS
RETURN
(
WITH Split(stpos,endpos)
AS(
SELECT 0 AS stpos, CHARINDEX(#Delimiter,#String) AS endpos
UNION ALL
SELECT endpos+1, CHARINDEX(#Delimiter,#String,endpos+1)
FROM Split
WHERE endpos > 0
)
SELECT 'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
'Data' = SUBSTRING(#String,stpos,COALESCE(NULLIF(endpos,0),LEN(#String)+1)-stpos)
FROM Split
)
Declare #YourTable table (Name varchar(50), Num varchar(50), Value varchar(50))
Insert into #YourTable values
('Peter','10', '10'),
('Mary','10,15','5,10'),
('John','5,20', '10,20')
Select A.Name
,B.*
From #YourTable A
Cross Apply (
Select Num = A.RetVal
,Value = B.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(A.Num,',','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) A
Join (
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(A.Value,',','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B on A.RetSeq=B.RetSeq
) B
Returns
Name Num Value
Peter 10 10
Mary 10 5
Mary 15 10
John 5 10
John 20 20
Edit - Another option with a UDF
Select A.Name
,B.*
From #YourTable A
Cross Apply (
Select Num = A.RetVal
,Value = B.RetVal
From [dbo].[udf-Str-Parse-8K](A.Num,',') A
Join [dbo].[udf-Str-Parse-8K](A.Value,',') B
on A.RetSeq=B.RetSeq
) B
The fastest UDF
CREATE FUNCTION [dbo].[udf-Str-Parse-8K] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 a,cte1 b,cte1 c,cte1 d) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter)) = #Delimiter),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By A.N)
,RetVal = Substring(#String, A.N, A.L)
From cte4 A
);
--Much faster than str-Parse, but limited to 8K
--Select * from [dbo].[udf-Str-Parse-8K]('Dog,Cat,House,Car',',')

Resources