Related
I am trying to parse out a value from a string that involves getting the last index of a string. Currently, I am doing a horrible hack that involves reversing a string:
SELECT REVERSE(SUBSTRING(REVERSE(DB_NAME()), 1,
CHARINDEX('_', REVERSE(DB_NAME()), 1) - 1))
To me this code is nearly unreadable. I just upgraded to SQL Server 2016 and I hoping there is a better way.
Is there?
If you want everything after the last _, then use:
select right(db_name(), charindex('_', reverse(db_name()) + '_') - 1)
If you want everything before, then use left():
select left(db_name(), len(db_name()) - charindex('_', reverse(db_name()) + '_'))
Wrote 2 functions, 1 to return LastIndexOf for the selected character.
CREATE FUNCTION dbo.LastIndexOf(#source nvarchar(80), #pattern char)
RETURNS int
BEGIN
RETURN (LEN(#source)) - CHARINDEX(#pattern, REVERSE(#source))
END;
GO
and 1 to return a string before this LastIndexOf. Maybe it will be useful to someone.
CREATE FUNCTION dbo.StringBeforeLastIndex(#source nvarchar(80), #pattern char)
RETURNS nvarchar(80)
BEGIN
DECLARE #lastIndex int
SET #lastIndex = (LEN(#source)) - CHARINDEX(#pattern, REVERSE(#source))
RETURN SUBSTRING(#source, 0, #lastindex + 1)
-- +1 because index starts at 0, but length at 1, so to get up to 11th index, we need LENGTH 11+1=12
END;
GO
No, SQL server doesnt have LastIndexOf.
This are the available string functions
But you can always can create your own function
CREATE FUNCTION dbo.LastIndexOf(#source text, #pattern char)
RETURNS
AS
BEGIN
DECLARE #ret text;
SELECT into #ret
REVERSE(SUBSTRING(REVERSE(#source), 1,
CHARINDEX(#pattern, REVERSE(#source), 1) - 1))
RETURN #ret;
END;
GO
Once you have one of the split strings from here,you can do it in a set based way like this..
declare #string varchar(max)
set #string='C:\Program Files\Microsoft SQL Server\MSSQL\DATA\AdventureWorks_Data.mdf'
;with cte
as
(select *,row_number() over (order by (select null)) as rownum
from [dbo].[SplitStrings_Numbers](#string,'\')
)
select top 1 item from cte order by rownum desc
**Output:**
AdventureWorks_Data.mdf
CREATE FUNCTION dbo.LastIndexOf(#text NTEXT, #delimiter NTEXT)
RETURNS INT
AS
BEGIN
IF (#text IS NULL) RETURN NULL;
IF (#delimiter IS NULL) RETURN NULL;
DECLARE #Text2 AS NVARCHAR(MAX) = #text;
DECLARE #Delimiter2 AS NVARCHAR(MAX) = #delimiter;
DECLARE #Index AS INT = CHARINDEX(REVERSE(#Delimiter2), REVERSE(#Text2));
IF (#Index < 1) RETURN 0;
DECLARE #ContentLength AS INT = (LEN('|' + #Text2 + '|') - 2);
DECLARE #DelimiterLength AS INT = (LEN('|' + #Delimiter2 + '|') - 2);
DECLARE #Result AS INT = (#ContentLength - #Index - #DelimiterLength + 2);
RETURN #Result;
END
Allows for multi-character delimiters like ", " (comma space).
Returns 0 if the delimiter is not found.
Takes a NTEXT for comfort reasons as NVARCHAR(MAX)s are implicitely cast into NTEXT but not vice-versa.
Handles delimiters with leading or tailing space correctly!
Try:
select LEN('tran van abc') + 1 - CHARINDEX(' ', REVERSE('tran van abc'))
So, the last index of ' ' is : 9
I came across this thread while searching for a solution to my similar problem which had the exact same requirement but was for a different kind of database that was lacking the REVERSE function.
In my case this was for a OpenEdge (Progress) database, which has a slightly different syntax. This made the INSTR function available to me that most Oracle typed databases offer.
So I came up with the following code:
SELECT
INSTR(foo.filepath, '/',1, LENGTH(foo.filepath) - LENGTH( REPLACE( foo.filepath, '/', ''))) AS IndexOfLastSlash
FROM foo
However, for my specific situation (being the OpenEdge (Progress) database) this did not result into the desired behaviour because replacing the character with an empty char gave the same length as the original string. This doesn't make much sense to me but I was able to bypass the problem with the code below:
SELECT
INSTR(foo.filepath, '/',1, LENGTH( REPLACE( foo.filepath, '/', 'XX')) - LENGTH(foo.filepath)) AS IndexOfLastSlash
FROM foo
Now I understand that this code won't solve the problem for T-SQL because there is no alternative to the INSTR function that offers the Occurence property.
Just to be thorough I'll add the code needed to create this scalar function so it can be used the same way like I did in the above examples. And will do exactly what the OP wanted, serve as a LastIndexOf method for SQL Server.
-- Drop the function if it already exists
IF OBJECT_ID('INSTR', 'FN') IS NOT NULL
DROP FUNCTION INSTR
GO
-- User-defined function to implement Oracle INSTR in SQL Server
CREATE FUNCTION INSTR (#str VARCHAR(8000), #substr VARCHAR(255), #start INT, #occurrence INT)
RETURNS INT
AS
BEGIN
DECLARE #found INT = #occurrence,
#pos INT = #start;
WHILE 1=1
BEGIN
-- Find the next occurrence
SET #pos = CHARINDEX(#substr, #str, #pos);
-- Nothing found
IF #pos IS NULL OR #pos = 0
RETURN #pos;
-- The required occurrence found
IF #found = 1
BREAK;
-- Prepare to find another one occurrence
SET #found = #found - 1;
SET #pos = #pos + 1;
END
RETURN #pos;
END
GO
To avoid the obvious, when the REVERSE function is available you do not need to create this scalar function and you can just get the required result like this:
SELECT
LEN(foo.filepath) - CHARINDEX('\', REVERSE(foo.filepath))+1 AS LastIndexOfSlash
FROM foo
Try this.
drop table #temp
declare #brokername1 nvarchar(max)='indiabullssecurities,canmoney,indianivesh,acumencapitalmarket,sharekhan,edelweisscapital';
Create Table #temp
(
ID int identity(1,1) not null,
value varchar(100) not null
)
INSERT INTO #temp(value) SELECT value from STRING_SPLIT(#brokername1,',')
declare #id int;
set #id=(select max(id) from #temp)
--print #id
declare #results varchar(500)
select #results = coalesce(#results + ',', '') + convert(varchar(12),value)
from #temp where id<#id
order by id
print #results
I have a need to create a function the will return nth element of a delimited string.
For a data migration project, I am converting JSON audit records stored in a SQL Server database into a structured report using SQL script. Goal is to deliver a sql script and a sql function used by the script without any code.
(This is a short-term fix will be used while a new auditing feature is added the ASP.NET/MVC application)
There is no shortage of delimited string to table examples available.
I've chosen a Common Table Expression example http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings
Example: I want to return 67 from '1,222,2,67,888,1111'
This is the easiest answer to rerieve the 67 (type-safe!!):
SELECT CAST('<x>' + REPLACE('1,222,2,67,888,1111',',','</x><x>') + '</x>' AS XML).value('/x[4]','int')
In the following you will find examples how to use this with variables for the string, the delimiter and the position (even for edge-cases with XML-forbidden characters)
The easy one
This question is not about a string split approach, but about how to get the nth element. The easiest, fully inlineable way would be this IMO:
This is a real one-liner to get part 2 delimited by a space:
DECLARE #input NVARCHAR(100)=N'part1 part2 part3';
SELECT CAST(N'<x>' + REPLACE(#input,N' ',N'</x><x>') + N'</x>' AS XML).value('/x[2]','nvarchar(max)')
Variables can be used with sql:variable() or sql:column()
Of course you can use variables for delimiter and position (use sql:column to retrieve the position directly from a query's value):
DECLARE #dlmt NVARCHAR(10)=N' ';
DECLARE #pos INT = 2;
SELECT CAST(N'<x>' + REPLACE(#input,#dlmt,N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("#pos")][1]','nvarchar(max)')
Edge-Case with XML-forbidden characters
If your string might include forbidden characters, you still can do it this way. Just use FOR XML PATH on your string first to replace all forbidden characters with the fitting escape sequence implicitly.
It's a very special case if - additionally - your delimiter is the semicolon. In this case I replace the delimiter first to '#DLMT#', and replace this to the XML tags finally:
SET #input=N'Some <, > and &;Other äöü#€;One more';
SET #dlmt=N';';
SELECT CAST(N'<x>' + REPLACE((SELECT REPLACE(#input,#dlmt,'#DLMT#') AS [*] FOR XML PATH('')),N'#DLMT#',N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("#pos")][1]','nvarchar(max)');
UPDATE for SQL-Server 2016+
Regretfully the developers forgot to return the part's index with STRING_SPLIT. But, using SQL-Server 2016+, there is JSON_VALUE and OPENJSON.
With JSON_VALUE we can pass in the position as the index' array.
For OPENJSON the documentation states clearly:
When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.
A string like 1,2,3 needs nothing more than brackets: [1,2,3].
A string of words like this is an example needs to be ["this","is","an"," example"].
These are very easy string operations. Just try it out:
DECLARE #str VARCHAR(100)='Hello John Smith';
DECLARE #position INT = 2;
--We can build the json-path '$[1]' using CONCAT
SELECT JSON_VALUE('["' + REPLACE(#str,' ','","') + '"]',CONCAT('$[',#position-1,']'));
--See this for a position safe string-splitter (zero-based):
SELECT JsonArray.[key] AS [Position]
,JsonArray.[value] AS [Part]
FROM OPENJSON('["' + REPLACE(#str,' ','","') + '"]') JsonArray
In this post I tested various approaches and found, that OPENJSON is really fast. Even much faster than the famous "delimitedSplit8k()" method...
UPDATE 2 - Get the values type-safe
We can use an array within an array simply by using doubled [[]]. This allows for a typed WITH-clause:
DECLARE #SomeDelimitedString VARCHAR(100)='part1|1|20190920';
DECLARE #JsonArray NVARCHAR(MAX)=CONCAT('[["',REPLACE(#SomeDelimitedString,'|','","'),'"]]');
SELECT #SomeDelimitedString AS TheOriginal
,#JsonArray AS TransformedToJSON
,ValuesFromTheArray.*
FROM OPENJSON(#JsonArray)
WITH(TheFirstFragment VARCHAR(100) '$[0]'
,TheSecondFragment INT '$[1]'
,TheThirdFragment DATE '$[2]') ValuesFromTheArray
Here is my initial solution...
It is based on work by Aaron Bertrand http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings
I simply changed the return type to make it a scalar function.
Example:
SELECT dbo.GetSplitString_CTE('1,222,2,67,888,1111',',',4)
CREATE FUNCTION dbo.GetSplitString_CTE
(
#List VARCHAR(MAX),
#Delimiter VARCHAR(255),
#ElementNumber int
)
RETURNS VARCHAR(4000)
AS
BEGIN
DECLARE #result varchar(4000)
DECLARE #Items TABLE ( position int IDENTITY PRIMARY KEY,
Item VARCHAR(4000)
)
DECLARE #ll INT = LEN(#List) + 1, #ld INT = LEN(#Delimiter);
WITH a AS
(
SELECT
[start] = 1,
[end] = COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, #ld), 0), #ll),
[value] = SUBSTRING(#List, 1,
COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, #ld), 0), #ll) - 1)
UNION ALL
SELECT
[start] = CONVERT(INT, [end]) + #ld,
[end] = COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, [end] + #ld), 0), #ll),
[value] = SUBSTRING(#List, [end] + #ld,
COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, [end] + #ld), 0), #ll)-[end]-#ld)
FROM a
WHERE [end] < #ll
)
INSERT #Items SELECT [value]
FROM a
WHERE LEN([value]) > 0
OPTION (MAXRECURSION 0);
SELECT #result=Item
FROM #Items
WHERE position=#ElementNumber
RETURN #result;
END
GO
How about:
CREATE FUNCTION dbo.NTH_ELEMENT (#Input NVARCHAR(MAX), #Delim CHAR = '-', #N INT = 0)
RETURNS NVARCHAR(MAX)
AS
BEGIN
RETURN (SELECT VALUE FROM STRING_SPLIT(#Input, #Delim) ORDER BY (SELECT NULL) OFFSET #N ROWS FETCH NEXT 1 ROW ONLY)
END
On Azure SQL Database, and on SQL Server 2022, STRING_SPLIT now has an optional ordinal parameter. If the parameter is omitted, or 0 is passed, then the function acts as it did before, and just returns a value column and the order is not guaranteed. If you pass the parameter with the value 1 then the function returns 2 columns, value, and ordinal which (unsurprisingly) provides the ordinal position of the value within the string.
So, if you wanted the 4th delimited value from the string '1,222,2,67,888,1111' you could do the following:
SELECT [value]
FROM STRING_SPLIT('1,222,2,67,888,1111',',',1)
WHERE ordinal = 4;
If the value was in a column, it would look like this:
SELECT SS.[value]
FROM dbo.YourTable YT
CROSS APPLY STRING_SPLIT(YT.YourColumn,',',1) SS
WHERE SS.ordinal = 4;
#a - the value (f.e. 'a/bb/ccc/dddd/ee/ff/....')
#p - the desired position (1,2,3...)
#d - the delimeter ( '/' )
trim(substring(replace(#a,#d,replicate(' ',len(#a))),(#p-1)*len(#a)+1,len(#a)))
only problem is - if desired part has trailing or leading blanks they get trimmed.
Completely Based on article from https://exceljet.net/formula/split-text-with-delimiter
In a rare moment of lunacy I just thought that split is far easier if we use XML to parse it out for us:
(Using the variables from #Gary Kindel's answer)
declare #xml xml
set #xml = '<split><el>' + replace(#list,#Delimiter,'</el><el>') + '</el></split>'
select
el = split.el.value('.','varchar(max)')
from #xml.nodes('/split/el') split(el))
This lists all elements of the string, split by the specified character.
We can use an xpath test to filter out empty values, and a further xpath test to restrict this to the element we're interested in. In full Gary's function becomes:
alter FUNCTION dbo.GetSplitString_CTE
(
#List VARCHAR(MAX),
#Delimiter VARCHAR(255),
#ElementNumber int
)
RETURNS VARCHAR(max)
AS
BEGIN
-- escape any XML https://dba.stackexchange.com/a/143140/65992
set #list = convert(VARCHAR(MAX),(select #list for xml path(''), type));
declare #xml xml
set #xml = '<split><el>' + replace(#list,#Delimiter,'</el><el>') + '</el></split>'
declare #ret varchar(max)
set #ret = (select
el = split.el.value('.','varchar(max)')
from #xml.nodes('/split/el[string-length(.)>0][position() = sql:variable("#elementnumber")]') split(el))
return #ret
END
you can put this select into UFN. if you need you can customize it for specifying delimiter as well. in that case your ufn will have two input. number Nth and delimiter to use.
DECLARE #tlist varchar(max)='10,20,30,40,50,60,70,80,90,100'
DECLARE #i INT=1, #nth INT=3
While len(#tlist) <> 0
BEGIN
IF #i=#nth
BEGIN
select Case when charindex(',',#tlist) <> 0 Then LEFT(#tlist,charindex(',',#tlist)-1)
Else #tlist
END
END
Select #tlist = Case when charindex(',',#tlist) <> 0 Then substring(#tlist,charindex(',',#tlist)+1,len(#tlist))
Else ''
END
SELECT #i=#i+1
END
Alternatively, one can use xml, nodes() and ROW_NUMBER. We can order the elements based on their document order. For example:
DECLARE #Input VARCHAR(100) = '1a,2b,3c,4d,5e,6f,7g,8h'
,#Number TINYINT = 3
DECLARE #XML XML;
DECLARE #value VARCHAR(100);
SET #XML = CAST('<x>' + REPLACE(#Input,',','</x><x>') + '</x>' AS XML);
WITH DataSource ([rowID], [rowValue]) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY T.c ASC)
,T.c.value('.', 'VARCHAR(100)')
FROM #XML.nodes('./x') T(c)
)
SELECT #value = [rowValue]
FROM DataSource
WHERE [rowID] = #Number;
SELECT #value;
I would rather create a temp table with an identity column and fill it up with output from the SPLIT function.
CREATE TABLE #tblVals(Id INT IDENTITY(1,1), Val NVARCHAR(100))
INSERT INTO #tblVals (Val)
SELECT [value] FROM STRING_SPLIT('Val1-Val3-Val2-Val5', '-')
SELECT * FROM #tblVals
Now you can easily do something like below.
DECLARE #val2 NVARCHAR(100) = (SELECT TOP 1 Val FROM #tblVals WHERE Id = 2)
See the snapshot below:
You can use STRING_SPLIT with ROW_NUMBER:
SELECT value, idx FROM
(
SELECT
value,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) idx
FROM STRING_SPLIT('Lorem ipsum dolor sit amet.', ' ')
) t
WHERE idx=2
returns second element (idx=2): 'ipsum'
We have the answer over below url.
DECLARE # AS VARCHAR(MAX) = 'Pawan1,Pawan2,Pawan4,Pawan3'
SELECT VALUE FROM
(
SELECT VALUE , ROW_NUMBER() OVER (ORDER BY (SELECT null)) rnk FROM STRING_SPLIT(#, ',')
)x where rnk = 3
GO
https://msbiskills.com/2018/06/15/sql-puzzle-multiple-ways-to-split-a-string-and-get-nth-row-xml-advanced-sql/
I don't have enough reputation to comment, so I am adding an answer. Please adjust as appropriate.
I have a problem with Gary Kindel's answer for cases where there is nothing between the two delimiters
If you do
select * from dbo.GetSplitString_CTE('abc^def^^ghi','^',3)
you get
ghi
instead of an empty string
If you comment out the
WHERE LEN([value]) > 0
line, you get the desired result
I cannot comment on Gary's solution because of my low reputation
I know Gary was referencing another link.
I have struggled to understand why we need this variable
#ld INT = LEN(#Delimiter)
I also don't understand why charindex has to start at the position of length of delimiter, #ld
I tested with many examples with a single character delimiter, and they work. Most of the time, delimiter character is a single character. However, since the developer included the ld as length of delimiter, the code has to work for delimiters that have more than one character
In this case, the following case will fail
11,,,22,,,33,,,44,,,55,,,
I cloned from the codes from this link. http://codebetter.com/raymondlewallen/2005/10/26/quick-t-sql-to-parse-a-delimited-string/
I have tested various scenarios including the delimiters that have more than one character
alter FUNCTION [dbo].[split1]
(
#string1 VARCHAR(8000) -- List of delimited items
, #Delimiter VARCHAR(40) = ',' -- delimiter that separates items
, #ElementNumber int
)
RETURNS varchar(8000)
AS
BEGIN
declare #position int
declare #piece varchar(8000)=''
declare #returnVal varchar(8000)=''
declare #Pattern varchar(50) = '%' + #Delimiter + '%'
declare #counter int =0
declare #ld int = len(#Delimiter)
declare #ls1 int = len (#string1)
declare #foundit int = 0
if patindex(#Pattern , #string1) = 0
return ''
if right(rtrim(#string1),1) <> #Delimiter
set #string1 = #string1 + #Delimiter
set #position = patindex(#Pattern , #string1) + #ld -1
while #position > 0
begin
set #counter = #counter +1
set #ls1 = len (#string1)
if (#ls1 >= #ld)
set #piece = left(#string1, #position - #ld)
else
break
if (#counter = #ElementNumber)
begin
set #foundit = 1
break
end
if len(#string1) > 0
begin
set #string1 = stuff(#string1, 1, #position, '')
set #position = patindex(#Pattern , #string1) + #ld -1
end
else
set #position = -1
end
if #foundit =1
set #returnVal = #piece
else
set #returnVal = ''
return #returnVal
you can create simple table variable and use it as below
Declare #tbl_split Table (Id INT IDENTITY(1,1), VAL VARCHAR(50))
INSERT #tbl_split SELECT VALUE
FROM string_split('999999:01', ':')
Select val from #tbl_split
WHERE Id=2
I have string like this 'SUZUKI GSX_1300/TWOWHEELER'
Now I want to split the above string into words and inserting the one by one word into one table.
DECLARE #a TABLE (a nvarchar(500));
declare #RTADESC nvarchar(100) = 'SUZUKI GSX_1300/TWOWHEELER';
declare #b int =1;
declare #c int;
set #c=CHARINDEX('_',#RTADESC,#b);
WHILE #c>0
BEGIN
SELECT #b= CHARINDEX('_' ,#RTADESC,#b)+1
INSERT INTO #a
SELECT SUBSTRING(#RTADESC,0,#b-1)
set #c=CHARINDEX('_',#RTADESC,#b)
END
SELECT * FROM #a
Now I want to see the data in the table #a as below
SUZUKI
GSX
1300
TWOWHEELER
Based on it I will search for vehicle makers in my table.
So please help on this how I can split my string I may get different special characters as well in future.
There are some excellent options available at: http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings
There are solutions which allow you to pass the delimiter also, so you could pass in different ones in future.
Create table function func_SplitString
CREATE FUNCTION [dbo].[func_SplitString](#DelimitedString varchar(8000), #Delimiter varchar(100))
RETURNS #tblArray TABLE ( ID int IDENTITY(1,1),Element varchar(1000))
AS
BEGIN
-- Local Variable Declarations
DECLARE #Index smallint,
#Start smallint,
#DelSize smallint
SET #DelSize = LEN(#Delimiter)
-- Loop through source string and add elements to destination table array
WHILE LEN(#DelimitedString) > 0
BEGIN
SET #Index = CHARINDEX(#Delimiter, #DelimitedString)
IF #Index = 0
BEGIN
INSERT INTO #tblArray (Element) VALUES (LTRIM(RTRIM(#DelimitedString)))
BREAK
END
ELSE
BEGIN
INSERT INTO #tblArray (Element) VALUES (LTRIM(RTRIM(SUBSTRING(#DelimitedString, 1,#Index - 1))))
SET #Start = #Index + #DelSize
SET #DelimitedString = SUBSTRING(#DelimitedString, #Start , LEN(#DelimitedString) - #Start + 1)
END
END
RETURN
END
Use the function to split string like this 'SUZUKI GSX_1300/TWOWHEELER'
Replace all special character to '_'
Using func_SplitString
SELECT * FROM func_SplitString (REPLACE(REPLACE('SUZUKI GSX_1300/TWOWHEELER',' ','_'),'/','_'), '_')
I have a need to create a function the will return nth element of a delimited string.
For a data migration project, I am converting JSON audit records stored in a SQL Server database into a structured report using SQL script. Goal is to deliver a sql script and a sql function used by the script without any code.
(This is a short-term fix will be used while a new auditing feature is added the ASP.NET/MVC application)
There is no shortage of delimited string to table examples available.
I've chosen a Common Table Expression example http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings
Example: I want to return 67 from '1,222,2,67,888,1111'
This is the easiest answer to rerieve the 67 (type-safe!!):
SELECT CAST('<x>' + REPLACE('1,222,2,67,888,1111',',','</x><x>') + '</x>' AS XML).value('/x[4]','int')
In the following you will find examples how to use this with variables for the string, the delimiter and the position (even for edge-cases with XML-forbidden characters)
The easy one
This question is not about a string split approach, but about how to get the nth element. The easiest, fully inlineable way would be this IMO:
This is a real one-liner to get part 2 delimited by a space:
DECLARE #input NVARCHAR(100)=N'part1 part2 part3';
SELECT CAST(N'<x>' + REPLACE(#input,N' ',N'</x><x>') + N'</x>' AS XML).value('/x[2]','nvarchar(max)')
Variables can be used with sql:variable() or sql:column()
Of course you can use variables for delimiter and position (use sql:column to retrieve the position directly from a query's value):
DECLARE #dlmt NVARCHAR(10)=N' ';
DECLARE #pos INT = 2;
SELECT CAST(N'<x>' + REPLACE(#input,#dlmt,N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("#pos")][1]','nvarchar(max)')
Edge-Case with XML-forbidden characters
If your string might include forbidden characters, you still can do it this way. Just use FOR XML PATH on your string first to replace all forbidden characters with the fitting escape sequence implicitly.
It's a very special case if - additionally - your delimiter is the semicolon. In this case I replace the delimiter first to '#DLMT#', and replace this to the XML tags finally:
SET #input=N'Some <, > and &;Other äöü#€;One more';
SET #dlmt=N';';
SELECT CAST(N'<x>' + REPLACE((SELECT REPLACE(#input,#dlmt,'#DLMT#') AS [*] FOR XML PATH('')),N'#DLMT#',N'</x><x>') + N'</x>' AS XML).value('/x[sql:variable("#pos")][1]','nvarchar(max)');
UPDATE for SQL-Server 2016+
Regretfully the developers forgot to return the part's index with STRING_SPLIT. But, using SQL-Server 2016+, there is JSON_VALUE and OPENJSON.
With JSON_VALUE we can pass in the position as the index' array.
For OPENJSON the documentation states clearly:
When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.
A string like 1,2,3 needs nothing more than brackets: [1,2,3].
A string of words like this is an example needs to be ["this","is","an"," example"].
These are very easy string operations. Just try it out:
DECLARE #str VARCHAR(100)='Hello John Smith';
DECLARE #position INT = 2;
--We can build the json-path '$[1]' using CONCAT
SELECT JSON_VALUE('["' + REPLACE(#str,' ','","') + '"]',CONCAT('$[',#position-1,']'));
--See this for a position safe string-splitter (zero-based):
SELECT JsonArray.[key] AS [Position]
,JsonArray.[value] AS [Part]
FROM OPENJSON('["' + REPLACE(#str,' ','","') + '"]') JsonArray
In this post I tested various approaches and found, that OPENJSON is really fast. Even much faster than the famous "delimitedSplit8k()" method...
UPDATE 2 - Get the values type-safe
We can use an array within an array simply by using doubled [[]]. This allows for a typed WITH-clause:
DECLARE #SomeDelimitedString VARCHAR(100)='part1|1|20190920';
DECLARE #JsonArray NVARCHAR(MAX)=CONCAT('[["',REPLACE(#SomeDelimitedString,'|','","'),'"]]');
SELECT #SomeDelimitedString AS TheOriginal
,#JsonArray AS TransformedToJSON
,ValuesFromTheArray.*
FROM OPENJSON(#JsonArray)
WITH(TheFirstFragment VARCHAR(100) '$[0]'
,TheSecondFragment INT '$[1]'
,TheThirdFragment DATE '$[2]') ValuesFromTheArray
Here is my initial solution...
It is based on work by Aaron Bertrand http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings
I simply changed the return type to make it a scalar function.
Example:
SELECT dbo.GetSplitString_CTE('1,222,2,67,888,1111',',',4)
CREATE FUNCTION dbo.GetSplitString_CTE
(
#List VARCHAR(MAX),
#Delimiter VARCHAR(255),
#ElementNumber int
)
RETURNS VARCHAR(4000)
AS
BEGIN
DECLARE #result varchar(4000)
DECLARE #Items TABLE ( position int IDENTITY PRIMARY KEY,
Item VARCHAR(4000)
)
DECLARE #ll INT = LEN(#List) + 1, #ld INT = LEN(#Delimiter);
WITH a AS
(
SELECT
[start] = 1,
[end] = COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, #ld), 0), #ll),
[value] = SUBSTRING(#List, 1,
COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, #ld), 0), #ll) - 1)
UNION ALL
SELECT
[start] = CONVERT(INT, [end]) + #ld,
[end] = COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, [end] + #ld), 0), #ll),
[value] = SUBSTRING(#List, [end] + #ld,
COALESCE(NULLIF(CHARINDEX(#Delimiter,
#List, [end] + #ld), 0), #ll)-[end]-#ld)
FROM a
WHERE [end] < #ll
)
INSERT #Items SELECT [value]
FROM a
WHERE LEN([value]) > 0
OPTION (MAXRECURSION 0);
SELECT #result=Item
FROM #Items
WHERE position=#ElementNumber
RETURN #result;
END
GO
How about:
CREATE FUNCTION dbo.NTH_ELEMENT (#Input NVARCHAR(MAX), #Delim CHAR = '-', #N INT = 0)
RETURNS NVARCHAR(MAX)
AS
BEGIN
RETURN (SELECT VALUE FROM STRING_SPLIT(#Input, #Delim) ORDER BY (SELECT NULL) OFFSET #N ROWS FETCH NEXT 1 ROW ONLY)
END
On Azure SQL Database, and on SQL Server 2022, STRING_SPLIT now has an optional ordinal parameter. If the parameter is omitted, or 0 is passed, then the function acts as it did before, and just returns a value column and the order is not guaranteed. If you pass the parameter with the value 1 then the function returns 2 columns, value, and ordinal which (unsurprisingly) provides the ordinal position of the value within the string.
So, if you wanted the 4th delimited value from the string '1,222,2,67,888,1111' you could do the following:
SELECT [value]
FROM STRING_SPLIT('1,222,2,67,888,1111',',',1)
WHERE ordinal = 4;
If the value was in a column, it would look like this:
SELECT SS.[value]
FROM dbo.YourTable YT
CROSS APPLY STRING_SPLIT(YT.YourColumn,',',1) SS
WHERE SS.ordinal = 4;
#a - the value (f.e. 'a/bb/ccc/dddd/ee/ff/....')
#p - the desired position (1,2,3...)
#d - the delimeter ( '/' )
trim(substring(replace(#a,#d,replicate(' ',len(#a))),(#p-1)*len(#a)+1,len(#a)))
only problem is - if desired part has trailing or leading blanks they get trimmed.
Completely Based on article from https://exceljet.net/formula/split-text-with-delimiter
In a rare moment of lunacy I just thought that split is far easier if we use XML to parse it out for us:
(Using the variables from #Gary Kindel's answer)
declare #xml xml
set #xml = '<split><el>' + replace(#list,#Delimiter,'</el><el>') + '</el></split>'
select
el = split.el.value('.','varchar(max)')
from #xml.nodes('/split/el') split(el))
This lists all elements of the string, split by the specified character.
We can use an xpath test to filter out empty values, and a further xpath test to restrict this to the element we're interested in. In full Gary's function becomes:
alter FUNCTION dbo.GetSplitString_CTE
(
#List VARCHAR(MAX),
#Delimiter VARCHAR(255),
#ElementNumber int
)
RETURNS VARCHAR(max)
AS
BEGIN
-- escape any XML https://dba.stackexchange.com/a/143140/65992
set #list = convert(VARCHAR(MAX),(select #list for xml path(''), type));
declare #xml xml
set #xml = '<split><el>' + replace(#list,#Delimiter,'</el><el>') + '</el></split>'
declare #ret varchar(max)
set #ret = (select
el = split.el.value('.','varchar(max)')
from #xml.nodes('/split/el[string-length(.)>0][position() = sql:variable("#elementnumber")]') split(el))
return #ret
END
you can put this select into UFN. if you need you can customize it for specifying delimiter as well. in that case your ufn will have two input. number Nth and delimiter to use.
DECLARE #tlist varchar(max)='10,20,30,40,50,60,70,80,90,100'
DECLARE #i INT=1, #nth INT=3
While len(#tlist) <> 0
BEGIN
IF #i=#nth
BEGIN
select Case when charindex(',',#tlist) <> 0 Then LEFT(#tlist,charindex(',',#tlist)-1)
Else #tlist
END
END
Select #tlist = Case when charindex(',',#tlist) <> 0 Then substring(#tlist,charindex(',',#tlist)+1,len(#tlist))
Else ''
END
SELECT #i=#i+1
END
Alternatively, one can use xml, nodes() and ROW_NUMBER. We can order the elements based on their document order. For example:
DECLARE #Input VARCHAR(100) = '1a,2b,3c,4d,5e,6f,7g,8h'
,#Number TINYINT = 3
DECLARE #XML XML;
DECLARE #value VARCHAR(100);
SET #XML = CAST('<x>' + REPLACE(#Input,',','</x><x>') + '</x>' AS XML);
WITH DataSource ([rowID], [rowValue]) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY T.c ASC)
,T.c.value('.', 'VARCHAR(100)')
FROM #XML.nodes('./x') T(c)
)
SELECT #value = [rowValue]
FROM DataSource
WHERE [rowID] = #Number;
SELECT #value;
I would rather create a temp table with an identity column and fill it up with output from the SPLIT function.
CREATE TABLE #tblVals(Id INT IDENTITY(1,1), Val NVARCHAR(100))
INSERT INTO #tblVals (Val)
SELECT [value] FROM STRING_SPLIT('Val1-Val3-Val2-Val5', '-')
SELECT * FROM #tblVals
Now you can easily do something like below.
DECLARE #val2 NVARCHAR(100) = (SELECT TOP 1 Val FROM #tblVals WHERE Id = 2)
See the snapshot below:
You can use STRING_SPLIT with ROW_NUMBER:
SELECT value, idx FROM
(
SELECT
value,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) idx
FROM STRING_SPLIT('Lorem ipsum dolor sit amet.', ' ')
) t
WHERE idx=2
returns second element (idx=2): 'ipsum'
We have the answer over below url.
DECLARE # AS VARCHAR(MAX) = 'Pawan1,Pawan2,Pawan4,Pawan3'
SELECT VALUE FROM
(
SELECT VALUE , ROW_NUMBER() OVER (ORDER BY (SELECT null)) rnk FROM STRING_SPLIT(#, ',')
)x where rnk = 3
GO
https://msbiskills.com/2018/06/15/sql-puzzle-multiple-ways-to-split-a-string-and-get-nth-row-xml-advanced-sql/
I don't have enough reputation to comment, so I am adding an answer. Please adjust as appropriate.
I have a problem with Gary Kindel's answer for cases where there is nothing between the two delimiters
If you do
select * from dbo.GetSplitString_CTE('abc^def^^ghi','^',3)
you get
ghi
instead of an empty string
If you comment out the
WHERE LEN([value]) > 0
line, you get the desired result
I cannot comment on Gary's solution because of my low reputation
I know Gary was referencing another link.
I have struggled to understand why we need this variable
#ld INT = LEN(#Delimiter)
I also don't understand why charindex has to start at the position of length of delimiter, #ld
I tested with many examples with a single character delimiter, and they work. Most of the time, delimiter character is a single character. However, since the developer included the ld as length of delimiter, the code has to work for delimiters that have more than one character
In this case, the following case will fail
11,,,22,,,33,,,44,,,55,,,
I cloned from the codes from this link. http://codebetter.com/raymondlewallen/2005/10/26/quick-t-sql-to-parse-a-delimited-string/
I have tested various scenarios including the delimiters that have more than one character
alter FUNCTION [dbo].[split1]
(
#string1 VARCHAR(8000) -- List of delimited items
, #Delimiter VARCHAR(40) = ',' -- delimiter that separates items
, #ElementNumber int
)
RETURNS varchar(8000)
AS
BEGIN
declare #position int
declare #piece varchar(8000)=''
declare #returnVal varchar(8000)=''
declare #Pattern varchar(50) = '%' + #Delimiter + '%'
declare #counter int =0
declare #ld int = len(#Delimiter)
declare #ls1 int = len (#string1)
declare #foundit int = 0
if patindex(#Pattern , #string1) = 0
return ''
if right(rtrim(#string1),1) <> #Delimiter
set #string1 = #string1 + #Delimiter
set #position = patindex(#Pattern , #string1) + #ld -1
while #position > 0
begin
set #counter = #counter +1
set #ls1 = len (#string1)
if (#ls1 >= #ld)
set #piece = left(#string1, #position - #ld)
else
break
if (#counter = #ElementNumber)
begin
set #foundit = 1
break
end
if len(#string1) > 0
begin
set #string1 = stuff(#string1, 1, #position, '')
set #position = patindex(#Pattern , #string1) + #ld -1
end
else
set #position = -1
end
if #foundit =1
set #returnVal = #piece
else
set #returnVal = ''
return #returnVal
you can create simple table variable and use it as below
Declare #tbl_split Table (Id INT IDENTITY(1,1), VAL VARCHAR(50))
INSERT #tbl_split SELECT VALUE
FROM string_split('999999:01', ':')
Select val from #tbl_split
WHERE Id=2
I can't see a function like LPAD in SQL Server 2008. For example how can I convert the following queries into T-SQL?
select LPAD(MY_VALUE,2,' ')) VALUE
FROM MY_TABLE
Basically pad it with the number of characters you are intending to select and then right the string.
Select right(replicate(' ',2) + YourFieldValue,2) from YourTable
You can use the space function instead of replicate, space(number_of_spaces), replicate just allows you to pad with alternative characters.
Manual calculations can be annoying to apply inside queries. Luckily, we can create a function:
CREATE FUNCTION LPAD
(
#string VARCHAR(MAX), -- Initial string
#length INT, -- Size of final string
#pad CHAR -- Pad character
)
RETURNS VARCHAR(MAX)
AS
BEGIN
RETURN REPLICATE(#pad, #length - LEN(#string)) + #string;
END
GO
(Please replace VARCHAR with NVARCHAR to your liking, or use an alternative algorithm.)
Then:
SELECT dbo.LPAD(MY_VALUE, 2, ' ') VALUE
FROM MY_TABLE
Should work since SQL Server 2005.
I've come up with a LPAD and RPAD function where you can use a characterstring for the pad-part.
They should work the same as the DB2 versions.
Here's the LPAD:
CREATE FUNCTION dbo.LPAD
(
#string NVARCHAR(MAX), -- Initial string
#length INT, -- Size of final string
#pad NVARCHAR(MAX) -- Pad string
)
RETURNS VARCHAR(MAX)
AS
BEGIN
RETURN SUBSTRING(REPLICATE(#pad, #length),1,#length - LEN(#string)) + #string;
END
GO
And here is the RPAD:
CREATE FUNCTION dbo.RPAD
(
#string NVARCHAR(MAX), -- Initial string
#length INT, -- Size of final string
#pad NVARCHAR(MAX) -- Pad string
)
RETURNS VARCHAR(MAX)
AS
BEGIN
RETURN #string + SUBSTRING(REPLICATE(#pad, #length),1,#length - LEN(#string));
END
GO
I needed something similar but I couldn't use '+' because then it was interpreted as an addition between the numbers '0000000000' and [seq_no]. So I used concat instead and it worked fine.
select right (concat(replicate('0', 10), next value for seq_no), 10);