SQL - Get specific element from an array - arrays

Suppose I have 2 variables that look like an array:
declare #code nvarchar(200) =
',10501,10203,10491,10490,10091,10253,10008,10020,10570,10499,';
declare #value nvarchar(200) =
'True~~100000006~Digital~0~0~~1388.76~Completed~True';
I need to find if #code contains 10490 (for example) and if it does, I need to find a corresponding value (by its index) in #value variable which would be Digital since 10490 is the 4th element in #code array and 4th element of #value array is Digital (note that the 2nd element of the #value array is NULL.
Disclaimer:
#code array will ALWAYS contain unique values. It's not possible to have more than 1 10490 for example.
#code array will always start and end with ','.
Number of elements in #code and #value will always be the same if you take 1st and last comma off the #code variable.
I cannot use functions or stored procedures, so everything needs to be done as part of 1 query.

I think you know, that this is a very bad design... If you can change this, you really should. But this can be solved:
declare #code nvarchar(200) =
',10501,10203,10491,10490,10091,10253,10008,10020,10570,10499,';
declare #value nvarchar(200) =
'True~~100000006~Digital~0~0~~1388.76~Completed~True';
--The query will cast both strings to a splittable XML
--The query('/x[text()]') will remove empty entries (leading and trailing comma)
--(...assuming there will never be an empty entry in #code)
--Then it will read a derived numbered list from both
--finally it will join both lists on their PartIndex
WITH Casted AS
(
SELECT CAST('<x>' + REPLACE(#code,',','</x><x>') + '</x>' AS XML).query('/x[text()]') AS CodeXml
,CAST('<x>' + REPLACE(#value,'~','</x><x>') + '</x>' AS XML) AS ValueXml
)
,CodeDerived AS
(
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS PartIndex
,x.value('text()[1]','nvarchar(max)') AS CodePart
FROM Casted
CROSS APPLY CodeXml.nodes('/x') A(x)
)
,ValueDerived AS
(
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS PartIndex
,x.value('text()[1]','nvarchar(max)') AS ValuePart
FROM Casted
CROSS APPLY ValueXml.nodes('/x') A(x)
)
SELECT cd.PartIndex
,CodePart
,ValuePart
FROM CodeDerived cd
INNER JOIN ValueDerived vd ON cd.PartIndex=vd.PartIndex
The result
inx CodePart ValuePart
1 10501 True
2 10203 NULL
3 10491 100000006
4 10490 Digital
5 10091 0
6 10253 0
7 10008 NULL
8 10020 1388.76
9 10570 Completed
10 10499 True
Just add a simple WHERE to reduce this to the one value you need.
Disclaimer: it is not guaranteed, that the numbering with ROW_NUMBER and ORDER BY (SELECT NULL) will ever return the correct sequence, but for a better chance you'd need SQL Server 2016+. For more details: read this link and the other contributions there

Here are two possibilities. In your case I would even try to merge it into one WHILE loop.
SQL Server 2016 and above
(compatibility level 130 and up) you can use built in function STRING_SPLIT
DECLARE #code nvarchar(200) =
',10501,10203,10491,10490,10091,10253,10008,10020,10570,10499,';
DECLARE #value nvarchar(200) =
'True~~100000006~Digital~0~0~~1388.76~Completed~True';
DECLARE #valuetosearch nvarchar(200) = '10490'
SELECT value FROM
(
SELECT value ,ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS 'idx'
FROM STRING_SPLIT ( #value , '~' )
) AS x2
WHERE x2.idx =
(
SELECT idx-1 FROM
(
SELECT value ,ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS 'idx'
FROM STRING_SPLIT ( #code , ',' )
) AS x1
WHERE x1.[value] = #valuetosearch
)
For earlier versions of SQL Server:
DECLARE #code nvarchar(200) =
',10501,10203,10491,10490,10091,10253,10008,10020,10570,10499,';
DECLARE #value nvarchar(200) =
'True~~100000006~Digital~0~0~~1388.76~Completed~True';
DECLARE #valuetosearch nvarchar(200) = '10490'
DECLARE #codetbl AS TABLE (idx int IDENTITY(1,1)
,code nvarchar(200))
DECLARE #valuetbl AS TABLE (idx int IDENTITY(1,1)
,value nvarchar(200))
DECLARE #name nvarchar(200)
DECLARE #pos int
WHILE CHARINDEX(',', #code) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #code)
SELECT #name = SUBSTRING(#code, 1, #pos-1)
INSERT INTO #codetbl
SELECT #name
SELECT #code = SUBSTRING(#code, #pos+1, LEN(#code)-#pos)
END
INSERT INTO #codetbl
SELECT #code
WHILE CHARINDEX('~', #value) > 0
BEGIN
SELECT #pos = CHARINDEX('~', #value)
SELECT #name = SUBSTRING(#value, 1, #pos-1)
INSERT INTO #valuetbl
SELECT #name
SELECT #value = SUBSTRING(#value, #pos+1, LEN(#value)-#pos)
END
INSERT INTO #valuetbl
SELECT #value
SELECT value FROM #valuetbl
WHERE idx = (SELECT idx-1 FROM #codetbl WHERE code = #valuetosearch)

You may need to add some code for when #tofind is not found
declare #code nvarchar(200) =
',10501,10203,10491,10490,10091,10253,10008,10020,10570,10499,';
declare #value nvarchar(200) =
'True~~100000006~Digital~0~0~~1388.76~Completed~True';
declare #tofind nvarchar(200) = '10490';
--select left(#code,CHARINDEX(#tofind,#code))
--select len(left(#code,CHARINDEX(#tofind,#code))) - LEN( REPLACE( left(#code,CHARINDEX(#tofind,#code)) , ',', ''))
declare #nth int;
set #nth = len(left(#code,CHARINDEX(#tofind,#code))) - LEN( REPLACE( left(#code,CHARINDEX(#tofind,#code)) , ',', ''))
declare #SplitOn nvarchar = '~';
declare #RowData nvarchar(200) = #value + '~';
declare #Cnt int = 1
While (Charindex(#SplitOn,#RowData)>0) and #Cnt < #nth
Begin
Set #RowData = Substring(#RowData,Charindex(#SplitOn,#RowData)+1,len(#RowData))
Set #Cnt = #Cnt + 1
End
Select --Data = ltrim(rtrim(#RowData)),
Case when ltrim(rtrim(#RowData)) = '' then null else
LEFT(ltrim(rtrim(#RowData)) , CHARINDEX('~',ltrim(rtrim(#RowData))) -1)
end as Result

This should be quite simple. If performance is important I would suggest splitting the strings using DelimitedSplit8K. Here's a simple, high-performing solution:
DECLARE #searchFor INT = 10490;
SELECT code = s1.item, s2.item
FROM dbo.DelimitedSplit8K(#code,',') s1
JOIN dbo.DelimitedSplit8K(#value,'~') s2 ON s2.ItemNumber = s1.ItemNumber-1
WHERE s1.Item = #searchFor;
Results:
code item
---------- ------------
10490 Digital

Related

SQL Server : column contains values like (1,2,3,4) - I need convert rows level A 1 2 3 4 [duplicate]

I have a SQL Server 2008 R2 column containing a string which I need to split by a comma. I have seen many answers on StackOverflow but none of them works in R2. I have made sure I have select permissions on any split function examples. Any help greatly appreciated.
I've used this SQL before which may work for you:-
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE CHARINDEX(',', #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
and to use it:-
SELECT * FROM dbo.splitstring('91,12,65,78,56,789')
Instead of recursive CTEs and while loops, has anyone considered a more set-based approach? Note that this function was written for the question, which was based on SQL Server 2008 and comma as the delimiter. In SQL Server 2016 and above (and in compatibility level 130 and above), STRING_SPLIT() is a better option.
CREATE FUNCTION dbo.SplitString
(
#List nvarchar(max),
#Delim nvarchar(255)
)
RETURNS TABLE
AS
RETURN ( SELECT [Value] FROM
(
SELECT [Value] = LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delim, #List + #Delim, [Number]) - [Number])))
FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
FROM sys.all_columns) AS x WHERE Number <= LEN(#List)
AND SUBSTRING(#Delim + #List, [Number], DATALENGTH(#Delim)/2) = #Delim
) AS y
);
GO
If you want to avoid the limitation of the length of the string being <= the number of rows in sys.all_columns (9,980 in model in SQL Server 2017; much higher in your own user databases), you can use other approaches for deriving the numbers, such as building your own table of numbers. You could also use a recursive CTE in cases where you can't use system tables or create your own:
CREATE FUNCTION dbo.SplitString
(
#List nvarchar(max),
#Delim nvarchar(255)
)
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN ( WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1
FROM n WHERE n <= LEN(#List))
SELECT [Value] = SUBSTRING(#List, n,
CHARINDEX(#Delim, #List + #Delim, n) - n)
FROM n WHERE n <= LEN(#List)
AND SUBSTRING(#Delim + #List, n, DATALENGTH(#Delim)/2) = #Delim
);
GO
But you'll have to append OPTION (MAXRECURSION 0) (or MAXRECURSION <longest possible string length if < 32768>) to the outer query in order to avoid errors with recursion for strings > 100 characters. If that is also not a good alternative then see this answer as pointed out in the comments, or this answer if you need an ordered split string function.
(Also, the delimiter will have to be NCHAR(<=1228). Still researching why.)
More on split functions, why (and proof that) while loops and recursive CTEs don't scale, and better alternatives, if you're splitting strings coming from the application layer:
Splitting strings
Finally the wait is over in SQL Server 2016 they have introduced Split string function : STRING_SPLIT
select * From STRING_SPLIT ('a,b', ',') cs
All the other methods to split string like XML, Tally table, while loop, etc.. has been blown away by this STRING_SPLIT function.
Here is an excellent article with performance comparison : Performance Surprises and Assumptions : STRING_SPLIT
The easiest way to do this is by using XML format.
1. Converting string to rows without table
QUERY
DECLARE #String varchar(100) = 'String1,String2,String3'
-- To change ',' to any other delimeter, just change ',' to your desired one
DECLARE #Delimiter CHAR = ','
SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value'
FROM
(
SELECT CAST ('<M>' + REPLACE(#String, #Delimiter, '</M><M>') + '</M>' AS XML) AS Data
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
RESULT
x---------x
| Value |
x---------x
| String1 |
| String2 |
| String3 |
x---------x
2. Converting to rows from a table which have an ID for each CSV row
SOURCE TABLE
x-----x--------------------------x
| Id | Value |
x-----x--------------------------x
| 1 | String1,String2,String3 |
| 2 | String4,String5,String6 |
x-----x--------------------------x
QUERY
-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
DECLARE #Delimiter CHAR = ','
SELECT ID,LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value'
FROM
(
SELECT ID,CAST ('<M>' + REPLACE(VALUE, #Delimiter, '</M><M>') + '</M>' AS XML) AS Data
FROM TABLENAME
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
RESULT
x-----x----------x
| Id | Value |
x-----x----------x
| 1 | String1 |
| 1 | String2 |
| 1 | String3 |
| 2 | String4 |
| 2 | String5 |
| 2 | String6 |
x-----x----------x
I needed a quick way to get rid of the +4 from a zip code.
UPDATE #Emails
SET ZIPCode = SUBSTRING(ZIPCode, 1, (CHARINDEX('-', ZIPCODE)-1))
WHERE ZIPCode LIKE '%-%'
No proc... no UDF... just one tight little inline command that does what it must. Not fancy, not elegant.
Change the delimiter as needed, etc, and it will work for anything.
if you replace
WHILE CHARINDEX(',', #stringToSplit) > 0
with
WHILE LEN(#stringToSplit) > 0
you can eliminate that last insert after the while loop!
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE LEN(#stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
if #pos = 0
SELECT #pos = LEN(#stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
RETURN
END
The often used approach with XML elements breaks in case of forbidden characters. This is an approach to use this method with any kind of character, even with the semicolon as delimiter.
The trick is, first to use SELECT SomeString AS [*] FOR XML PATH('') to get all forbidden characters properly escaped. That's the reason, why I replace the delimiter to a magic value to avoid troubles with ; as delimiter.
DECLARE #Dummy TABLE (ID INT, SomeTextToSplit NVARCHAR(MAX))
INSERT INTO #Dummy VALUES
(1,N'A&B;C;D;E, F')
,(2,N'"C" & ''D'';<C>;D;E, F');
DECLARE #Delimiter NVARCHAR(10)=';'; --special effort needed (due to entities coding with "&code;")!
WITH Casted AS
(
SELECT *
,CAST(N'<x>' + REPLACE((SELECT REPLACE(SomeTextToSplit,#Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
FROM #Dummy
)
SELECT Casted.ID
,x.value(N'.',N'nvarchar(max)') AS Part
FROM Casted
CROSS APPLY SplitMe.nodes(N'/x') AS A(x)
The result
ID Part
1 A&B
1 C
1 D
1 E, F
2 "C" & 'D'
2 <C>
2 D
2 E, F
All the functions for string splitting that use some kind of Loop-ing (iterations) have bad performance. They should be replaced with set-based solution.
This code executes excellent.
CREATE FUNCTION dbo.SplitStrings
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
I had to write something like this recently. Here's the solution I came up with. It's generalized for any delimiter string and I think it would perform slightly better:
CREATE FUNCTION [dbo].[SplitString]
( #string nvarchar(4000)
, #delim nvarchar(100) )
RETURNS
#result TABLE
( [Value] nvarchar(4000) NOT NULL
, [Index] int NOT NULL )
AS
BEGIN
DECLARE #str nvarchar(4000)
, #pos int
, #prv int = 1
SELECT #pos = CHARINDEX(#delim, #string)
WHILE #pos > 0
BEGIN
SELECT #str = SUBSTRING(#string, #prv, #pos - #prv)
INSERT INTO #result SELECT #str, #prv
SELECT #prv = #pos + LEN(#delim)
, #pos = CHARINDEX(#delim, #string, #pos + 1)
END
INSERT INTO #result SELECT SUBSTRING(#string, #prv, 4000), #prv
RETURN
END
If you need a quick ad-hoc solution for common cases with minimum code, then this recursive CTE two-liner will do it:
DECLARE #s VARCHAR(200) = ',1,2,,3,,,4,,,,5,'
;WITH
a AS (SELECT i=-1, j=0 UNION ALL SELECT j, CHARINDEX(',', #s, j + 1) FROM a WHERE j > i),
b AS (SELECT SUBSTRING(#s, i+1, IIF(j>0, j, LEN(#s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b
Either use this as a stand-alone statement or just add the above CTEs to any of your queries and you will be able to join the resulting table b with others for use in any further expressions.
edit (by Shnugo)
If you add a counter, you will get a position index together with the List:
DECLARE #s VARCHAR(200) = '1,2333,344,4'
;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(',', #s, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(#s, i+1, IIF(j>0, j, LEN(#s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;
The result:
n s
1 1
2 2333
3 344
4 4
I take the xml route by wrapping the values into elements (M but anything works):
declare #v nvarchar(max) = '100,201,abcde'
select
a.value('.', 'varchar(max)')
from
(select cast('<M>' + REPLACE(#v, ',', '</M><M>') + '</M>' AS XML) as col) as A
CROSS APPLY A.col.nodes ('/M') AS Split(a)
A solution using a CTE, if anyone should need that (apart from me, who obviously did, that is why I wrote it).
declare #StringToSplit varchar(100) = 'Test1,Test2,Test3';
declare #SplitChar varchar(10) = ',';
with StringToSplit as (
select
ltrim( rtrim( substring( #StringToSplit, 1, charindex( #SplitChar, #StringToSplit ) - 1 ) ) ) Head
, substring( #StringToSplit, charindex( #SplitChar, #StringToSplit ) + 1, len( #StringToSplit ) ) Tail
union all
select
ltrim( rtrim( substring( Tail, 1, charindex( #SplitChar, Tail ) - 1 ) ) ) Head
, substring( Tail, charindex( #SplitChar, Tail ) + 1, len( Tail ) ) Tail
from StringToSplit
where charindex( #SplitChar, Tail ) > 0
union all
select
ltrim( rtrim( Tail ) ) Head
, '' Tail
from StringToSplit
where charindex( #SplitChar, Tail ) = 0
and len( Tail ) > 0
)
select Head from StringToSplit
This is more narrowly-tailored. When I do this I usually have a comma-delimited list of unique ids (INT or BIGINT), which I want to cast as a table to use as an inner join to another table that has a primary key of INT or BIGINT. I want an in-line table-valued function returned so that I have the most efficient join possible.
Sample usage would be:
DECLARE #IDs VARCHAR(1000);
SET #IDs = ',99,206,124,8967,1,7,3,45234,2,889,987979,';
SELECT me.Value
FROM dbo.MyEnum me
INNER JOIN dbo.GetIntIdsTableFromDelimitedString(#IDs) ids ON me.PrimaryKey = ids.ID
I stole the idea from http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html, changing it to be in-line table-valued and cast as INT.
create function dbo.GetIntIDTableFromDelimitedString
(
#IDs VARCHAR(1000) --this parameter must start and end with a comma, eg ',123,456,'
--all items in list must be perfectly formatted or function will error
)
RETURNS TABLE AS
RETURN
SELECT
CAST(SUBSTRING(#IDs,Nums.number + 1,CHARINDEX(',',#IDs,(Nums.number+2)) - Nums.number - 1) AS INT) AS ID
FROM
[master].[dbo].[spt_values] Nums
WHERE Nums.Type = 'P'
AND Nums.number BETWEEN 1 AND DATALENGTH(#IDs)
AND SUBSTRING(#IDs,Nums.number,1) = ','
AND CHARINDEX(',',#IDs,(Nums.number+1)) > Nums.number;
GO
There is a correct version on here but I thought it would be nice to add a little fault tolerance in case they have a trailing comma as well as make it so you could use it not as a function but as part of a larger piece of code. Just in case you're only using it once time and don't need a function. This is also for integers (which is what I needed it for) so you might have to change your data types.
DECLARE #StringToSeperate VARCHAR(10)
SET #StringToSeperate = '1,2,5'
--SELECT #StringToSeperate IDs INTO #Test
DROP TABLE #IDs
CREATE TABLE #IDs (ID int)
DECLARE #CommaSeperatedValue NVARCHAR(255) = ''
DECLARE #Position INT = LEN(#StringToSeperate)
--Add Each Value
WHILE CHARINDEX(',', #StringToSeperate) > 0
BEGIN
SELECT #Position = CHARINDEX(',', #StringToSeperate)
SELECT #CommaSeperatedValue = SUBSTRING(#StringToSeperate, 1, #Position-1)
INSERT INTO #IDs
SELECT #CommaSeperatedValue
SELECT #StringToSeperate = SUBSTRING(#StringToSeperate, #Position+1, LEN(#StringToSeperate)-#Position)
END
--Add Last Value
IF (LEN(LTRIM(RTRIM(#StringToSeperate)))>0)
BEGIN
INSERT INTO #IDs
SELECT SUBSTRING(#StringToSeperate, 1, #Position)
END
SELECT * FROM #IDs
I modified +Andy Robinson's function a little bit. Now you can select only required part from returning table:
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([numOrder] [tinyint] , [Name] [nvarchar] (500)) AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
DECLARE #orderNum INT
SET #orderNum=0
WHILE CHARINDEX('.', #stringToSplit) > 0
BEGIN
SELECT #orderNum=#orderNum+1;
SELECT #pos = CHARINDEX('.', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #orderNum,#name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
SELECT #orderNum=#orderNum+1;
INSERT INTO #returnList
SELECT #orderNum, #stringToSplit
RETURN
END
Usage:
SELECT Name FROM dbo.splitstring('ELIS.YD.CRP1.1.CBA.MDSP.T389.BT') WHERE numOrder=5
Simples
DECLARE #String varchar(100) = '11,21,84,85,87'
SELECT * FROM TB_PAPEL WHERE CD_PAPEL IN (SELECT value FROM STRING_SPLIT(#String, ','))
-- EQUIVALENTE
SELECT * FROM TB_PAPEL WHERE CD_PAPEL IN (11,21,84,85,87)
here is a version that can split on a pattern using patindex, a simple adaptation of the post above. I had a case where I needed to split a string that contained multiple separator chars.
alter FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(1000), #splitPattern varchar(10) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE PATINDEX(#splitPattern, #stringToSplit) > 0
BEGIN
SELECT #pos = PATINDEX(#splitPattern, #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
select * from dbo.splitstring('stringa/stringb/x,y,z','%[/,]%');
result looks like this
stringa
stringb
x
y
z
Personnaly I use this function :
ALTER FUNCTION [dbo].[CUST_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)-stpos)
FROM Split
)
I have developed a double Splitter (Takes two split characters) as requested Here. Could be of some value in this thread seeing its the most referenced for queries relating to string splitting.
CREATE FUNCTION uft_DoubleSplitter
(
-- Add the parameters for the function here
#String VARCHAR(4000),
#Splitter1 CHAR,
#Splitter2 CHAR
)
RETURNS #Result TABLE (Id INT,MId INT,SValue VARCHAR(4000))
AS
BEGIN
DECLARE #FResult TABLE(Id INT IDENTITY(1, 1),
SValue VARCHAR(4000))
DECLARE #SResult TABLE(Id INT IDENTITY(1, 1),
MId INT,
SValue VARCHAR(4000))
SET #String = #String+#Splitter1
WHILE CHARINDEX(#Splitter1, #String) > 0
BEGIN
DECLARE #WorkingString VARCHAR(4000) = NULL
SET #WorkingString = SUBSTRING(#String, 1, CHARINDEX(#Splitter1, #String) - 1)
--Print #workingString
INSERT INTO #FResult
SELECT CASE
WHEN #WorkingString = '' THEN NULL
ELSE #WorkingString
END
SET #String = SUBSTRING(#String, LEN(#WorkingString) + 2, LEN(#String))
END
IF ISNULL(#Splitter2, '') != ''
BEGIN
DECLARE #OStartLoop INT
DECLARE #OEndLoop INT
SELECT #OStartLoop = MIN(Id),
#OEndLoop = MAX(Id)
FROM #FResult
WHILE #OStartLoop <= #OEndLoop
BEGIN
DECLARE #iString VARCHAR(4000)
DECLARE #iMId INT
SELECT #iString = SValue+#Splitter2,
#iMId = Id
FROM #FResult
WHERE Id = #OStartLoop
WHILE CHARINDEX(#Splitter2, #iString) > 0
BEGIN
DECLARE #iWorkingString VARCHAR(4000) = NULL
SET #IWorkingString = SUBSTRING(#iString, 1, CHARINDEX(#Splitter2, #iString) - 1)
INSERT INTO #SResult
SELECT #iMId,
CASE
WHEN #iWorkingString = '' THEN NULL
ELSE #iWorkingString
END
SET #iString = SUBSTRING(#iString, LEN(#iWorkingString) + 2, LEN(#iString))
END
SET #OStartLoop = #OStartLoop + 1
END
INSERT INTO #Result
SELECT MId AS PrimarySplitID,
ROW_NUMBER() OVER (PARTITION BY MId ORDER BY Mid, Id) AS SecondarySplitID ,
SValue
FROM #SResult
END
ELSE
BEGIN
INSERT INTO #Result
SELECT Id AS PrimarySplitID,
NULL AS SecondarySplitID,
SValue
FROM #FResult
END
RETURN
Usage:
--FirstSplit
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&',NULL)
--Second Split
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&','=')
Possible Usage (Get second value of each split):
SELECT fn.SValue
FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===', '&', '=')AS fn
WHERE fn.mid = 2
A recursive cte based solution
declare #T table (iden int identity, col1 varchar(100));
insert into #T(col1) values
('ROOT/South America/Lima/Test/Test2')
, ('ROOT/South America/Peru/Test/Test2')
, ('ROOT//South America/Venuzuala ')
, ('RtT/South America / ')
, ('ROOT/South Americas// ');
declare #split char(1) = '/';
select #split as split;
with cte as
( select t.iden, case when SUBSTRING(REVERSE(rtrim(t.col1)), 1, 1) = #split then LTRIM(RTRIM(t.col1)) else LTRIM(RTRIM(t.col1)) + #split end as col1, 0 as pos , 1 as cnt
from #T t
union all
select t.iden, t.col1 , charindex(#split, t.col1, t.pos + 1), cnt + 1
from cte t
where charindex(#split, t.col1, t.pos + 1) > 0
)
select t1.*, t2.pos, t2.cnt
, ltrim(rtrim(SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1))) as bingo
from cte t1
join cte t2
on t2.iden = t1.iden
and t2.cnt = t1.cnt+1
and t2.pos > t1.pos
order by t1.iden, t1.cnt;
With all due respect to #AviG this is the bug free version of function deviced by him to return all the tokens in full.
IF EXISTS (SELECT * FROM sys.objects WHERE type = 'TF' AND name = 'TF_SplitString')
DROP FUNCTION [dbo].[TF_SplitString]
GO
-- =============================================
-- Author: AviG
-- Amendments: Parameterize the delimeter and included the missing chars in last token - Gemunu Wickremasinghe
-- Description: Tabel valued function that Breaks the delimeted string by given delimeter and returns a tabel having split results
-- Usage
-- select * from [dbo].[TF_SplitString]('token1,token2,,,,,,,,token969',',')
-- 969 items should be returned
-- select * from [dbo].[TF_SplitString]('4672978261,4672978255',',')
-- 2 items should be returned
-- =============================================
CREATE FUNCTION dbo.TF_SplitString
( #stringToSplit VARCHAR(MAX) ,
#delimeter char = ','
)
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE LEN(#stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(#delimeter, #stringToSplit)
if #pos = 0
BEGIN
SELECT #pos = LEN(#stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos)
END
else
BEGIN
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
END
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
RETURN
END
This is based on Andy Robertson's answer, I needed a delimiter other than comma.
CREATE FUNCTION dbo.splitstring ( #stringToSplit nvarchar(MAX), #delim nvarchar(max))
RETURNS
#returnList TABLE ([value] [nvarchar] (MAX))
AS
BEGIN
DECLARE #value NVARCHAR(max)
DECLARE #pos INT
WHILE CHARINDEX(#delim, #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(#delim, #stringToSplit)
SELECT #value = SUBSTRING(#stringToSplit, 1, #pos - 1)
INSERT INTO #returnList
SELECT #value
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos + LEN(#delim), LEN(#stringToSplit) - #pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
GO
And to use it:
SELECT * FROM dbo.splitstring('test1 test2 test3', ' ');
(Tested on SQL Server 2008 R2)
EDIT: correct test code
ALTER FUNCTION [dbo].func_split_string
(
#input as varchar(max),
#delimiter as varchar(10) = ";"
)
RETURNS #result TABLE
(
id smallint identity(1,1),
csv_value varchar(max) not null
)
AS
BEGIN
DECLARE #pos AS INT;
DECLARE #string AS VARCHAR(MAX) = '';
WHILE LEN(#input) > 0
BEGIN
SELECT #pos = CHARINDEX(#delimiter,#input);
IF(#pos<=0)
select #pos = len(#input)
IF(#pos <> LEN(#input))
SELECT #string = SUBSTRING(#input, 1, #pos-1);
ELSE
SELECT #string = SUBSTRING(#input, 1, #pos);
INSERT INTO #result SELECT #string
SELECT #input = SUBSTRING(#input, #pos+len(#delimiter), LEN(#input)-#pos)
END
RETURN
END
You can Use this function:
CREATE FUNCTION SplitString
(
#Input NVARCHAR(MAX),
#Character CHAR(1)
)
RETURNS #Output TABLE (
Item NVARCHAR(1000)
)
AS
BEGIN
DECLARE #StartIndex INT, #EndIndex INT
SET #StartIndex = 1
IF SUBSTRING(#Input, LEN(#Input) - 1, LEN(#Input)) <> #Character
BEGIN
SET #Input = #Input + #Character
END
WHILE CHARINDEX(#Character, #Input) > 0
BEGIN
SET #EndIndex = CHARINDEX(#Character, #Input)
INSERT INTO #Output(Item)
SELECT SUBSTRING(#Input, #StartIndex, #EndIndex - 1)
SET #Input = SUBSTRING(#Input, #EndIndex + 1, LEN(#Input))
END
RETURN
END
GO
Here is an example that you can use as function or also you can put the same logic in procedure.
--SELECT * from [dbo].fn_SplitString ;
CREATE FUNCTION [dbo].[fn_SplitString]
(#CSV VARCHAR(MAX), #Delimeter VARCHAR(100) = ',')
RETURNS #retTable TABLE
(
[value] VARCHAR(MAX) NULL
)AS
BEGIN
DECLARE
#vCSV VARCHAR (MAX) = #CSV,
#vDelimeter VARCHAR (100) = #Delimeter;
IF #vDelimeter = ';'
BEGIN
SET #vCSV = REPLACE(#vCSV, ';', '~!~#~');
SET #vDelimeter = REPLACE(#vDelimeter, ';', '~!~#~');
END;
SET #vCSV = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(#vCSV, '&', '&'), '<', '<'), '>', '>'), '''', '&apos;'), '"', '"');
DECLARE #xml XML;
SET #xml = '<i>' + REPLACE(#vCSV, #vDelimeter, '</i><i>') + '</i>';
INSERT INTO #retTable
SELECT
x.i.value('.', 'varchar(max)') AS COLUMNNAME
FROM #xml.nodes('//i')AS x(i);
RETURN;
END;
/*
Answer to T-SQL split string
Based on answers from Andy Robinson and AviG
Enhanced functionality ref: LEN function not including trailing spaces in SQL Server
This 'file' should be valid as both a markdown file and an SQL file
*/
CREATE FUNCTION dbo.splitstring ( --CREATE OR ALTER
#stringToSplit NVARCHAR(MAX)
) RETURNS #returnList TABLE ([Item] NVARCHAR (MAX))
AS BEGIN
DECLARE #name NVARCHAR(MAX)
DECLARE #pos BIGINT
SET #stringToSplit = #stringToSplit + ',' -- this should allow entries that end with a `,` to have a blank value in that "column"
WHILE ((LEN(#stringToSplit+'_') > 1)) BEGIN -- `+'_'` gets around LEN trimming terminal spaces. See URL referenced above
SET #pos = COALESCE(NULLIF(CHARINDEX(',', #stringToSplit),0),LEN(#stringToSplit+'_')) -- COALESCE grabs first non-null value
SET #name = SUBSTRING(#stringToSplit, 1, #pos-1) --MAX size of string of type nvarchar is 4000
SET #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, 4000) -- With SUBSTRING fn (MS web): "If start is greater than the number of characters in the value expression, a zero-length expression is returned."
INSERT INTO #returnList SELECT #name --additional debugging parameters below can be added
-- + ' pos:' + CAST(#pos as nvarchar) + ' remain:''' + #stringToSplit + '''(' + CAST(LEN(#stringToSplit+'_')-1 as nvarchar) + ')'
END
RETURN
END
GO
/*
Test cases: see URL referenced as "enhanced functionality" above
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,b')
Item | L
--- | ---
a | 1
| 0
b | 1
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,')
Item | L
--- | ---
a | 1
| 0
| 0
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, ')
Item | L
--- | ---
a | 1
| 0
| 1
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, c ')
Item | L
--- | ---
a | 1
| 0
c | 3
*/
The easiest way:
Install SQL Server 2016
Use STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx
It works even in express edition :).

Optimize finding the Nth occurrence of character in string

I wrote a sql server function which returns substring before the Nth occurence of character.
For example,
SELECT dbo.fn_getFirstNthSentence('.', 'hello world.It.is.raining.today', 3)
returns 'hello world.It.Is.' as a result.
The function I wrote looks dirty and slow so I want to optimize it.
Any advice to make it clean is appreciated.
Thank you.
CREATE FUNCTION fn_getFirstNthSentence
(
#TargetStr VARCHAR(MAX) ,
#SearchedStr VARCHAR(8000) ,
#Occurrence INT
)
RETURNS varchar(MAX)
AS
BEGIN
DECLARE #pos INT ,
#counter INT ,
#ret INT;
SET #pos = CHARINDEX(#TargetStr, #SearchedStr);
IF ( #pos = 0 )
RETURN #SearchedStr
SET #counter = 1;
IF #Occurrence = 1
SET #ret = #pos;
ELSE
BEGIN
WHILE ( #counter < #Occurrence )
BEGIN
IF(LEN(#SearchedStr) < #pos + 1)
RETURN #SearchedStr
SELECT #ret = CHARINDEX(#TargetStr, #SearchedStr,
#pos + 1);
IF(#ret = 0)
RETURN #SearchedStr
SET #counter = #counter + 1;
SET #pos = #ret;
END;
END;
RETURN LEFT(#SearchedStr, #ret)
END;
Here is yet another option using a delimited string splitter. The XML method already posted is a good one but this approach does not require a table variable.
This is created as an inline table valued function which should keep the performance really fast.
create function fn_getFirstNthSentence
(
#SearchedStr varchar(100)
, #Occurrence int
, #Delimiter char(1)
) returns table as return
with ParsedValues as
(
select Item
, ItemNumber
from dbo.DelimitedSplit8K(#SearchedStr, #Delimiter)
where ItemNumber <= #Occurrence
)
select top 1 ResultString = STUFF(
(
select #Delimiter + Item
from ParsedValues
order by ItemNumber
for xml path('')), 1,1, '') + #Delimiter
from ParsedValues
This is also using a splitter created by Jeff Moden. It has one feature that none of the other splitter have...a column to indicate which position the value came from. You can find his article an ensuing discussion here. http://www.sqlservercentral.com/articles/Tally+Table/72993/
Then if you want to execute it you can do this quite simply.
declare #String varchar(100) = 'hello world.It.is.raining.today.'
, #Num int = 3
, #Delimiter char(1) = '.'
;
select *
from fn_getFirstNthSentence(#String, #Num, #Delimiter)
If you don't like Jeff Moden's splitter you can find several other options here. http://sqlperformance.com/2012/07/t-sql-queries/split-strings I don't use Moden's for everything but when you need to keep the parsed values in order it is awesome.
--EDIT--
Here is how you could modify this to become a scalar function instead of an inline table valued function. My preference would be to keep the itvf as they are faster and more flexible.
create function fn_getFirstNthSentenceScalar
(
#SearchedStr varchar(100) = 'hello world.It.is.raining.today.this is after 5'
, #Occurrence int = 5
, #Delimiter char(1) = '.'
) returns varchar(max) as begin
declare #RetVal varchar(max);
with ParsedValues as
(
select Item
, ItemNumber
from dbo.DelimitedSplit8K(#SearchedStr, #Delimiter)
where ItemNumber <= #Occurrence
)
select top 1 #RetVal = STUFF(
(
select #Delimiter + Item
from ParsedValues
order by ItemNumber
for xml path('')), 1,1, '') + #Delimiter
from ParsedValues;
return #RetVal
end
--I find these functions to be a mine-field, and at the risk of stepping on a mine I've tried some simplifications - maybe a microscopic improvement in performance
alter FUNCTION fn_getFirstNthSentence
(
#TargetStr VARCHAR(MAX) ,
#SearchedStr VARCHAR(8000) ,
#Occurrence INT
)
RETURNS varchar(MAX)
AS
BEGIN
DECLARE #pos INT ,
#counter INT ;
IF #Occurrence < 1
RETURN NULL;
SELECT #counter = 0, #POS = 1;
WHILE (#counter < #Occurrence AND #POS > 0)
BEGIN
SELECT #POS = CHARINDEX(#TargetStr, #SearchedStr,
#pos + 1);
IF #POS > 0
SET #counter = #counter + 1;
END;
RETURN CASE WHEN #POS > 0 THEN
LEFT(#SearchedStr, #POS)
ELSE
#SearchedStr
END;
END;
Another option is via XML
I can't see your benchmarks, but it is certainly far less code. An added option could be Find the 3rd through 5th occurrence by adding a parameter and changing the Where Seq<=#FindPos to Where Seq Between range1 and range2.
Declare #FindPos int = 3
Declare #String varchar(max) = 'hello world.It.is.raining.today'
Declare #Delim varchar(10) = '.'
Declare #XML xml,#RetVal varchar(max) = ''
Set #XML = Cast('<x>' + Replace(#String,#Delim,'</x><x>')+'</x>' as XML)
Declare #Table table (Seq int identity(1,1),String varchar(max))
Insert Into #Table Select ltrim(rtrim(String.value('.', 'varchar(max)')))+#Delim as value FROM #XML.nodes('x') as T(String)
Select #RetVal=#RetVal + String from #Table Where Seq<=#FindPos Order By Seq
Select #RetVal
Returns
hello world.It.is.
EDIT: If it helps, below is my generic parsing function which returns a
normalized table...
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimeter varchar(10))
--Usage: 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]('id26,id46|id658,id967','|')
-- Select * from [dbo].[udf-Str-Parse]('hello world. It. is. . raining.today','.')
Returns #ReturnTable Table (Key_PS int IDENTITY(1,1), Key_Value varchar(max))
As
Begin
Declare #XML xml;Set #XML = Cast('<x>' + Replace(#String,#Delimeter,'</x><x>')+'</x>' as XML)
Insert Into #ReturnTable Select Key_Value = ltrim(rtrim(String.value('.', 'varchar(max)'))) FROM #XML.nodes('x') as T(String)
Return
End
So for example:
Select * from [dbo].[udf-Str-Parse]('hello world.It.is.raining.today','.')
Returns
Key_PS Key_Value
1 hello world
2 It
3 is
4 raining
5 today

Convert CSV into rows [duplicate]

I have a SQL Server 2008 R2 column containing a string which I need to split by a comma. I have seen many answers on StackOverflow but none of them works in R2. I have made sure I have select permissions on any split function examples. Any help greatly appreciated.
I've used this SQL before which may work for you:-
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE CHARINDEX(',', #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
and to use it:-
SELECT * FROM dbo.splitstring('91,12,65,78,56,789')
Instead of recursive CTEs and while loops, has anyone considered a more set-based approach? Note that this function was written for the question, which was based on SQL Server 2008 and comma as the delimiter. In SQL Server 2016 and above (and in compatibility level 130 and above), STRING_SPLIT() is a better option.
CREATE FUNCTION dbo.SplitString
(
#List nvarchar(max),
#Delim nvarchar(255)
)
RETURNS TABLE
AS
RETURN ( SELECT [Value] FROM
(
SELECT [Value] = LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delim, #List + #Delim, [Number]) - [Number])))
FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
FROM sys.all_columns) AS x WHERE Number <= LEN(#List)
AND SUBSTRING(#Delim + #List, [Number], DATALENGTH(#Delim)/2) = #Delim
) AS y
);
GO
If you want to avoid the limitation of the length of the string being <= the number of rows in sys.all_columns (9,980 in model in SQL Server 2017; much higher in your own user databases), you can use other approaches for deriving the numbers, such as building your own table of numbers. You could also use a recursive CTE in cases where you can't use system tables or create your own:
CREATE FUNCTION dbo.SplitString
(
#List nvarchar(max),
#Delim nvarchar(255)
)
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN ( WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1
FROM n WHERE n <= LEN(#List))
SELECT [Value] = SUBSTRING(#List, n,
CHARINDEX(#Delim, #List + #Delim, n) - n)
FROM n WHERE n <= LEN(#List)
AND SUBSTRING(#Delim + #List, n, DATALENGTH(#Delim)/2) = #Delim
);
GO
But you'll have to append OPTION (MAXRECURSION 0) (or MAXRECURSION <longest possible string length if < 32768>) to the outer query in order to avoid errors with recursion for strings > 100 characters. If that is also not a good alternative then see this answer as pointed out in the comments, or this answer if you need an ordered split string function.
(Also, the delimiter will have to be NCHAR(<=1228). Still researching why.)
More on split functions, why (and proof that) while loops and recursive CTEs don't scale, and better alternatives, if you're splitting strings coming from the application layer:
Splitting strings
Finally the wait is over in SQL Server 2016 they have introduced Split string function : STRING_SPLIT
select * From STRING_SPLIT ('a,b', ',') cs
All the other methods to split string like XML, Tally table, while loop, etc.. has been blown away by this STRING_SPLIT function.
Here is an excellent article with performance comparison : Performance Surprises and Assumptions : STRING_SPLIT
The easiest way to do this is by using XML format.
1. Converting string to rows without table
QUERY
DECLARE #String varchar(100) = 'String1,String2,String3'
-- To change ',' to any other delimeter, just change ',' to your desired one
DECLARE #Delimiter CHAR = ','
SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value'
FROM
(
SELECT CAST ('<M>' + REPLACE(#String, #Delimiter, '</M><M>') + '</M>' AS XML) AS Data
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
RESULT
x---------x
| Value |
x---------x
| String1 |
| String2 |
| String3 |
x---------x
2. Converting to rows from a table which have an ID for each CSV row
SOURCE TABLE
x-----x--------------------------x
| Id | Value |
x-----x--------------------------x
| 1 | String1,String2,String3 |
| 2 | String4,String5,String6 |
x-----x--------------------------x
QUERY
-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
DECLARE #Delimiter CHAR = ','
SELECT ID,LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value'
FROM
(
SELECT ID,CAST ('<M>' + REPLACE(VALUE, #Delimiter, '</M><M>') + '</M>' AS XML) AS Data
FROM TABLENAME
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
RESULT
x-----x----------x
| Id | Value |
x-----x----------x
| 1 | String1 |
| 1 | String2 |
| 1 | String3 |
| 2 | String4 |
| 2 | String5 |
| 2 | String6 |
x-----x----------x
I needed a quick way to get rid of the +4 from a zip code.
UPDATE #Emails
SET ZIPCode = SUBSTRING(ZIPCode, 1, (CHARINDEX('-', ZIPCODE)-1))
WHERE ZIPCode LIKE '%-%'
No proc... no UDF... just one tight little inline command that does what it must. Not fancy, not elegant.
Change the delimiter as needed, etc, and it will work for anything.
if you replace
WHILE CHARINDEX(',', #stringToSplit) > 0
with
WHILE LEN(#stringToSplit) > 0
you can eliminate that last insert after the while loop!
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE LEN(#stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
if #pos = 0
SELECT #pos = LEN(#stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
RETURN
END
The often used approach with XML elements breaks in case of forbidden characters. This is an approach to use this method with any kind of character, even with the semicolon as delimiter.
The trick is, first to use SELECT SomeString AS [*] FOR XML PATH('') to get all forbidden characters properly escaped. That's the reason, why I replace the delimiter to a magic value to avoid troubles with ; as delimiter.
DECLARE #Dummy TABLE (ID INT, SomeTextToSplit NVARCHAR(MAX))
INSERT INTO #Dummy VALUES
(1,N'A&B;C;D;E, F')
,(2,N'"C" & ''D'';<C>;D;E, F');
DECLARE #Delimiter NVARCHAR(10)=';'; --special effort needed (due to entities coding with "&code;")!
WITH Casted AS
(
SELECT *
,CAST(N'<x>' + REPLACE((SELECT REPLACE(SomeTextToSplit,#Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
FROM #Dummy
)
SELECT Casted.ID
,x.value(N'.',N'nvarchar(max)') AS Part
FROM Casted
CROSS APPLY SplitMe.nodes(N'/x') AS A(x)
The result
ID Part
1 A&B
1 C
1 D
1 E, F
2 "C" & 'D'
2 <C>
2 D
2 E, F
All the functions for string splitting that use some kind of Loop-ing (iterations) have bad performance. They should be replaced with set-based solution.
This code executes excellent.
CREATE FUNCTION dbo.SplitStrings
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
I had to write something like this recently. Here's the solution I came up with. It's generalized for any delimiter string and I think it would perform slightly better:
CREATE FUNCTION [dbo].[SplitString]
( #string nvarchar(4000)
, #delim nvarchar(100) )
RETURNS
#result TABLE
( [Value] nvarchar(4000) NOT NULL
, [Index] int NOT NULL )
AS
BEGIN
DECLARE #str nvarchar(4000)
, #pos int
, #prv int = 1
SELECT #pos = CHARINDEX(#delim, #string)
WHILE #pos > 0
BEGIN
SELECT #str = SUBSTRING(#string, #prv, #pos - #prv)
INSERT INTO #result SELECT #str, #prv
SELECT #prv = #pos + LEN(#delim)
, #pos = CHARINDEX(#delim, #string, #pos + 1)
END
INSERT INTO #result SELECT SUBSTRING(#string, #prv, 4000), #prv
RETURN
END
If you need a quick ad-hoc solution for common cases with minimum code, then this recursive CTE two-liner will do it:
DECLARE #s VARCHAR(200) = ',1,2,,3,,,4,,,,5,'
;WITH
a AS (SELECT i=-1, j=0 UNION ALL SELECT j, CHARINDEX(',', #s, j + 1) FROM a WHERE j > i),
b AS (SELECT SUBSTRING(#s, i+1, IIF(j>0, j, LEN(#s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b
Either use this as a stand-alone statement or just add the above CTEs to any of your queries and you will be able to join the resulting table b with others for use in any further expressions.
edit (by Shnugo)
If you add a counter, you will get a position index together with the List:
DECLARE #s VARCHAR(200) = '1,2333,344,4'
;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(',', #s, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(#s, i+1, IIF(j>0, j, LEN(#s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;
The result:
n s
1 1
2 2333
3 344
4 4
I take the xml route by wrapping the values into elements (M but anything works):
declare #v nvarchar(max) = '100,201,abcde'
select
a.value('.', 'varchar(max)')
from
(select cast('<M>' + REPLACE(#v, ',', '</M><M>') + '</M>' AS XML) as col) as A
CROSS APPLY A.col.nodes ('/M') AS Split(a)
A solution using a CTE, if anyone should need that (apart from me, who obviously did, that is why I wrote it).
declare #StringToSplit varchar(100) = 'Test1,Test2,Test3';
declare #SplitChar varchar(10) = ',';
with StringToSplit as (
select
ltrim( rtrim( substring( #StringToSplit, 1, charindex( #SplitChar, #StringToSplit ) - 1 ) ) ) Head
, substring( #StringToSplit, charindex( #SplitChar, #StringToSplit ) + 1, len( #StringToSplit ) ) Tail
union all
select
ltrim( rtrim( substring( Tail, 1, charindex( #SplitChar, Tail ) - 1 ) ) ) Head
, substring( Tail, charindex( #SplitChar, Tail ) + 1, len( Tail ) ) Tail
from StringToSplit
where charindex( #SplitChar, Tail ) > 0
union all
select
ltrim( rtrim( Tail ) ) Head
, '' Tail
from StringToSplit
where charindex( #SplitChar, Tail ) = 0
and len( Tail ) > 0
)
select Head from StringToSplit
This is more narrowly-tailored. When I do this I usually have a comma-delimited list of unique ids (INT or BIGINT), which I want to cast as a table to use as an inner join to another table that has a primary key of INT or BIGINT. I want an in-line table-valued function returned so that I have the most efficient join possible.
Sample usage would be:
DECLARE #IDs VARCHAR(1000);
SET #IDs = ',99,206,124,8967,1,7,3,45234,2,889,987979,';
SELECT me.Value
FROM dbo.MyEnum me
INNER JOIN dbo.GetIntIdsTableFromDelimitedString(#IDs) ids ON me.PrimaryKey = ids.ID
I stole the idea from http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html, changing it to be in-line table-valued and cast as INT.
create function dbo.GetIntIDTableFromDelimitedString
(
#IDs VARCHAR(1000) --this parameter must start and end with a comma, eg ',123,456,'
--all items in list must be perfectly formatted or function will error
)
RETURNS TABLE AS
RETURN
SELECT
CAST(SUBSTRING(#IDs,Nums.number + 1,CHARINDEX(',',#IDs,(Nums.number+2)) - Nums.number - 1) AS INT) AS ID
FROM
[master].[dbo].[spt_values] Nums
WHERE Nums.Type = 'P'
AND Nums.number BETWEEN 1 AND DATALENGTH(#IDs)
AND SUBSTRING(#IDs,Nums.number,1) = ','
AND CHARINDEX(',',#IDs,(Nums.number+1)) > Nums.number;
GO
There is a correct version on here but I thought it would be nice to add a little fault tolerance in case they have a trailing comma as well as make it so you could use it not as a function but as part of a larger piece of code. Just in case you're only using it once time and don't need a function. This is also for integers (which is what I needed it for) so you might have to change your data types.
DECLARE #StringToSeperate VARCHAR(10)
SET #StringToSeperate = '1,2,5'
--SELECT #StringToSeperate IDs INTO #Test
DROP TABLE #IDs
CREATE TABLE #IDs (ID int)
DECLARE #CommaSeperatedValue NVARCHAR(255) = ''
DECLARE #Position INT = LEN(#StringToSeperate)
--Add Each Value
WHILE CHARINDEX(',', #StringToSeperate) > 0
BEGIN
SELECT #Position = CHARINDEX(',', #StringToSeperate)
SELECT #CommaSeperatedValue = SUBSTRING(#StringToSeperate, 1, #Position-1)
INSERT INTO #IDs
SELECT #CommaSeperatedValue
SELECT #StringToSeperate = SUBSTRING(#StringToSeperate, #Position+1, LEN(#StringToSeperate)-#Position)
END
--Add Last Value
IF (LEN(LTRIM(RTRIM(#StringToSeperate)))>0)
BEGIN
INSERT INTO #IDs
SELECT SUBSTRING(#StringToSeperate, 1, #Position)
END
SELECT * FROM #IDs
I modified +Andy Robinson's function a little bit. Now you can select only required part from returning table:
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([numOrder] [tinyint] , [Name] [nvarchar] (500)) AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
DECLARE #orderNum INT
SET #orderNum=0
WHILE CHARINDEX('.', #stringToSplit) > 0
BEGIN
SELECT #orderNum=#orderNum+1;
SELECT #pos = CHARINDEX('.', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #orderNum,#name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
SELECT #orderNum=#orderNum+1;
INSERT INTO #returnList
SELECT #orderNum, #stringToSplit
RETURN
END
Usage:
SELECT Name FROM dbo.splitstring('ELIS.YD.CRP1.1.CBA.MDSP.T389.BT') WHERE numOrder=5
Simples
DECLARE #String varchar(100) = '11,21,84,85,87'
SELECT * FROM TB_PAPEL WHERE CD_PAPEL IN (SELECT value FROM STRING_SPLIT(#String, ','))
-- EQUIVALENTE
SELECT * FROM TB_PAPEL WHERE CD_PAPEL IN (11,21,84,85,87)
here is a version that can split on a pattern using patindex, a simple adaptation of the post above. I had a case where I needed to split a string that contained multiple separator chars.
alter FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(1000), #splitPattern varchar(10) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE PATINDEX(#splitPattern, #stringToSplit) > 0
BEGIN
SELECT #pos = PATINDEX(#splitPattern, #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
select * from dbo.splitstring('stringa/stringb/x,y,z','%[/,]%');
result looks like this
stringa
stringb
x
y
z
Personnaly I use this function :
ALTER FUNCTION [dbo].[CUST_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)-stpos)
FROM Split
)
I have developed a double Splitter (Takes two split characters) as requested Here. Could be of some value in this thread seeing its the most referenced for queries relating to string splitting.
CREATE FUNCTION uft_DoubleSplitter
(
-- Add the parameters for the function here
#String VARCHAR(4000),
#Splitter1 CHAR,
#Splitter2 CHAR
)
RETURNS #Result TABLE (Id INT,MId INT,SValue VARCHAR(4000))
AS
BEGIN
DECLARE #FResult TABLE(Id INT IDENTITY(1, 1),
SValue VARCHAR(4000))
DECLARE #SResult TABLE(Id INT IDENTITY(1, 1),
MId INT,
SValue VARCHAR(4000))
SET #String = #String+#Splitter1
WHILE CHARINDEX(#Splitter1, #String) > 0
BEGIN
DECLARE #WorkingString VARCHAR(4000) = NULL
SET #WorkingString = SUBSTRING(#String, 1, CHARINDEX(#Splitter1, #String) - 1)
--Print #workingString
INSERT INTO #FResult
SELECT CASE
WHEN #WorkingString = '' THEN NULL
ELSE #WorkingString
END
SET #String = SUBSTRING(#String, LEN(#WorkingString) + 2, LEN(#String))
END
IF ISNULL(#Splitter2, '') != ''
BEGIN
DECLARE #OStartLoop INT
DECLARE #OEndLoop INT
SELECT #OStartLoop = MIN(Id),
#OEndLoop = MAX(Id)
FROM #FResult
WHILE #OStartLoop <= #OEndLoop
BEGIN
DECLARE #iString VARCHAR(4000)
DECLARE #iMId INT
SELECT #iString = SValue+#Splitter2,
#iMId = Id
FROM #FResult
WHERE Id = #OStartLoop
WHILE CHARINDEX(#Splitter2, #iString) > 0
BEGIN
DECLARE #iWorkingString VARCHAR(4000) = NULL
SET #IWorkingString = SUBSTRING(#iString, 1, CHARINDEX(#Splitter2, #iString) - 1)
INSERT INTO #SResult
SELECT #iMId,
CASE
WHEN #iWorkingString = '' THEN NULL
ELSE #iWorkingString
END
SET #iString = SUBSTRING(#iString, LEN(#iWorkingString) + 2, LEN(#iString))
END
SET #OStartLoop = #OStartLoop + 1
END
INSERT INTO #Result
SELECT MId AS PrimarySplitID,
ROW_NUMBER() OVER (PARTITION BY MId ORDER BY Mid, Id) AS SecondarySplitID ,
SValue
FROM #SResult
END
ELSE
BEGIN
INSERT INTO #Result
SELECT Id AS PrimarySplitID,
NULL AS SecondarySplitID,
SValue
FROM #FResult
END
RETURN
Usage:
--FirstSplit
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&',NULL)
--Second Split
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&','=')
Possible Usage (Get second value of each split):
SELECT fn.SValue
FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===', '&', '=')AS fn
WHERE fn.mid = 2
A recursive cte based solution
declare #T table (iden int identity, col1 varchar(100));
insert into #T(col1) values
('ROOT/South America/Lima/Test/Test2')
, ('ROOT/South America/Peru/Test/Test2')
, ('ROOT//South America/Venuzuala ')
, ('RtT/South America / ')
, ('ROOT/South Americas// ');
declare #split char(1) = '/';
select #split as split;
with cte as
( select t.iden, case when SUBSTRING(REVERSE(rtrim(t.col1)), 1, 1) = #split then LTRIM(RTRIM(t.col1)) else LTRIM(RTRIM(t.col1)) + #split end as col1, 0 as pos , 1 as cnt
from #T t
union all
select t.iden, t.col1 , charindex(#split, t.col1, t.pos + 1), cnt + 1
from cte t
where charindex(#split, t.col1, t.pos + 1) > 0
)
select t1.*, t2.pos, t2.cnt
, ltrim(rtrim(SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1))) as bingo
from cte t1
join cte t2
on t2.iden = t1.iden
and t2.cnt = t1.cnt+1
and t2.pos > t1.pos
order by t1.iden, t1.cnt;
With all due respect to #AviG this is the bug free version of function deviced by him to return all the tokens in full.
IF EXISTS (SELECT * FROM sys.objects WHERE type = 'TF' AND name = 'TF_SplitString')
DROP FUNCTION [dbo].[TF_SplitString]
GO
-- =============================================
-- Author: AviG
-- Amendments: Parameterize the delimeter and included the missing chars in last token - Gemunu Wickremasinghe
-- Description: Tabel valued function that Breaks the delimeted string by given delimeter and returns a tabel having split results
-- Usage
-- select * from [dbo].[TF_SplitString]('token1,token2,,,,,,,,token969',',')
-- 969 items should be returned
-- select * from [dbo].[TF_SplitString]('4672978261,4672978255',',')
-- 2 items should be returned
-- =============================================
CREATE FUNCTION dbo.TF_SplitString
( #stringToSplit VARCHAR(MAX) ,
#delimeter char = ','
)
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE LEN(#stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(#delimeter, #stringToSplit)
if #pos = 0
BEGIN
SELECT #pos = LEN(#stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos)
END
else
BEGIN
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
END
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
RETURN
END
This is based on Andy Robertson's answer, I needed a delimiter other than comma.
CREATE FUNCTION dbo.splitstring ( #stringToSplit nvarchar(MAX), #delim nvarchar(max))
RETURNS
#returnList TABLE ([value] [nvarchar] (MAX))
AS
BEGIN
DECLARE #value NVARCHAR(max)
DECLARE #pos INT
WHILE CHARINDEX(#delim, #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(#delim, #stringToSplit)
SELECT #value = SUBSTRING(#stringToSplit, 1, #pos - 1)
INSERT INTO #returnList
SELECT #value
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos + LEN(#delim), LEN(#stringToSplit) - #pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
GO
And to use it:
SELECT * FROM dbo.splitstring('test1 test2 test3', ' ');
(Tested on SQL Server 2008 R2)
EDIT: correct test code
ALTER FUNCTION [dbo].func_split_string
(
#input as varchar(max),
#delimiter as varchar(10) = ";"
)
RETURNS #result TABLE
(
id smallint identity(1,1),
csv_value varchar(max) not null
)
AS
BEGIN
DECLARE #pos AS INT;
DECLARE #string AS VARCHAR(MAX) = '';
WHILE LEN(#input) > 0
BEGIN
SELECT #pos = CHARINDEX(#delimiter,#input);
IF(#pos<=0)
select #pos = len(#input)
IF(#pos <> LEN(#input))
SELECT #string = SUBSTRING(#input, 1, #pos-1);
ELSE
SELECT #string = SUBSTRING(#input, 1, #pos);
INSERT INTO #result SELECT #string
SELECT #input = SUBSTRING(#input, #pos+len(#delimiter), LEN(#input)-#pos)
END
RETURN
END
You can Use this function:
CREATE FUNCTION SplitString
(
#Input NVARCHAR(MAX),
#Character CHAR(1)
)
RETURNS #Output TABLE (
Item NVARCHAR(1000)
)
AS
BEGIN
DECLARE #StartIndex INT, #EndIndex INT
SET #StartIndex = 1
IF SUBSTRING(#Input, LEN(#Input) - 1, LEN(#Input)) <> #Character
BEGIN
SET #Input = #Input + #Character
END
WHILE CHARINDEX(#Character, #Input) > 0
BEGIN
SET #EndIndex = CHARINDEX(#Character, #Input)
INSERT INTO #Output(Item)
SELECT SUBSTRING(#Input, #StartIndex, #EndIndex - 1)
SET #Input = SUBSTRING(#Input, #EndIndex + 1, LEN(#Input))
END
RETURN
END
GO
Here is an example that you can use as function or also you can put the same logic in procedure.
--SELECT * from [dbo].fn_SplitString ;
CREATE FUNCTION [dbo].[fn_SplitString]
(#CSV VARCHAR(MAX), #Delimeter VARCHAR(100) = ',')
RETURNS #retTable TABLE
(
[value] VARCHAR(MAX) NULL
)AS
BEGIN
DECLARE
#vCSV VARCHAR (MAX) = #CSV,
#vDelimeter VARCHAR (100) = #Delimeter;
IF #vDelimeter = ';'
BEGIN
SET #vCSV = REPLACE(#vCSV, ';', '~!~#~');
SET #vDelimeter = REPLACE(#vDelimeter, ';', '~!~#~');
END;
SET #vCSV = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(#vCSV, '&', '&'), '<', '<'), '>', '>'), '''', '&apos;'), '"', '"');
DECLARE #xml XML;
SET #xml = '<i>' + REPLACE(#vCSV, #vDelimeter, '</i><i>') + '</i>';
INSERT INTO #retTable
SELECT
x.i.value('.', 'varchar(max)') AS COLUMNNAME
FROM #xml.nodes('//i')AS x(i);
RETURN;
END;
/*
Answer to T-SQL split string
Based on answers from Andy Robinson and AviG
Enhanced functionality ref: LEN function not including trailing spaces in SQL Server
This 'file' should be valid as both a markdown file and an SQL file
*/
CREATE FUNCTION dbo.splitstring ( --CREATE OR ALTER
#stringToSplit NVARCHAR(MAX)
) RETURNS #returnList TABLE ([Item] NVARCHAR (MAX))
AS BEGIN
DECLARE #name NVARCHAR(MAX)
DECLARE #pos BIGINT
SET #stringToSplit = #stringToSplit + ',' -- this should allow entries that end with a `,` to have a blank value in that "column"
WHILE ((LEN(#stringToSplit+'_') > 1)) BEGIN -- `+'_'` gets around LEN trimming terminal spaces. See URL referenced above
SET #pos = COALESCE(NULLIF(CHARINDEX(',', #stringToSplit),0),LEN(#stringToSplit+'_')) -- COALESCE grabs first non-null value
SET #name = SUBSTRING(#stringToSplit, 1, #pos-1) --MAX size of string of type nvarchar is 4000
SET #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, 4000) -- With SUBSTRING fn (MS web): "If start is greater than the number of characters in the value expression, a zero-length expression is returned."
INSERT INTO #returnList SELECT #name --additional debugging parameters below can be added
-- + ' pos:' + CAST(#pos as nvarchar) + ' remain:''' + #stringToSplit + '''(' + CAST(LEN(#stringToSplit+'_')-1 as nvarchar) + ')'
END
RETURN
END
GO
/*
Test cases: see URL referenced as "enhanced functionality" above
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,b')
Item | L
--- | ---
a | 1
| 0
b | 1
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,')
Item | L
--- | ---
a | 1
| 0
| 0
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, ')
Item | L
--- | ---
a | 1
| 0
| 1
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, c ')
Item | L
--- | ---
a | 1
| 0
c | 3
*/
The easiest way:
Install SQL Server 2016
Use STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx
It works even in express edition :).

How to split string a value and parse it to int in SQL Server 2012? [duplicate]

I have a SQL Server 2008 R2 column containing a string which I need to split by a comma. I have seen many answers on StackOverflow but none of them works in R2. I have made sure I have select permissions on any split function examples. Any help greatly appreciated.
I've used this SQL before which may work for you:-
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE CHARINDEX(',', #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
and to use it:-
SELECT * FROM dbo.splitstring('91,12,65,78,56,789')
Instead of recursive CTEs and while loops, has anyone considered a more set-based approach? Note that this function was written for the question, which was based on SQL Server 2008 and comma as the delimiter. In SQL Server 2016 and above (and in compatibility level 130 and above), STRING_SPLIT() is a better option.
CREATE FUNCTION dbo.SplitString
(
#List nvarchar(max),
#Delim nvarchar(255)
)
RETURNS TABLE
AS
RETURN ( SELECT [Value] FROM
(
SELECT [Value] = LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delim, #List + #Delim, [Number]) - [Number])))
FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
FROM sys.all_columns) AS x WHERE Number <= LEN(#List)
AND SUBSTRING(#Delim + #List, [Number], DATALENGTH(#Delim)/2) = #Delim
) AS y
);
GO
If you want to avoid the limitation of the length of the string being <= the number of rows in sys.all_columns (9,980 in model in SQL Server 2017; much higher in your own user databases), you can use other approaches for deriving the numbers, such as building your own table of numbers. You could also use a recursive CTE in cases where you can't use system tables or create your own:
CREATE FUNCTION dbo.SplitString
(
#List nvarchar(max),
#Delim nvarchar(255)
)
RETURNS TABLE WITH SCHEMABINDING
AS
RETURN ( WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1
FROM n WHERE n <= LEN(#List))
SELECT [Value] = SUBSTRING(#List, n,
CHARINDEX(#Delim, #List + #Delim, n) - n)
FROM n WHERE n <= LEN(#List)
AND SUBSTRING(#Delim + #List, n, DATALENGTH(#Delim)/2) = #Delim
);
GO
But you'll have to append OPTION (MAXRECURSION 0) (or MAXRECURSION <longest possible string length if < 32768>) to the outer query in order to avoid errors with recursion for strings > 100 characters. If that is also not a good alternative then see this answer as pointed out in the comments, or this answer if you need an ordered split string function.
(Also, the delimiter will have to be NCHAR(<=1228). Still researching why.)
More on split functions, why (and proof that) while loops and recursive CTEs don't scale, and better alternatives, if you're splitting strings coming from the application layer:
Splitting strings
Finally the wait is over in SQL Server 2016 they have introduced Split string function : STRING_SPLIT
select * From STRING_SPLIT ('a,b', ',') cs
All the other methods to split string like XML, Tally table, while loop, etc.. has been blown away by this STRING_SPLIT function.
Here is an excellent article with performance comparison : Performance Surprises and Assumptions : STRING_SPLIT
The easiest way to do this is by using XML format.
1. Converting string to rows without table
QUERY
DECLARE #String varchar(100) = 'String1,String2,String3'
-- To change ',' to any other delimeter, just change ',' to your desired one
DECLARE #Delimiter CHAR = ','
SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value'
FROM
(
SELECT CAST ('<M>' + REPLACE(#String, #Delimiter, '</M><M>') + '</M>' AS XML) AS Data
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
RESULT
x---------x
| Value |
x---------x
| String1 |
| String2 |
| String3 |
x---------x
2. Converting to rows from a table which have an ID for each CSV row
SOURCE TABLE
x-----x--------------------------x
| Id | Value |
x-----x--------------------------x
| 1 | String1,String2,String3 |
| 2 | String4,String5,String6 |
x-----x--------------------------x
QUERY
-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
DECLARE #Delimiter CHAR = ','
SELECT ID,LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'Value'
FROM
(
SELECT ID,CAST ('<M>' + REPLACE(VALUE, #Delimiter, '</M><M>') + '</M>' AS XML) AS Data
FROM TABLENAME
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
RESULT
x-----x----------x
| Id | Value |
x-----x----------x
| 1 | String1 |
| 1 | String2 |
| 1 | String3 |
| 2 | String4 |
| 2 | String5 |
| 2 | String6 |
x-----x----------x
I needed a quick way to get rid of the +4 from a zip code.
UPDATE #Emails
SET ZIPCode = SUBSTRING(ZIPCode, 1, (CHARINDEX('-', ZIPCODE)-1))
WHERE ZIPCode LIKE '%-%'
No proc... no UDF... just one tight little inline command that does what it must. Not fancy, not elegant.
Change the delimiter as needed, etc, and it will work for anything.
if you replace
WHILE CHARINDEX(',', #stringToSplit) > 0
with
WHILE LEN(#stringToSplit) > 0
you can eliminate that last insert after the while loop!
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE LEN(#stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(',', #stringToSplit)
if #pos = 0
SELECT #pos = LEN(#stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
RETURN
END
The often used approach with XML elements breaks in case of forbidden characters. This is an approach to use this method with any kind of character, even with the semicolon as delimiter.
The trick is, first to use SELECT SomeString AS [*] FOR XML PATH('') to get all forbidden characters properly escaped. That's the reason, why I replace the delimiter to a magic value to avoid troubles with ; as delimiter.
DECLARE #Dummy TABLE (ID INT, SomeTextToSplit NVARCHAR(MAX))
INSERT INTO #Dummy VALUES
(1,N'A&B;C;D;E, F')
,(2,N'"C" & ''D'';<C>;D;E, F');
DECLARE #Delimiter NVARCHAR(10)=';'; --special effort needed (due to entities coding with "&code;")!
WITH Casted AS
(
SELECT *
,CAST(N'<x>' + REPLACE((SELECT REPLACE(SomeTextToSplit,#Delimiter,N'§§Split$me$here§§') AS [*] FOR XML PATH('')),N'§§Split$me$here§§',N'</x><x>') + N'</x>' AS XML) AS SplitMe
FROM #Dummy
)
SELECT Casted.ID
,x.value(N'.',N'nvarchar(max)') AS Part
FROM Casted
CROSS APPLY SplitMe.nodes(N'/x') AS A(x)
The result
ID Part
1 A&B
1 C
1 D
1 E, F
2 "C" & 'D'
2 <C>
2 D
2 E, F
All the functions for string splitting that use some kind of Loop-ing (iterations) have bad performance. They should be replaced with set-based solution.
This code executes excellent.
CREATE FUNCTION dbo.SplitStrings
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
I had to write something like this recently. Here's the solution I came up with. It's generalized for any delimiter string and I think it would perform slightly better:
CREATE FUNCTION [dbo].[SplitString]
( #string nvarchar(4000)
, #delim nvarchar(100) )
RETURNS
#result TABLE
( [Value] nvarchar(4000) NOT NULL
, [Index] int NOT NULL )
AS
BEGIN
DECLARE #str nvarchar(4000)
, #pos int
, #prv int = 1
SELECT #pos = CHARINDEX(#delim, #string)
WHILE #pos > 0
BEGIN
SELECT #str = SUBSTRING(#string, #prv, #pos - #prv)
INSERT INTO #result SELECT #str, #prv
SELECT #prv = #pos + LEN(#delim)
, #pos = CHARINDEX(#delim, #string, #pos + 1)
END
INSERT INTO #result SELECT SUBSTRING(#string, #prv, 4000), #prv
RETURN
END
If you need a quick ad-hoc solution for common cases with minimum code, then this recursive CTE two-liner will do it:
DECLARE #s VARCHAR(200) = ',1,2,,3,,,4,,,,5,'
;WITH
a AS (SELECT i=-1, j=0 UNION ALL SELECT j, CHARINDEX(',', #s, j + 1) FROM a WHERE j > i),
b AS (SELECT SUBSTRING(#s, i+1, IIF(j>0, j, LEN(#s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b
Either use this as a stand-alone statement or just add the above CTEs to any of your queries and you will be able to join the resulting table b with others for use in any further expressions.
edit (by Shnugo)
If you add a counter, you will get a position index together with the List:
DECLARE #s VARCHAR(200) = '1,2333,344,4'
;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(',', #s, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(#s, i+1, IIF(j>0, j, LEN(#s)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;
The result:
n s
1 1
2 2333
3 344
4 4
I take the xml route by wrapping the values into elements (M but anything works):
declare #v nvarchar(max) = '100,201,abcde'
select
a.value('.', 'varchar(max)')
from
(select cast('<M>' + REPLACE(#v, ',', '</M><M>') + '</M>' AS XML) as col) as A
CROSS APPLY A.col.nodes ('/M') AS Split(a)
A solution using a CTE, if anyone should need that (apart from me, who obviously did, that is why I wrote it).
declare #StringToSplit varchar(100) = 'Test1,Test2,Test3';
declare #SplitChar varchar(10) = ',';
with StringToSplit as (
select
ltrim( rtrim( substring( #StringToSplit, 1, charindex( #SplitChar, #StringToSplit ) - 1 ) ) ) Head
, substring( #StringToSplit, charindex( #SplitChar, #StringToSplit ) + 1, len( #StringToSplit ) ) Tail
union all
select
ltrim( rtrim( substring( Tail, 1, charindex( #SplitChar, Tail ) - 1 ) ) ) Head
, substring( Tail, charindex( #SplitChar, Tail ) + 1, len( Tail ) ) Tail
from StringToSplit
where charindex( #SplitChar, Tail ) > 0
union all
select
ltrim( rtrim( Tail ) ) Head
, '' Tail
from StringToSplit
where charindex( #SplitChar, Tail ) = 0
and len( Tail ) > 0
)
select Head from StringToSplit
This is more narrowly-tailored. When I do this I usually have a comma-delimited list of unique ids (INT or BIGINT), which I want to cast as a table to use as an inner join to another table that has a primary key of INT or BIGINT. I want an in-line table-valued function returned so that I have the most efficient join possible.
Sample usage would be:
DECLARE #IDs VARCHAR(1000);
SET #IDs = ',99,206,124,8967,1,7,3,45234,2,889,987979,';
SELECT me.Value
FROM dbo.MyEnum me
INNER JOIN dbo.GetIntIdsTableFromDelimitedString(#IDs) ids ON me.PrimaryKey = ids.ID
I stole the idea from http://sqlrecords.blogspot.com/2012/11/converting-delimited-list-to-table.html, changing it to be in-line table-valued and cast as INT.
create function dbo.GetIntIDTableFromDelimitedString
(
#IDs VARCHAR(1000) --this parameter must start and end with a comma, eg ',123,456,'
--all items in list must be perfectly formatted or function will error
)
RETURNS TABLE AS
RETURN
SELECT
CAST(SUBSTRING(#IDs,Nums.number + 1,CHARINDEX(',',#IDs,(Nums.number+2)) - Nums.number - 1) AS INT) AS ID
FROM
[master].[dbo].[spt_values] Nums
WHERE Nums.Type = 'P'
AND Nums.number BETWEEN 1 AND DATALENGTH(#IDs)
AND SUBSTRING(#IDs,Nums.number,1) = ','
AND CHARINDEX(',',#IDs,(Nums.number+1)) > Nums.number;
GO
There is a correct version on here but I thought it would be nice to add a little fault tolerance in case they have a trailing comma as well as make it so you could use it not as a function but as part of a larger piece of code. Just in case you're only using it once time and don't need a function. This is also for integers (which is what I needed it for) so you might have to change your data types.
DECLARE #StringToSeperate VARCHAR(10)
SET #StringToSeperate = '1,2,5'
--SELECT #StringToSeperate IDs INTO #Test
DROP TABLE #IDs
CREATE TABLE #IDs (ID int)
DECLARE #CommaSeperatedValue NVARCHAR(255) = ''
DECLARE #Position INT = LEN(#StringToSeperate)
--Add Each Value
WHILE CHARINDEX(',', #StringToSeperate) > 0
BEGIN
SELECT #Position = CHARINDEX(',', #StringToSeperate)
SELECT #CommaSeperatedValue = SUBSTRING(#StringToSeperate, 1, #Position-1)
INSERT INTO #IDs
SELECT #CommaSeperatedValue
SELECT #StringToSeperate = SUBSTRING(#StringToSeperate, #Position+1, LEN(#StringToSeperate)-#Position)
END
--Add Last Value
IF (LEN(LTRIM(RTRIM(#StringToSeperate)))>0)
BEGIN
INSERT INTO #IDs
SELECT SUBSTRING(#StringToSeperate, 1, #Position)
END
SELECT * FROM #IDs
I modified +Andy Robinson's function a little bit. Now you can select only required part from returning table:
CREATE FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(MAX) )
RETURNS
#returnList TABLE ([numOrder] [tinyint] , [Name] [nvarchar] (500)) AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
DECLARE #orderNum INT
SET #orderNum=0
WHILE CHARINDEX('.', #stringToSplit) > 0
BEGIN
SELECT #orderNum=#orderNum+1;
SELECT #pos = CHARINDEX('.', #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #orderNum,#name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
SELECT #orderNum=#orderNum+1;
INSERT INTO #returnList
SELECT #orderNum, #stringToSplit
RETURN
END
Usage:
SELECT Name FROM dbo.splitstring('ELIS.YD.CRP1.1.CBA.MDSP.T389.BT') WHERE numOrder=5
Simples
DECLARE #String varchar(100) = '11,21,84,85,87'
SELECT * FROM TB_PAPEL WHERE CD_PAPEL IN (SELECT value FROM STRING_SPLIT(#String, ','))
-- EQUIVALENTE
SELECT * FROM TB_PAPEL WHERE CD_PAPEL IN (11,21,84,85,87)
here is a version that can split on a pattern using patindex, a simple adaptation of the post above. I had a case where I needed to split a string that contained multiple separator chars.
alter FUNCTION dbo.splitstring ( #stringToSplit VARCHAR(1000), #splitPattern varchar(10) )
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE PATINDEX(#splitPattern, #stringToSplit) > 0
BEGIN
SELECT #pos = PATINDEX(#splitPattern, #stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
select * from dbo.splitstring('stringa/stringb/x,y,z','%[/,]%');
result looks like this
stringa
stringb
x
y
z
Personnaly I use this function :
ALTER FUNCTION [dbo].[CUST_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)-stpos)
FROM Split
)
I have developed a double Splitter (Takes two split characters) as requested Here. Could be of some value in this thread seeing its the most referenced for queries relating to string splitting.
CREATE FUNCTION uft_DoubleSplitter
(
-- Add the parameters for the function here
#String VARCHAR(4000),
#Splitter1 CHAR,
#Splitter2 CHAR
)
RETURNS #Result TABLE (Id INT,MId INT,SValue VARCHAR(4000))
AS
BEGIN
DECLARE #FResult TABLE(Id INT IDENTITY(1, 1),
SValue VARCHAR(4000))
DECLARE #SResult TABLE(Id INT IDENTITY(1, 1),
MId INT,
SValue VARCHAR(4000))
SET #String = #String+#Splitter1
WHILE CHARINDEX(#Splitter1, #String) > 0
BEGIN
DECLARE #WorkingString VARCHAR(4000) = NULL
SET #WorkingString = SUBSTRING(#String, 1, CHARINDEX(#Splitter1, #String) - 1)
--Print #workingString
INSERT INTO #FResult
SELECT CASE
WHEN #WorkingString = '' THEN NULL
ELSE #WorkingString
END
SET #String = SUBSTRING(#String, LEN(#WorkingString) + 2, LEN(#String))
END
IF ISNULL(#Splitter2, '') != ''
BEGIN
DECLARE #OStartLoop INT
DECLARE #OEndLoop INT
SELECT #OStartLoop = MIN(Id),
#OEndLoop = MAX(Id)
FROM #FResult
WHILE #OStartLoop <= #OEndLoop
BEGIN
DECLARE #iString VARCHAR(4000)
DECLARE #iMId INT
SELECT #iString = SValue+#Splitter2,
#iMId = Id
FROM #FResult
WHERE Id = #OStartLoop
WHILE CHARINDEX(#Splitter2, #iString) > 0
BEGIN
DECLARE #iWorkingString VARCHAR(4000) = NULL
SET #IWorkingString = SUBSTRING(#iString, 1, CHARINDEX(#Splitter2, #iString) - 1)
INSERT INTO #SResult
SELECT #iMId,
CASE
WHEN #iWorkingString = '' THEN NULL
ELSE #iWorkingString
END
SET #iString = SUBSTRING(#iString, LEN(#iWorkingString) + 2, LEN(#iString))
END
SET #OStartLoop = #OStartLoop + 1
END
INSERT INTO #Result
SELECT MId AS PrimarySplitID,
ROW_NUMBER() OVER (PARTITION BY MId ORDER BY Mid, Id) AS SecondarySplitID ,
SValue
FROM #SResult
END
ELSE
BEGIN
INSERT INTO #Result
SELECT Id AS PrimarySplitID,
NULL AS SecondarySplitID,
SValue
FROM #FResult
END
RETURN
Usage:
--FirstSplit
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&',NULL)
--Second Split
SELECT * FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===','&','=')
Possible Usage (Get second value of each split):
SELECT fn.SValue
FROM uft_DoubleSplitter('ValueA=ValueB=ValueC=ValueD==ValueE&ValueA=ValueB=ValueC===ValueE&ValueA=ValueB==ValueD===', '&', '=')AS fn
WHERE fn.mid = 2
A recursive cte based solution
declare #T table (iden int identity, col1 varchar(100));
insert into #T(col1) values
('ROOT/South America/Lima/Test/Test2')
, ('ROOT/South America/Peru/Test/Test2')
, ('ROOT//South America/Venuzuala ')
, ('RtT/South America / ')
, ('ROOT/South Americas// ');
declare #split char(1) = '/';
select #split as split;
with cte as
( select t.iden, case when SUBSTRING(REVERSE(rtrim(t.col1)), 1, 1) = #split then LTRIM(RTRIM(t.col1)) else LTRIM(RTRIM(t.col1)) + #split end as col1, 0 as pos , 1 as cnt
from #T t
union all
select t.iden, t.col1 , charindex(#split, t.col1, t.pos + 1), cnt + 1
from cte t
where charindex(#split, t.col1, t.pos + 1) > 0
)
select t1.*, t2.pos, t2.cnt
, ltrim(rtrim(SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1))) as bingo
from cte t1
join cte t2
on t2.iden = t1.iden
and t2.cnt = t1.cnt+1
and t2.pos > t1.pos
order by t1.iden, t1.cnt;
With all due respect to #AviG this is the bug free version of function deviced by him to return all the tokens in full.
IF EXISTS (SELECT * FROM sys.objects WHERE type = 'TF' AND name = 'TF_SplitString')
DROP FUNCTION [dbo].[TF_SplitString]
GO
-- =============================================
-- Author: AviG
-- Amendments: Parameterize the delimeter and included the missing chars in last token - Gemunu Wickremasinghe
-- Description: Tabel valued function that Breaks the delimeted string by given delimeter and returns a tabel having split results
-- Usage
-- select * from [dbo].[TF_SplitString]('token1,token2,,,,,,,,token969',',')
-- 969 items should be returned
-- select * from [dbo].[TF_SplitString]('4672978261,4672978255',',')
-- 2 items should be returned
-- =============================================
CREATE FUNCTION dbo.TF_SplitString
( #stringToSplit VARCHAR(MAX) ,
#delimeter char = ','
)
RETURNS
#returnList TABLE ([Name] [nvarchar] (500))
AS
BEGIN
DECLARE #name NVARCHAR(255)
DECLARE #pos INT
WHILE LEN(#stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(#delimeter, #stringToSplit)
if #pos = 0
BEGIN
SELECT #pos = LEN(#stringToSplit)
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos)
END
else
BEGIN
SELECT #name = SUBSTRING(#stringToSplit, 1, #pos-1)
END
INSERT INTO #returnList
SELECT #name
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, LEN(#stringToSplit)-#pos)
END
RETURN
END
This is based on Andy Robertson's answer, I needed a delimiter other than comma.
CREATE FUNCTION dbo.splitstring ( #stringToSplit nvarchar(MAX), #delim nvarchar(max))
RETURNS
#returnList TABLE ([value] [nvarchar] (MAX))
AS
BEGIN
DECLARE #value NVARCHAR(max)
DECLARE #pos INT
WHILE CHARINDEX(#delim, #stringToSplit) > 0
BEGIN
SELECT #pos = CHARINDEX(#delim, #stringToSplit)
SELECT #value = SUBSTRING(#stringToSplit, 1, #pos - 1)
INSERT INTO #returnList
SELECT #value
SELECT #stringToSplit = SUBSTRING(#stringToSplit, #pos + LEN(#delim), LEN(#stringToSplit) - #pos)
END
INSERT INTO #returnList
SELECT #stringToSplit
RETURN
END
GO
And to use it:
SELECT * FROM dbo.splitstring('test1 test2 test3', ' ');
(Tested on SQL Server 2008 R2)
EDIT: correct test code
ALTER FUNCTION [dbo].func_split_string
(
#input as varchar(max),
#delimiter as varchar(10) = ";"
)
RETURNS #result TABLE
(
id smallint identity(1,1),
csv_value varchar(max) not null
)
AS
BEGIN
DECLARE #pos AS INT;
DECLARE #string AS VARCHAR(MAX) = '';
WHILE LEN(#input) > 0
BEGIN
SELECT #pos = CHARINDEX(#delimiter,#input);
IF(#pos<=0)
select #pos = len(#input)
IF(#pos <> LEN(#input))
SELECT #string = SUBSTRING(#input, 1, #pos-1);
ELSE
SELECT #string = SUBSTRING(#input, 1, #pos);
INSERT INTO #result SELECT #string
SELECT #input = SUBSTRING(#input, #pos+len(#delimiter), LEN(#input)-#pos)
END
RETURN
END
You can Use this function:
CREATE FUNCTION SplitString
(
#Input NVARCHAR(MAX),
#Character CHAR(1)
)
RETURNS #Output TABLE (
Item NVARCHAR(1000)
)
AS
BEGIN
DECLARE #StartIndex INT, #EndIndex INT
SET #StartIndex = 1
IF SUBSTRING(#Input, LEN(#Input) - 1, LEN(#Input)) <> #Character
BEGIN
SET #Input = #Input + #Character
END
WHILE CHARINDEX(#Character, #Input) > 0
BEGIN
SET #EndIndex = CHARINDEX(#Character, #Input)
INSERT INTO #Output(Item)
SELECT SUBSTRING(#Input, #StartIndex, #EndIndex - 1)
SET #Input = SUBSTRING(#Input, #EndIndex + 1, LEN(#Input))
END
RETURN
END
GO
Here is an example that you can use as function or also you can put the same logic in procedure.
--SELECT * from [dbo].fn_SplitString ;
CREATE FUNCTION [dbo].[fn_SplitString]
(#CSV VARCHAR(MAX), #Delimeter VARCHAR(100) = ',')
RETURNS #retTable TABLE
(
[value] VARCHAR(MAX) NULL
)AS
BEGIN
DECLARE
#vCSV VARCHAR (MAX) = #CSV,
#vDelimeter VARCHAR (100) = #Delimeter;
IF #vDelimeter = ';'
BEGIN
SET #vCSV = REPLACE(#vCSV, ';', '~!~#~');
SET #vDelimeter = REPLACE(#vDelimeter, ';', '~!~#~');
END;
SET #vCSV = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(#vCSV, '&', '&'), '<', '<'), '>', '>'), '''', '&apos;'), '"', '"');
DECLARE #xml XML;
SET #xml = '<i>' + REPLACE(#vCSV, #vDelimeter, '</i><i>') + '</i>';
INSERT INTO #retTable
SELECT
x.i.value('.', 'varchar(max)') AS COLUMNNAME
FROM #xml.nodes('//i')AS x(i);
RETURN;
END;
/*
Answer to T-SQL split string
Based on answers from Andy Robinson and AviG
Enhanced functionality ref: LEN function not including trailing spaces in SQL Server
This 'file' should be valid as both a markdown file and an SQL file
*/
CREATE FUNCTION dbo.splitstring ( --CREATE OR ALTER
#stringToSplit NVARCHAR(MAX)
) RETURNS #returnList TABLE ([Item] NVARCHAR (MAX))
AS BEGIN
DECLARE #name NVARCHAR(MAX)
DECLARE #pos BIGINT
SET #stringToSplit = #stringToSplit + ',' -- this should allow entries that end with a `,` to have a blank value in that "column"
WHILE ((LEN(#stringToSplit+'_') > 1)) BEGIN -- `+'_'` gets around LEN trimming terminal spaces. See URL referenced above
SET #pos = COALESCE(NULLIF(CHARINDEX(',', #stringToSplit),0),LEN(#stringToSplit+'_')) -- COALESCE grabs first non-null value
SET #name = SUBSTRING(#stringToSplit, 1, #pos-1) --MAX size of string of type nvarchar is 4000
SET #stringToSplit = SUBSTRING(#stringToSplit, #pos+1, 4000) -- With SUBSTRING fn (MS web): "If start is greater than the number of characters in the value expression, a zero-length expression is returned."
INSERT INTO #returnList SELECT #name --additional debugging parameters below can be added
-- + ' pos:' + CAST(#pos as nvarchar) + ' remain:''' + #stringToSplit + '''(' + CAST(LEN(#stringToSplit+'_')-1 as nvarchar) + ')'
END
RETURN
END
GO
/*
Test cases: see URL referenced as "enhanced functionality" above
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,b')
Item | L
--- | ---
a | 1
| 0
b | 1
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,,')
Item | L
--- | ---
a | 1
| 0
| 0
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, ')
Item | L
--- | ---
a | 1
| 0
| 1
SELECT *,LEN(Item+'_')-1 'L' from splitstring('a,, c ')
Item | L
--- | ---
a | 1
| 0
c | 3
*/
The easiest way:
Install SQL Server 2016
Use STRING_SPLIT https://msdn.microsoft.com/en-us/library/mt684588.aspx
It works even in express edition :).

How to get substring in SQLsever by finding nth occurance of character

I have a varchar field in SQL Server 2008 like
colName_vch = 'field1;field2;field3;field4;field5;field6;field7'
I want the value of field4.
Now one long way is to do use RIGHT,CHARINDEX for ; and SUBSTRING in recursion, but that becomes very complex and increases query time.
Is there any other quick/less complex way for achieving this?
I know this is a bad DB design, but I am stuck with this for a while now.
Any help is appreciated!!
I'm not sure if i've really understood your question, but here is my guess:
You could write a custom Split function which splits by a delimiter(in this case ;). Then you can use ROW_NUMBER to get the desired part with a given index(4 here).
For example:
DECLARE #string VARCHAR(100);
SET #string='field1;field2;field3;field4;field5;field6;field7';
DECLARE #index INT;
SET #index = 4;
WITH cte
AS (SELECT item,
rn=Row_number()
OVER(
ORDER BY item)
FROM dbo.Split(#string, ';'))
SELECT TOP 1 item
FROM cte
WHERE rn = #index
Here a DEMO on sql-fiddle.
This is my split-function:
CREATE FUNCTION [dbo].[Split]
(
#ItemList NVARCHAR(MAX),
#delimiter CHAR(1)
)
RETURNS #IDTable TABLE (Item VARCHAR(50))
AS
BEGIN
DECLARE #tempItemList NVARCHAR(MAX)
SET #tempItemList = #ItemList
DECLARE #i INT
DECLARE #Item NVARCHAR(4000)
SET #tempItemList = REPLACE (#tempItemList, ' ', '')
SET #i = CHARINDEX(#delimiter, #tempItemList)
WHILE (LEN(#tempItemList) > 0)
BEGIN
IF #i = 0
SET #Item = #tempItemList
ELSE
SET #Item = LEFT(#tempItemList, #i - 1)
INSERT INTO #IDTable(Item) VALUES(#Item)
IF #i = 0
SET #tempItemList = ''
ELSE
SET #tempItemList = RIGHT(#tempItemList, LEN(#tempItemList) - #i)
SET #i = CHARINDEX(#delimiter, #tempItemList)
END
RETURN
END
CREATE FUNCTION dbo.SplitStrings
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS #t TABLE([Index] INT IDENTITY(1,1), Item NVARCHAR(255))
AS
BEGIN
INSERT #t(Item) SELECT SUBSTRING(#List, Number,
CHARINDEX(#Delimiter, #List + #Delimiter, Number) - Number)
FROM (SELECT ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects) AS n(Number)
WHERE Number <= CONVERT(INT, LEN(#List))
AND SUBSTRING(#Delimiter + #List, Number, 1) = #Delimiter
ORDER BY Number OPTION (MAXDOP 1);
RETURN;
END
GO
DECLARE #x TABLE(i INT, string NVARCHAR(4000));
INSERT #x SELECT 1, N'field1;field2;field3;field4;field5;'
UNION ALL SELECT 2, N'x;y;6;r;3;2;w;'
UNION ALL SELECT 3, N'ttt;444;rrr;333;111;444;777;888;';
SELECT x.i, s1.Item
FROM #x AS x
CROSS APPLY dbo.SplitStrings(x.string, ';') AS s1
WHERE s1.[Index] = 4;

Resources