SQL to Split between Pipe [duplicate] - sql

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

Related

Get value from a string between special characters in sql server [duplicate]

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

Return all words starting with a character in a column

I have a VARCHAR column with data like this:
abc = :abc and this = :that
I need a query to find all of the special "words" that start with a colon in this column of data. I don't really need any other data (IDs or otherwise) and duplicates would be OK. I can remove duplicates in Excel later if need be. So if this was the only row, I'd like something like this as the output:
SpecialWords
:abc
:that
I'm thinking it'll require a CHARINDEX or something like that. But since there could be more than one special word in the column, I can't just find the first : and strip out the rest.
Any help is greatly appreciated! Thanks in advance!
You have to split this value based on spaces and return only fields that starts with a colon :, i provided 2 solutions to achieve this based on the result type you need (Table or Single Value)
Table-Valued Function
You can create a TV function to split this column into a table:
CREATE FUNCTION [dbo].[GETVALUES]
(
#DelimitedString varchar(8000)
)
RETURNS #tblArray TABLE
(
ElementID int IDENTITY(1,1), -- Array index
Element varchar(1000) -- Array element contents
)
AS
BEGIN
-- Local Variable Declarations
-- ---------------------------
DECLARE #Index smallint,
#Start smallint,
#DelSize smallint
SET #DelSize = 1
-- Loop through source string and add elements to destination table array
-- ----------------------------------------------------------------------
WHILE LEN(#DelimitedString) > 0
BEGIN
SET #Index = CHARINDEX(' ', #DelimitedString)
IF #Index = 0
BEGIN
IF ((LTRIM(RTRIM(#DelimitedString))) LIKE ':%')
INSERT INTO
#tblArray
(Element)
VALUES
(LTRIM(RTRIM(#DelimitedString)))
BREAK
END
ELSE
BEGIN
IF (LTRIM(RTRIM(SUBSTRING(#DelimitedString, 1,#Index - 1)))) LIKE ':%'
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
And you can use it like the following:
DECLARE #SQLStr varchar(100)
SELECT #SQLStr = 'abc = :abc and this = :that and xyz = :asd'
SELECT
*
FROM
dbo.GETVALUES(#SQLStr)
Result:
Scalar-Valued Function
If you need to return a value (not table) so you can use this function which will return on all values separated by (line feed + carridge return CHAR(13) + CHAR(10))
CREATE FUNCTION dbo.GetValues2
(
#DelimitedString varchar(8000)
)
RETURNS varchar(8000)
AS
BEGIN
DECLARE #Index smallint,
#Start smallint,
#DelSize smallint,
#Result varchar(8000)
SET #DelSize = 1
SET #Result = ''
WHILE LEN(#DelimitedString) > 0
BEGIN
SET #Index = CHARINDEX(' ', #DelimitedString)
IF #Index = 0
BEGIN
if (LTRIM(RTRIM(#DelimitedString))) LIKE ':%'
SET #Result = #Result + char(13) + char(10) + (LTRIM(RTRIM(#DelimitedString)))
BREAK
END
ELSE
BEGIN
IF (LTRIM(RTRIM(SUBSTRING(#DelimitedString, 1,#Index - 1)))) LIKE ':%'
SET #Result = #Result + char(13) + char(10) + (LTRIM(RTRIM(SUBSTRING(#DelimitedString, 1,#Index - 1))))
SET #Start = #Index + #DelSize
SET #DelimitedString = SUBSTRING(#DelimitedString, #Start , LEN(#DelimitedString) - #Start + 1)
END
END
return #Result
END
GO
you can use it as the following
DECLARE #SQLStr varchar(100)
SELECT #SQLStr = 'abc = :abc and this = :that and xyz = :asd'
SELECT dbo.GetValues2(#SQLStr)
Result
in the table result line feed are not visible, just copy the data to an editor and it will appears as shown in the image
References
Splitting the string in sql server
One way is to write a specialized SPLIT function. I would suggest getting a TSQL Split function off the internet and see if you can adapt the code to your needs.
Working from scratch, you could write a function that loops over the column value using CHARINDEX until it doesn't find any more : characters.
How about using a charindex?
rextester sample:
create table mytable (testcolumn varchar(20))
insert into mytable values ('this = :that'),('yes'), (':no'), ('abc = :abc')
select right(testcolumn, charindex(':', reverse(testcolumn)) - 1) from mytable
where testcolumn like '%:%'
reference:
SQL Select everything after character
Update
Addressing Sami's:
Didn't see that two words could be in one colon, how about this?
select replace(substring(testcolumn, charindex(':', testcolumn), len(testcolumn)), ':', '')
Update again
I see, the actual statement is this = :that and that = :this
If performance is important then you want to use an inline table valued function to split the string and extract what you need. You could use delimitedSplit8K or delimitedSplit8K_lead for this.
declare #string varchar(8000) = 'abc = :abc and this = :that';
select item
from dbo.DelimitedSplit8K(#string, ' ')
where item like ':%';
returns:
item
------
:abc
:that
And for even better performance than what I posted above you could use ngrams8k like so:
declare #string varchar(8000) = 'abc = :abc and this = :that';
select position, item =
substring(#string, position,
isnull(nullif(charindex(' ',#string,position+1),0),8000)-position)
from dbo.ngrams8k(#string, 1)
where token = ':';
This even gives you the location of the item you are searching for:
position item
---------- -------
7 :abc
23 :that

Is there a LastIndexOf in SQL Server?

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

SQL query using substring to separate data

I have data like this and I need to separate them based on
account=10825 and instance id =0
I have tried this using substring with charindex, but need to improve for further queries as it was not in the same format every time.
data:
1:
Month=12&Year=2015&Accounts=[10825].[44].[1]&Users=[RL665480003].[44]&Culture=en-US&DMSWebService=http%3A%2F%2Fausydapi01.recall.com%2Fdmswebservice%2Fdmswebservice.svc&OLTAccountID=0&OLTInstanceID=0&DaystoDestroy=90&LastLoadDate=12%2F30%2F2015 00%3A00%3A00&connectionString=Data Source%3Damatldb09%3BInitial Catalog%3DLocalizationDB%3BUser ID%3Dlocalization%3BPassword%3Dr3call%3B&ResourceType=BICustomerPortal&LastLoadDateDW=12%2F31%2F2015 12%3A00%3A00 AM&Period=12%2F30%2F2015 00%3A00%3A00
2:
Culture=en-US&Month=12&Year=2015&Accounts=[2784].[6].[1]&Users=[RL042671018].[6]&DMSWebService=http%3A%2F%2Fruss-app.recall.com%2Fdmswebservice%2Fdmswebservice.svc&OLTAccountID=0&OLTInstanceID=0&DaystoDestroy=90&ResourceType=BICustomerPortal&connectionString=Data Source%3Damatldb09%3BInitial Catalog%3DLocalizationDB%3BUser ID%3Dlocalization%3BPassword%3Dr3call%3B
What you've got there looks like a querystring from a browser (or a set of posted variables). So it will vary in length as you say, and also maybe the ordering and quantity of the variables could potentially change too.
So the only reliable way I can think of to do this is to effectively de-serialize the querystring. Since you're using SQL, we'll do it into a temp table. You can use the special characters used in querystrings: "&" (to separate parameters) and "=" (to separate the parameter name from the value) as markers to do this.
DECLARE #data nvarchar(MAX)
-- using your first data sample as an example:
SET #data = 'Month=12&Year=2015&Accounts=[10825].[44].[1]&Users=[RL665480003].[44]&Culture=en-US&DMSWebService=http%3A%2F%2Fausydapi01.recall.com%2Fdmswebservice%2Fdmswebservice.svc&OLTAccountID=0&OLTInstanceID=0&DaystoDestroy=90&LastLoadDate=12%2F30%2F2015 00%3A00%3A00&connectionString=Data Source%3Damatldb09%3BInitial Catalog%3DLocalizationDB%3BUser ID%3Dlocalization%3BPassword%3Dr3call%3B&ResourceType=BICustomerPortal&LastLoadDateDW=12%2F31%2F2015 12%3A00%3A00 AM&Period=12%2F30%2F2015 00%3A00%3A00'
DECLARE #workingtable TABLE
(
ID [int] IDENTITY(1, 1),
ItemID [nvarchar](2000),
ItemValue [nvarchar](2000)
)
DECLARE #Item nvarchar(4000)
,#ItemID [nvarchar](2000)
,#ItemValue [nvarchar](2000)
,#pos int
,#count int
,#row int
,#delimiter1 varchar(1)
,#delimiter2 varchar(1);
SET #delimiter1 = '&';
SET #delimiter2 = '='
SET #data = LTRIM(RTRIM(#data)) + #delimiter1
SET #pos = CHARINDEX(#delimiter1, #data, 1)
SET #count = 0;
SET #row = 0;
IF REPLACE(#data, #delimiter1, '') <> '' -- make sure there are actually any delimited items in the list
BEGIN
WHILE #pos > 0
BEGIN
SET #count = #count + 1
SET #Item = LTRIM(RTRIM(LEFT(#data, #pos - 1))) -- get the querystring parameter and its value
SET #ItemID = LTRIM(RTRIM(LEFT(#Item, CHARINDEX(#delimiter2, #Item) -1))) -- now extract the parameter value
SET #ItemValue = LTRIM(RTRIM(RIGHT(#Item, LEN(#Item) - CHARINDEX(#delimiter2, #Item)))) -- now extract the parameter name
INSERT INTO #workingtable ([ItemID], [ItemValue]) VALUES (#ItemID, #ItemValue) -- store in working table
SET #data = RIGHT(#data, LEN(#data) - #pos) -- remove the item we just extracted from the list
SET #pos = CHARINDEX(#delimiter1, #data, 1) -- reset the position to point to the next delimiter
END
END
SELECT ItemID, ItemValue FROM #workingtable
Once you've got that, you can find the specific value you want very easily. You mentioned getting the first part of the "Accounts" field. So you could get the whole Accounts field like this:
SELECT ItemValue FROM #workingtable WHERE [ItemID] = 'Accounts'
or find the specific part like this:
SELECT SUBSTRING(ItemValue, CHARINDEX('[', [ItemValue]) + 1, CHARINDEX(']', [ItemValue]) - 2) FROM #workingtable WHERE [ItemID] = 'Accounts'
Obviously if you want this functionality to be easily re-usable I suggest you encapsulate it in a function or procedure.
Hope that helps.
Or you could use the following that has the limitation that the len of the accountid is not more than 100 chars.
declare #str varchar(1000)
set #str='Month=12&Year=2015&Users=[RL665480003].[44]&Culture=en-US&DMSWebService=http%3A%2F%2Fausydapi01.recall.com%2Fdmswebservice%2Fdmswebservice.svc&OLTAccountID=0&OLTInstanceID=0&DaystoDestroy=90&LastLoadDate=12%2F30%2F2015 00%3A00%3A00&connectionString=Data Source%3Damatldb09%3BInitial Catalog%3DLocalizationDB%3BUser ID%3Dlocalization%3BPassword%3Dr3call%3B&ResourceType=BICustomerPortal&LastLoadDateDW=12%2F31%2F2015 12%3A00%3A00 AM&Period=12%2F30%2F2015 00%3A00%3A00&Accounts=[10825].[44].[1]'
select SUBSTRING(SUBSTRING(#str, patindex('%Accounts=[[]%', #str) + len('Accounts=[') , 100) , 0 , patindex('%]%', SUBSTRING(#str, patindex('%Accounts=[[]%', #str) + len('Accounts=[') , 100)) )

Query to get only numbers from a string

I have data like this:
string 1: 003Preliminary Examination Plan
string 2: Coordination005
string 3: Balance1000sheet
The output I expect is
string 1: 003
string 2: 005
string 3: 1000
And I want to implement it in SQL.
First create this UDF
CREATE FUNCTION dbo.udf_GetNumeric
(
#strAlphaNumeric VARCHAR(256)
)
RETURNS VARCHAR(256)
AS
BEGIN
DECLARE #intAlpha INT
SET #intAlpha = PATINDEX('%[^0-9]%', #strAlphaNumeric)
BEGIN
WHILE #intAlpha > 0
BEGIN
SET #strAlphaNumeric = STUFF(#strAlphaNumeric, #intAlpha, 1, '' )
SET #intAlpha = PATINDEX('%[^0-9]%', #strAlphaNumeric )
END
END
RETURN ISNULL(#strAlphaNumeric,0)
END
GO
Now use the function as
SELECT dbo.udf_GetNumeric(column_name)
from table_name
SQL FIDDLE
I hope this solved your problem.
Reference
Try this one -
Query:
DECLARE #temp TABLE
(
string NVARCHAR(50)
)
INSERT INTO #temp (string)
VALUES
('003Preliminary Examination Plan'),
('Coordination005'),
('Balance1000sheet')
SELECT LEFT(subsrt, PATINDEX('%[^0-9]%', subsrt + 't') - 1)
FROM (
SELECT subsrt = SUBSTRING(string, pos, LEN(string))
FROM (
SELECT string, pos = PATINDEX('%[0-9]%', string)
FROM #temp
) d
) t
Output:
----------
003
005
1000
Query:
DECLARE #temp TABLE
(
string NVARCHAR(50)
)
INSERT INTO #temp (string)
VALUES
('003Preliminary Examination Plan'),
('Coordination005'),
('Balance1000sheet')
SELECT SUBSTRING(string, PATINDEX('%[0-9]%', string), PATINDEX('%[0-9][^0-9]%', string + 't') - PATINDEX('%[0-9]%',
string) + 1) AS Number
FROM #temp
Please try:
declare #var nvarchar(max)='Balance1000sheet'
SELECT LEFT(Val,PATINDEX('%[^0-9]%', Val+'a')-1) from(
SELECT SUBSTRING(#var, PATINDEX('%[0-9]%', #var), LEN(#var)) Val
)x
Getting only numbers from a string can be done in a one-liner.
Try this :
SUBSTRING('your-string-here', PATINDEX('%[0-9]%', 'your-string-here'), LEN('your-string-here'))
NB: Only works for the first int in the string, ex: abc123vfg34 returns 123.
I found this approach works about 3x faster than the top voted answer. Create the following function, dbo.GetNumbers:
CREATE FUNCTION dbo.GetNumbers(#String VARCHAR(8000))
RETURNS VARCHAR(8000)
AS
BEGIN;
WITH
Numbers
AS (
--Step 1.
--Get a column of numbers to represent
--every character position in the #String.
SELECT 1 AS Number
UNION ALL
SELECT Number + 1
FROM Numbers
WHERE Number < LEN(#String)
)
,Characters
AS (
SELECT Character
FROM Numbers
CROSS APPLY (
--Step 2.
--Use the column of numbers generated above
--to tell substring which character to extract.
SELECT SUBSTRING(#String, Number, 1) AS Character
) AS c
)
--Step 3.
--Pattern match to return only numbers from the CTE
--and use STRING_AGG to rebuild it into a single string.
SELECT #String = STRING_AGG(Character,'')
FROM Characters
WHERE Character LIKE '[0-9]'
--allows going past the default maximum of 100 loops in the CTE
OPTION (MAXRECURSION 8000)
RETURN #String
END
GO
Testing
Testing for purpose:
SELECT dbo.GetNumbers(InputString) AS Numbers
FROM ( VALUES
('003Preliminary Examination Plan') --output: 003
,('Coordination005') --output: 005
,('Balance1000sheet') --output: 1000
,('(111) 222-3333') --output: 1112223333
,('1.38hello#f00.b4r#\-6') --output: 1380046
) testData(InputString)
Testing for performance:
Start off setting up the test data...
--Add table to hold test data
CREATE TABLE dbo.NumTest (String VARCHAR(8000))
--Make an 8000 character string with mix of numbers and letters
DECLARE #Num VARCHAR(8000) = REPLICATE('12tf56se',800)
--Add this to the test table 500 times
DECLARE #n INT = 0
WHILE #n < 500
BEGIN
INSERT INTO dbo.NumTest VALUES (#Num)
SET #n = #n +1
END
Now testing the dbo.GetNumbers function:
SELECT dbo.GetNumbers(NumTest.String) AS Numbers
FROM dbo.NumTest -- Time to complete: 1 min 7s
Then testing the UDF from the top voted answer on the same data.
SELECT dbo.udf_GetNumeric(NumTest.String)
FROM dbo.NumTest -- Time to complete: 3 mins 12s
Inspiration for dbo.GetNumbers
Decimals
If you need it to handle decimals, you can use either of the following approaches, I found no noticeable performance differences between them.
change '[0-9]' to '[0-9.]'
change Character LIKE '[0-9]' to ISNUMERIC(Character) = 1 (SQL treats a single decimal point as "numeric")
Bonus
You can easily adapt this to differing requirements by swapping out WHERE Character LIKE '[0-9]' with the following options:
WHERE Letter LIKE '[a-zA-Z]' --Get only letters
WHERE Letter LIKE '[0-9a-zA-Z]' --Remove non-alphanumeric
WHERE Letter LIKE '[^0-9a-zA-Z]' --Get only non-alphanumeric
With the previous queries I get these results:
'AAAA1234BBBB3333' >>>> Output: 1234
'-çã+0!\aº1234' >>>> Output: 0
The code below returns All numeric chars:
1st output: 12343333
2nd output: 01234
declare #StringAlphaNum varchar(255)
declare #Character varchar
declare #SizeStringAlfaNumerica int
declare #CountCharacter int
set #StringAlphaNum = 'AAAA1234BBBB3333'
set #SizeStringAlfaNumerica = len(#StringAlphaNum)
set #CountCharacter = 1
while isnumeric(#StringAlphaNum) = 0
begin
while #CountCharacter < #SizeStringAlfaNumerica
begin
if substring(#StringAlphaNum,#CountCharacter,1) not like '[0-9]%'
begin
set #Character = substring(#StringAlphaNum,#CountCharacter,1)
set #StringAlphaNum = replace(#StringAlphaNum, #Character, '')
end
set #CountCharacter = #CountCharacter + 1
end
set #CountCharacter = 0
end
select #StringAlphaNum
declare #puvodni nvarchar(20)
set #puvodni = N'abc1d8e8ttr987avc'
WHILE PATINDEX('%[^0-9]%', #puvodni) > 0 SET #puvodni = REPLACE(#puvodni, SUBSTRING(#puvodni, PATINDEX('%[^0-9]%', #puvodni), 1), '' )
SELECT #puvodni
A solution for SQL Server 2017 and later, using TRANSLATE:
DECLARE #T table (string varchar(50) NOT NULL);
INSERT #T
(string)
VALUES
('003Preliminary Examination Plan'),
('Coordination005'),
('Balance1000sheet');
SELECT
result =
REPLACE(
TRANSLATE(
T.string COLLATE Latin1_General_CI_AI,
'abcdefghijklmnopqrstuvwxyz',
SPACE(26)),
SPACE(1),
SPACE(0))
FROM #T AS T;
Output:
result
003
005
1000
The code works by:
Replacing characters a-z (ignoring case & accents) with a space
Replacing spaces with an empty string.
The string supplied to TRANSLATE can be expanded to include additional characters.
I did not have rights to create functions but had text like
["blahblah012345679"]
And needed to extract the numbers out of the middle
Note this assumes the numbers are grouped together and not at the start and end of the string.
select substring(column_name,patindex('%[0-9]%', column_name),patindex('%[0-9][^0-9]%', column_name)-patindex('%[0-9]%', column_name)+1)
from table name
Although this is an old thread its the first in google search, I came up with a different answer than what came before. This will allow you to pass your criteria for what to keep within a string, whatever that criteria might be. You can put it in a function to call over and over again if you want.
declare #String VARCHAR(MAX) = '-123. a 456-78(90)'
declare #MatchExpression VARCHAR(255) = '%[0-9]%'
declare #return varchar(max)
WHILE PatIndex(#MatchExpression, #String) > 0
begin
set #return = CONCAT(#return, SUBSTRING(#string,patindex(#matchexpression, #string),1))
SET #String = Stuff(#String, PatIndex(#MatchExpression, #String), 1, '')
end
select (#return)
This UDF will work for all types of strings:
CREATE FUNCTION udf_getNumbersFromString (#string varchar(max))
RETURNS varchar(max)
AS
BEGIN
WHILE #String like '%[^0-9]%'
SET #String = REPLACE(#String, SUBSTRING(#String, PATINDEX('%[^0-9]%', #String), 1), '')
RETURN #String
END
Just a little modification to #Epsicron 's answer
SELECT SUBSTRING(string, PATINDEX('%[0-9]%', string), PATINDEX('%[0-9][^0-9]%', string + 't') - PATINDEX('%[0-9]%',
string) + 1) AS Number
FROM (values ('003Preliminary Examination Plan'),
('Coordination005'),
('Balance1000sheet')) as a(string)
no need for a temporary variable
Firstly find out the number's starting length then reverse the string to find out the first position again(which will give you end position of number from the end). Now if you deduct 1 from both number and deduct it from string whole length you'll get only number length. Now get the number using SUBSTRING
declare #fieldName nvarchar(100)='AAAA1221.121BBBB'
declare #lenSt int=(select PATINDEX('%[0-9]%', #fieldName)-1)
declare #lenEnd int=(select PATINDEX('%[0-9]%', REVERSE(#fieldName))-1)
select SUBSTRING(#fieldName, PATINDEX('%[0-9]%', #fieldName), (LEN(#fieldName) - #lenSt -#lenEnd))
T-SQL function to read all the integers from text and return the one at the indicated index, starting from left or right, also using a starting search term (optional):
create or alter function dbo.udf_number_from_text(
#text nvarchar(max),
#search_term nvarchar(1000) = N'',
#number_position tinyint = 1,
#rtl bit = 0
) returns int
as
begin
declare #result int = 0;
declare #search_term_index int = 0;
if #text is null or len(#text) = 0 goto exit_label;
set #text = trim(#text);
if len(#text) = len(#search_term) goto exit_label;
if len(#search_term) > 0
begin
set #search_term_index = charindex(#search_term, #text);
if #search_term_index = 0 goto exit_label;
end;
if #search_term_index > 0
if #rtl = 0
set #text = trim(right(#text, len(#text) - #search_term_index - len(#search_term) + 1));
else
set #text = trim(left(#text, #search_term_index - 1));
if len(#text) = 0 goto exit_label;
declare #patt_number nvarchar(10) = '%[0-9]%';
declare #patt_not_number nvarchar(10) = '%[^0-9]%';
declare #number_start int = 1;
declare #number_end int;
declare #found_numbers table (id int identity(1,1), val int);
while #number_start > 0
begin
set #number_start = patindex(#patt_number, #text);
if #number_start > 0
begin
if #number_start = len(#text)
begin
insert into #found_numbers(val)
select cast(substring(#text, #number_start, 1) as int);
break;
end;
else
begin
set #text = right(#text, len(#text) - #number_start + 1);
set #number_end = patindex(#patt_not_number, #text);
if #number_end = 0
begin
insert into #found_numbers(val)
select cast(#text as int);
break;
end;
else
begin
insert into #found_numbers(val)
select cast(left(#text, #number_end - 1) as int);
if #number_end = len(#text)
break;
else
begin
set #text = trim(right(#text, len(#text) - #number_end));
if len(#text) = 0 break;
end;
end;
end;
end;
end;
if #rtl = 0
select #result = coalesce(a.val, 0)
from (select row_number() over (order by m.id asc) as c_row, m.val
from #found_numbers as m) as a
where a.c_row = #number_position;
else
select #result = coalesce(a.val, 0)
from (select row_number() over (order by m.id desc) as c_row, m.val
from #found_numbers as m) as a
where a.c_row = #number_position;
exit_label:
return #result;
end;
Example:
select dbo.udf_number_from text(N'Text text 10 text, 25 term', N'term',2,1);
returns 10;
This is one of the simplest and easiest one. This will work on the entire String for multiple occurences as well.
CREATE FUNCTION dbo.fn_GetNumbers(#strInput NVARCHAR(500))
RETURNS NVARCHAR(500)
AS
BEGIN
DECLARE #strOut NVARCHAR(500) = '', #intCounter INT = 1
WHILE #intCounter <= LEN(#strInput)
BEGIN
SELECT #strOut = #strOut + CASE WHEN SUBSTRING(#strInput, #intCounter, 1) LIKE '[0-9]' THEN SUBSTRING(#strInput, #intCounter, 1) ELSE '' END
SET #intCounter = #intCounter + 1
END
RETURN #strOut
END
Following a solution using a single common table expression (CTE).
DECLARE #s AS TABLE (id int PRIMARY KEY, value nvarchar(max));
INSERT INTO #s
VALUES
(1, N'003Preliminary Examination Plan'),
(2, N'Coordination005'),
(3, N'Balance1000sheet');
SELECT * FROM #s ORDER BY id;
WITH t AS (
SELECT
id,
1 AS i,
SUBSTRING(value, 1, 1) AS c
FROM
#s
WHERE
LEN(value) > 0
UNION ALL
SELECT
t.id,
t.i + 1 AS i,
SUBSTRING(s.value, t.i + 1, 1) AS c
FROM
t
JOIN #s AS s ON t.id = s.id
WHERE
t.i < LEN(s.value)
)
SELECT
id,
STRING_AGG(c, N'') WITHIN GROUP (ORDER BY i ASC) AS value
FROM
t
WHERE
c LIKE '[0-9]'
GROUP BY
id
ORDER BY
id;
DECLARE #index NVARCHAR(20);
SET #index = 'abd565klaf12';
WHILE PATINDEX('%[0-9]%', #index) != 0
BEGIN
SET #index = REPLACE(#index, SUBSTRING(#index, PATINDEX('%[0-9]%', #index), 1), '');
END
SELECT #index;
One can replace [0-9] with [a-z] if numbers only are wanted with desired castings using the CAST function.
If we use the User Define Function, the query speed will be greatly reduced. This code extracts the number from the string....
SELECT
Reverse(substring(Reverse(rtrim(ltrim( substring([FieldName] , patindex('%[0-9]%', [FieldName] ) , len([FieldName]) )))) , patindex('%[0-9]%', Reverse(rtrim(ltrim( substring([FieldName] , patindex('%[0-9]%', [FieldName] ) , len([FieldName]) )))) ), len(Reverse(rtrim(ltrim( substring([FieldName] , patindex('%[0-9]%', [FieldName] ) , len([FieldName]) ))))) )) NumberValue
FROM dbo.TableName
CREATE OR REPLACE FUNCTION count_letters_and_numbers(input_string TEXT)
RETURNS TABLE (letters INT, numbers INT) AS $$
BEGIN
RETURN QUERY SELECT
sum(CASE WHEN input_string ~ '[A-Za-z]' THEN 1 ELSE 0 END) as letters,
sum(CASE WHEN input_string ~ '[0-9]' THEN 1 ELSE 0 END) as numbers
FROM unnest(string_to_array(input_string, '')) as input_string;
END;
$$ LANGUAGE plpgsql;
For the hell of it...
This solution is different to all earlier solutions, viz:
There is no need to create a function
There is no need to use pattern matching
There is no need for a temporary table
This solution uses a recursive common table expression (CTE)
But first - note the question does not specify where such strings are stored. In my solution below, I create a CTE as a quick and dirty way to put these strings into some kind of "source table".
Note also - this solution uses a recursive common table expression (CTE) - so don't get confused by the usage of two CTEs here. The first is simply to make the data avaliable to the solution - but it is only the second CTE that is required in order to solve this problem. You can adapt the code to make this second CTE query your existing table, view, etc.
Lastly - my coding is verbose, trying to use column and CTE names that explain what is going on and you might be able to simplify this solution a little. I've added in a few pseudo phone numbers with some (expected and atypical, as the case may be) formatting for the fun of it.
with SOURCE_TABLE as (
select '003Preliminary Examination Plan' as numberString
union all select 'Coordination005' as numberString
union all select 'Balance1000sheet' as numberString
union all select '1300 456 678' as numberString
union all select '(012) 995 8322 ' as numberString
union all select '073263 6122,' as numberString
),
FIRST_CHAR_PROCESSED as (
select
len(numberString) as currentStringLength,
isNull(cast(try_cast(replace(left(numberString, 1),' ','z') as tinyint) as nvarchar),'') as firstCharAsNumeric,
cast(isNull(cast(try_cast(nullIf(left(numberString, 1),'') as tinyint) as nvarchar),'') as nvarchar(4000)) as newString,
cast(substring(numberString,2,len(numberString)) as nvarchar) as remainingString
from SOURCE_TABLE
union all
select
len(remainingString) as currentStringLength,
cast(try_cast(replace(left(remainingString, 1),' ','z') as tinyint) as nvarchar) as firstCharAsNumeric,
cast(isNull(newString,'') as nvarchar(3999)) + isNull(cast(try_cast(nullIf(left(remainingString, 1),'') as tinyint) as nvarchar(1)),'') as newString,
substring(remainingString,2,len(remainingString)) as remainingString
from FIRST_CHAR_PROCESSED fcp2
where fcp2.currentStringLength > 1
)
select
newString
,* -- comment this out when required
from FIRST_CHAR_PROCESSED
where currentStringLength = 1
So what's going on here?
Basically in our CTE we are selecting the first character and using try_cast (see docs) to cast it to a tinyint (which is a large enough data type for a single-digit numeral). Note that the type-casting rules in SQL Server say that an empty string (or a space, for that matter) will resolve to zero, so the nullif is added to force spaces and empty strings to resolve to null (see discussion) (otherwise our result would include a zero character any time a space is encountered in the source data).
The CTE also returns everything after the first character - and that becomes the input to our recursive call on the CTE; in other words: now let's process the next character.
Lastly, the field newString in the CTE is generated (in the second SELECT) via concatenation. With recursive CTEs the data type must match between the two SELECT statements for any given column - including the column size. Because we know we are adding (at most) a single character, we are casting that character to nvarchar(1) and we are casting the newString (so far) as nvarchar(3999). Concatenated, the result will be nvarchar(4000) - which matches the type casting we carry out in the first SELECT.
If you run this query and exclude the WHERE clause, you'll get a sense of what's going on - but the rows may be in a strange order. (You won't necessarily see all rows relating to a single input value grouped together - but you should still be able to follow).
Hope it's an interesting option that may help a few people wanting a strictly expression-based solution.
In Oracle
You can get what you want using this:
SUBSTR('ABCD1234EFGH',REGEXP_INSTR ('ABCD1234EFGH', '[[:digit:]]'),REGEXP_COUNT ('ABCD1234EFGH', '[[:digit:]]'))
Sample Query:
SELECT SUBSTR('003Preliminary Examination Plan ',REGEXP_INSTR ('003Preliminary Examination Plan ', '[[:digit:]]'),REGEXP_COUNT ('003Preliminary Examination Plan ', '[[:digit:]]')) SAMPLE1,
SUBSTR('Coordination005',REGEXP_INSTR ('Coordination005', '[[:digit:]]'),REGEXP_COUNT ('Coordination005', '[[:digit:]]')) SAMPLE2,
SUBSTR('Balance1000sheet',REGEXP_INSTR ('Balance1000sheet', '[[:digit:]]'),REGEXP_COUNT ('Balance1000sheet', '[[:digit:]]')) SAMPLE3 FROM DUAL
If you are using Postgres and you have data like '2000 - some sample text' then try substring and position combination, otherwise if in your scenario there is no delimiter, you need to write regex:
SUBSTRING(Column_name from 0 for POSITION('-' in column_name) - 1) as
number_column_name