I have a host information table stored in a database of SQL Server, and the table has a text column storing a string formatted like Ansible inventory. See the text below for a sample item in the text column.
host-001.servers.company.com desc='Production Web Cache' env='Prod' patch_round='Beta' dc='Main' rhel_v='7.6' primary='admin#company.com' secondary='manager#company.com'
I need to extract certain attributes from the text column, e.g. extract desc='Production Web Cache', and get its value Production Web Cache. I want to use regular expression in a SQL query and hope to get some pointers.
Or, if you know another way of achieving this purpose, I will also highly appreciate your hints. Let me know if you need more clarification.
A very similar approach to John. I use a JSON splitter firstly to get the data into parts, though this puts the value with the next header. I use CHARINDEX to find the end of the value, and then use that LEFT/STUFF to get the 2 values into their respective parts. Then I use LAG to get the actual header, rather that the next values header. Finally, I remove the surrounding quotes.
This follows on the assumptions from my comment:
A domain is present at the start and followed by a space.
Values cannot contain =.
All values are enclosed in single quotes (') and all names are not
Note I don't include the domain in the results, but the SQL should give you more than enough to work out how to add it:
DECLARE #YourString nvarchar(4000) = N'host-001.servers.company.com desc=''Production Web Cache'' env=''Prod'' patch_round=''Beta'' dc=''Main'' rhel_v=''7.6'' primary=''admin#company.com'' secondary=''manager#company.com''';
WITH CTE AS(
SELECT *,
LAG(ContentHeader) OVER (ORDER BY [Key]) AS ActualHeader
FROM (VALUES(#YourString))V(YourString)
CROSS APPLY(VALUES(STUFF(#YourString, 1, CHARINDEX(N' ',#YourString),N'')))S(NewString)
CROSS APPLY OPENJSON('["' + REPLACE(NewString,'=','","') + '"]')OJ
CROSS APPLY(VALUES(NULLIF(CHARINDEX('''',OJ.[value],2),0)))CI(I)
CROSS APPLY(VALUES(LEFT(OJ.[Value],CI.I),STUFF(OJ.[Value],1,ISNULL(CI.I+1,0),'')))P(ContentValue,ContentHeader))
SELECT ActualHeader AS Header,
REPLACE([ContentValue],'''','') AS [Value]
FROM CTE
WHERE ActualHeader IS NOT NULL;
db<>fiddle
A little ugly, but using a bit of JSON (to GTD the sequence) and the window function lead() over()
Example
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'host-001.servers.company.com desc=''Production Web Cache'' env=''Prod'' patch_round=''Beta'' dc=''Main'' rhel_v=''7.6'' primary=''admin#company.com'' secondary=''manager#company.com''')
Select A.ID
,Host = left(SomeCol,charindex(' ',SomeCol+' '))
,B.*
From #YourTable A
Cross Apply (
Select Item = ltrim(rtrim(right(Value,charindex(' ',reverse(Value)+' '))))
,Value = ltrim(rtrim(replace(
IsNull(lead( left(Value,nullif(len(Value)+1-charindex(' ',reverse(Value)+' '),0)),1) over (order by [Key])
,lead(right(Value,charindex(' ',reverse(Value)+' ')),1) over (order by [key])
),'''','')))
From OpenJSON( '["'+replace(string_escape(SomeCol,'json'),'=','","')+'"]' )
) B
Where B.Value is not null
Results
ID Host Item Value
1 host-001.servers.company.com desc Production Web Cache
1 host-001.servers.company.com env Prod
1 host-001.servers.company.com patch_round Beta
1 host-001.servers.company.com dc Main
1 host-001.servers.company.com rhel_v 7.6
1 host-001.servers.company.com primary admin#company.com
1 host-001.servers.company.com secondary manager#company.com
EDIT - Injected "HOST="
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'host-001.servers.company.com desc=''Production Web Cache'' env=''Prod'' patch_round=''Beta'' dc=''Main'' rhel_v=''7.6'' primary=''admin#company.com'' secondary=''manager#company.com''')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select Item = ltrim(rtrim(right(Value,charindex(' ',reverse(Value)+' '))))
,Value = ltrim(rtrim(replace(
IsNull(lead(left(Value,nullif(len(Value)+1-charindex(' ',reverse(Value)+' '),0)),1) over (order by [Key])
,lead(right(Value,charindex(' ',reverse(Value)+' ')),1) over (order by [key])
),'''','')))
From OpenJSON( '["'+replace(string_escape('host='+SomeCol,'json'),'=','","')+'"]' )
) B
Where B.Value is not null
Results
ID Item Value
1 host host-001.servers.company.com
1 desc Production Web Cache
1 env Prod
1 patch_round Beta
1 dc Main
1 rhel_v 7.6
1 primary admin#company.com
1 secondary manager#company.com
Ideally, your data should be stored in separate columns. But if you are going to cram it into one column, at least use a recognized format such as XML or JSON.
Given that single quotes are valid XML attribute delimiters, you can transform this into XML and use XQuery.
It's not pretty, because the hostname value is not delimited
SELECT
v3.n.value('#host','varchar(255)'),
v3.n.value('#desc','varchar(1000)')
FROM t
CROSS APPLY(VALUES(
CHARINDEX(' ', t.value)
)) v1(space)
CROSS APPLY(VALUES(
CAST(
'<x host=''' +
CASE WHEN v1.space = 0
THEN t.value
ELSE LEFT(t.value, v1.space - 1) + '''' + SUBSTRING(t.value, v1.space, LEN(t.value))
END +
' />'
AS xml)
)) v2(xml)
CROSS APPLY v2.xml.nodes('x') v3(n);
db<>fiddle
Related
I have a text column that contains values like CN-CHGO-BNSF.
I need to reverse the order of the segments between hyphens. So the example above would be converted to BNSF-CHGO-CN. I can easily do this in C# code, but here I could optimize my task if I could do it within a query.
Is there any way to do this in a SQL query? I'm using Entity Framework and SQL Server. Is this possible?
The number of segments will be one or more. (The number of hyphens will be zero or more.)
Examples
Input
Output
BNSF
BNSF
CHGO-BNSF
BNSF-CHGO
CN-CHGO-BNSF
BNSF-CHGO-CN
FXE-EAGPA-BNSF-ROBSP-(RVPR)
(RVPR)-ROBSP-BNSF-EAGPA-FXE
With SQL Server 2022, it looks like STRING_SPLIT is being added (which I'm not currently using). I don't know if that could be used for this.
If you have a maximum number of positions, you can use a bit of JSON in concert with concat()
Example
Declare #YourTable Table ([SomeCol] varchar(50)) Insert Into #YourTable Values
('BNSF')
,('CHGO-BNSF')
,('CN-CHGO-BNSF')
,('FXE-EAGPA-BNSF-ROBSP-(RVPR)')
Select A.*
,NewVal = stuff( concat('-'+JSON_VALUE(JS,'$[6]')
,'-'+JSON_VALUE(JS,'$[5]')
,'-'+JSON_VALUE(JS,'$[4]')
,'-'+JSON_VALUE(JS,'$[3]')
,'-'+JSON_VALUE(JS,'$[2]')
,'-'+JSON_VALUE(JS,'$[1]')
,'-'+JSON_VALUE(JS,'$[0]')
),1,1,'')
From #YourTable A
Cross Apply (values ( '["'+replace(string_escape(SomeCol,'json'),'-','","')+'"]' ) ) B(JS)
Results
EDIT Using String_AGG
Select A.*
,B.NewValue
From #YourTable A
Cross Apply ( Select NewValue = string_agg(value,'-') within group (ORDER BY convert(int,[key]) desc)
from openjson( '["'+replace(string_escape(SomeCol,'json'),'-','","')+'"]' )
) B
Please try the following solution.
It is generic and will work for any number of tokens in a column.
It is using XML and XQuery to tokenize tokens in a column.
After that XQuery's FLWOR expression is traversing tokens in a reverse order via order by $pos descending.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, tokens VARCHAR(MAX));
INSERT Into #tbl (tokens) VALUES
('BNSF'),
('CHGO-BNSF'),
('CN-CHGO-BNSF'),
('FXE-EAGPA-BNSF-ROBSP-(RVPR)');
-- DDL and sample data population, end
DECLARE #separator CHAR(1) = '-';
SELECT t.*
, REPLACE(c.query('
for $x in /root/r/text()
let $pos := count(/root/r[. << $x[1]])
order by $pos descending
return data($x)
').value('.','VARCHAR(MAX)'),SPACE(1), #separator) AS Result
FROM #tbl AS t
CROSS APPLY (SELECT TRY_CAST('<root><r><![CDATA[' +
REPLACE(tokens, #separator, ']]></r><r><![CDATA[') +
']]></r></root>' AS XML)) AS t1(c);
Output
ID
tokens
Result
1
BNSF
BNSF
2
CHGO-BNSF
BNSF-CHGO
3
CN-CHGO-BNSF
BNSF-CHGO-CN
4
FXE-EAGPA-BNSF-ROBSP-(RVPR)
(RVPR)-ROBSP-BNSF-EAGPA-FXE
I have my data in a form of 2 coma separated strings
DECLARE #ids nvarchar(max) = '1,2,3'
DECLARE #guids nvarchar(max) =
'0000000-0001-0000-0000-000000000000,
`0000000-0022-0000-0000-000000000000`,
`0000000-0013-0000-0000-000000000000'`
I need them in a table as separate columns based on their position in the string
Table1
| Id | Guid |
| 1 | 0000000-0001-0000-0000-000000000000 |
| 2 | 0000000-0022-0000-0000-000000000000 |
| 3 | 0000000-0013-0000-0000-000000000000 |
I can split both strings into separate tables by using
DECLARE #split_ids
(value nvarchar(max))
DECLARE #xml xml
SET #xml = N'<root><r>' + replace(#ids, ',' ,'</r><r>') + '</r></root>'
INSERT INTO #split_ids(Value)
SELECT r.value('.','nvarchar(max)')
FROM #xml.nodes('//root/r') as records(r)
I've tried
SELECT t1.*, t2.*
FROM (SELECT t1.*, row_number() OVER (ORDER BY [Value]) as seqnum
from cte_Ids t1
) t1 FULL OUTER JOIN
(SELECT t2.*, row_number() OVER (ORDER BY [Value]) as seqnum
from cte_barcodes t2
) t2
ON t1.seqnum = t2.seqnum;
But that orders the tables by Value and my data is random and can't be ordered.
Is there a way of joining tables based on their row numbers without ordering them first?
Or is there another way of inserting data from a string to a table?
You do not need to split and/or insert the input data into separate tables. In this situation you simply need to parse the input strings and get the substrings and their ordinal positions (an XML-based approach or a splitter function are possible solutions).
But if you use SQL Server 2016+, a JSON-based approach is also an option. The idea is to transform the strings into valid JSON arrays (1,2,3 into [1,2,3]), parse the arrays with OPENJSON() and join the tables returned from OPENJSON() calls. As is explained in the documentation, the columns that OPENJSON() function returns (when the default schema is used) are key, value and type and in case of JSON array, the key column holds the index of the element in the specified array.
DECLARE #ids nvarchar(max) = N'1,2,3'
DECLARE #guids nvarchar(max) = N'0000000-0001-0000-0000-000000000000,0000000-0022-0000-0000-000000000000,0000000-0013-0000-0000-000000000000'
SELECT j1.[value] AS Id, j2.[value] AS Guid
FROM OPENJSON(CONCAT('[', #ids, ']')) j1
JOIN OPENJSON(CONCAT('["', REPLACE(#guids, ',', '","'), '"]')) j2 ON j1.[key] = j2.[key]
Result:
Id Guid
1 0000000-0001-0000-0000-000000000000
2 0000000-0022-0000-0000-000000000000
3 0000000-0013-0000-0000-000000000000
You need row numbering over initial order, this means that you should use some constant expression in window function order_by clause.
SQL server does not allow use constants directly, but over(order_by (select 1)) is allowed:
SELECT t1.*, t2.*
FROM (SELECT t1.*, row_number() OVER (ORDER BY (select 1)) as seqnum
from cte_Ids t1
) t1 FULL OUTER JOIN
(SELECT t2.*, row_number() OVER (ORDER BY (select 1)) as seqnum
from cte_barcodes t2
) t2
ON t1.seqnum = t2.seqnum;
Note that this doesn't guarantee initial order (it will be unspecified), but often it behaves correctly :)
One of solutions is to parse your comma separated values in a loop (using WHILE) from both variables. Then you could insert those extracted in the same iteration values at once as one row to a table.
One solution uses recursive CTEs:
with cte as (
select cast(null as nvarchar(max)) as id, cast(null as nvarchar(max)) as guid, #ids + ',' as rest_ids, #guids + ',' as rest_guids, 0 as lev
union all
select left(rest_ids, charindex(',', rest_ids) - 1),
left(rest_guids, charindex(',', rest_guids) - 1),
stuff(rest_ids, 1, charindex(',', rest_ids), ''),
stuff(rest_guids, 1, charindex(',', rest_guids), ''),
lev + 1
from cte
where rest_ids <> ''
)
select id, guid
from cte
where lev > 0;
Here is a db<>fiddle.
I have a field which is a concatenation of single letters. I am trying to order these strings within a view. These values can't be hard coded as there are too many. Is someone able to provide some guidance on the function to use to achieve the desired output below? I am using MSSQL.
Current output
CustID | Code
123 | BCA
Desired output
CustID | Code
123 | ABC
I have tried using a UDF
CREATE FUNCTION [dbo].[Alphaorder] (#str VARCHAR(50))
returns VARCHAR(50)
BEGIN
DECLARE #len INT,
#cnt INT =1,
#str1 VARCHAR(50)='',
#output VARCHAR(50)=''
SELECT #len = Len(#str)
WHILE #cnt <= #len
BEGIN
SELECT #str1 += Substring(#str, #cnt, 1) + ','
SET #cnt+=1
END
SELECT #str1 = LEFT(#str1, Len(#str1) - 1)
SELECT #output += Sp_data
FROM (SELECT Split.a.value('.', 'VARCHAR(100)') Sp_data
FROM (SELECT Cast ('<M>' + Replace(#str1, ',', '</M><M>') + '</M>' AS XML) AS Data) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)) A
ORDER BY Sp_data
RETURN #output
END
This works when calling one field
ie.
Select CustID, dbo.alphaorder(Code)
from dbo.source
where custid = 123
however when i try to apply this to top(10) i receive the error
"Invalid length parameter passed to the LEFT or SUBSTRING function."
Keeping in mind my source has ~4million records, is this still the best solution?
Unfortunately i am not able to normalize the data into a separate table with records for each Code.
This doesn't rely on a id column to join with itself, performance is almost as fast
as the answer by #Shnugo:
SELECT
CustID,
(
SELECT
chr
FROM
(SELECT TOP(LEN(Code))
SUBSTRING(Code,ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),1)
FROM sys.messages) A(Chr)
ORDER by chr
FOR XML PATH(''), type).value('.', 'varchar(max)'
) As CODE
FROM
source t
First of all: Avoid loops...
You can try this:
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(100));
INSERT INTO #tbl VALUES ('ABC')
,('JSKEzXO')
,('QKEvYUJMKRC');
--the cte will create a list of all your strings separated in single characters.
--You can check the output with a simple SELECT * FROM SeparatedCharacters instead of the actual SELECT
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,(
SELECT Chr As [*]
FROM SeparatedCharacters sc1
WHERE sc1.ID=t.ID
ORDER BY sc1.Chr
FOR XML PATH(''),TYPE
).value('.','nvarchar(max)') AS Sorted
FROM #tbl t;
The result
ID YourString Sorted
1 ABC ABC
2 JSKEzXO EJKOSXz
3 QKEvYUJMKRC CEJKKMQRUvY
The idea in short
The trick is the first CROSS APPLY. This will create a tally on-the-fly. You will get a resultset with numbers from 1 to n where n is the length of the current string.
The second apply uses this number to get each character one-by-one using SUBSTRING().
The outer SELECT calls from the orginal table, which means one-row-per-ID and use a correalted sub-query to fetch all related characters. They will be sorted and re-concatenated using FOR XML. You might add DISTINCT in order to avoid repeating characters.
That's it :-)
Hint: SQL-Server 2017+
With version v2017 there's the new function STRING_AGG(). This would make the re-concatenation very easy:
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,STRING_AGG(sc.Chr,'') WITHIN GROUP(ORDER BY sc.Chr) AS Sorted
FROM SeparatedCharacters sc
GROUP BY ID,YourString;
Considering your table having good amount of rows (~4 Million), I would suggest you to create a persisted calculated field in the table, to store these values. As calculating these values at run time in a view, will lead to performance problems.
If you are not able to normalize, add this as a denormalized column to the existing table.
I think the error you are getting could be due to empty codes.
If LEN(#str) = 0
BEGIN
SET #output = ''
END
ELSE
BEGIN
... EXISTING CODE BLOCK ...
END
I can suggest to split string into its characters using referred SQL function.
Then you can concatenate string back, this time ordered alphabetically.
Are you using SQL Server 2017? Because with SQL Server 2017, you can use SQL String_Agg string aggregation function to concatenate characters splitted in an ordered way as follows
select
t.CustId, string_agg(strval, '') within GROUP (order by strval)
from CharacterTable t
cross apply dbo.SPLIT(t.code) s
where strval is not null
group by CustId
order by CustId
If you are not working on SQL2017, then you can follow below structure using SQL XML PATH for concatenation in SQL
select
CustId,
STUFF(
(
SELECT
'' + strval
from CharacterTable ct
cross apply dbo.SPLIT(t.code) s
where strval is not null
and t.CustId = ct.CustId
order by strval
FOR XML PATH('')
), 1, 0, ''
) As concatenated_string
from CharacterTable t
order by CustId
I'm using SQL Server and I'm trying to find results but I would like to get the results in the same order as I had input the conditions.
My code:
SELECT
AccountNumber, EndDate
FROM
Accounts
WHERE
AccountNumber IN (212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689) -- I would like the results to be in the same order as these numbers.
Here is an in-line approach
Example
Declare #List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689'
Select A.AccountNumber
,A.EndDate
From Accounts A
Join (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = v.value('(./text())[1]', 'int')
From (values (convert(xml,'<x>' + replace(#List,',','</x><x>')+'</x>'))) x(n)
Cross Apply n.nodes('x') node(v)
) B on A.AccountNumber = B.RetVal
Order By B.RetSeq
EDIT - the subquery Returns
RetSeq RetVal
1 212345
2 312345
3 145687
4 658975
5 256987
6 365874
7 568974
8 124578
9 125689
You can replace IN with a JOIN, and set a field for ordering, like this:
SELECT AccountNumber , EndDate
FROM Accounts a
JOIN (
SELECT 212345 AS Number, 1 AS SeqOrder
UNION ALL
SELECT 312345 AS Number, 2 AS SeqOrder
UNION ALL
SELECT 145687 AS Number, 3 AS SeqOrder
UNION ALL
... -- and so on
) AS inlist ON inlist.Number = a.AccountNumber
ORDER BY inlist.SeqOrder
I will offer one more approach I just found out, but this needs v2016. Regrettfully the developers forgot to include the index into the resultset of STRING_SPLIT(), but this would work and is documented:
A solution via FROM OPENJSON():
DECLARE #str VARCHAR(100) = 'val1,val2,val3';
SELECT *
FROM OPENJSON('["' + REPLACE(#str,',','","') + '"]');
The result
key value type
0 val1 1
1 val2 1
2 val3 1
The documentation tells clearly:
When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.
This is not an answer, just some test-code to check John Cappelletti's approach.
DECLARE #tbl TABLE(ID INT IDENTITY,SomeGuid UNIQUEIDENTIFIER);
--Create more than 6 mio rows with an running number and a changing Guid
WITH tally AS (SELECT ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) AS Nmbr
FROM master..spt_values v1
CROSS JOIN master..spt_values v2)
INSERT INTO #tbl
SELECT NEWID() from tally;
SELECT COUNT(*) FROM #tbl; --6.325.225 on my machine
--Create an XML with nothing more than a list of GUIDs in the order of the table's ID
DECLARE #xml XML=
(SELECT SomeGuid FRom #tbl ORDER BY ID FOR XML PATH(''),ROOT('root'),TYPE);
--Create one invalid entry
UPDATE #tbl SET SomeGuid = NEWID() WHERE ID=10000;
--Read all GUIDs out of the XML and number them
DECLARE #tbl2 TABLE(Position INT,TheGuid UNIQUEIDENTIFIER);
INSERT INTO #tbl2(Position,TheGuid)
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
,g.value(N'text()[1]',N'uniqueidentifier')
FROM #xml.nodes(N'/root/SomeGuid') AS A(g);
--then JOIN them via "Position" and check,
--if there are rows, where not the same values get into the same row.
SELECT *
FROM #tbl t
INNER JOIN #tbl2 t2 ON t2.Position=t.ID
WHERE t.SomeGuid<>t2.TheGuid;
At least in this simple case I always get exactly only the one record back which was invalidated...
Okay, after some re-thinking I'll offer the ultimative XML based type-safe and sort-safe splitter:
Declare #List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689';
DECLARE #delimiter VARCHAR(10)=', ';
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML) AS ListXml
)
,Tally(Nmbr) As
(
SELECT TOP((SELECT ElementCount FROM Casted)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values v1 CROSS JOIN master..spt_values v2
)
SELECT Tally.Nmbr AS Position
,(SELECT ListXml.value('(/x[sql:column("Tally.Nmbr")])[1]','int') FROM Casted) AS Item
FROM Tally;
The trick is to create a list of running numbers with the fitting number of element (a number's table was even better) and to pick the elements according to their position.
Hint: This is rather slow...
UPDATE: even better:
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML)
.query('
for $x in /x
return <x p="{count(/x[. << $x])}">{$x/text()[1]}</x>
') AS ListXml
)
SELECT x.value('#p','int') AS Position
,x.value('text()[1]','int') AS Item
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
Elements are create as
<x p="99">TheValue</x>
Regrettfully the XQuery function position() is not available to retrieve the value. But you can use the trick to count all elements before a given node. this is scaling badly, as this count must be performed over and over. The more elements the worse it goes...
UPDATE2: With a known count of elements one might use this (much better performance)
Use XQuery to iterate a literally given list:
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML)
.query('
for $i in (1,2,3,4,5,6,7,8,9)
return <x p="{$i}">{/x[$i]/text()[1]}</x>
') AS ListXml
)
SELECT x.value('#p','int') AS Position
,x.value('text()[1]','int') AS Item
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
In Azure SQL, there is now extended version of STRING_SPLIT which also can return the order of items if the third optional argument enable_ordinal is set to 1.
Then this simple task is finally easy:
DECLARE #string AS varchar(200) = 'a/b/c/d/e'
DECLARE #position AS int = 3
SELECT value FROM STRING_SPLIT(#string, '/', 1) WHERE ordinal = #position
Unfortunately not available in SQL Server 2019, only in Azure for now, lets hope it will be in SQL Server 2022.
For example:
Pillars 101 in an apartment
Zuzu Durga International Hotel
Wyndham Garden Fresh Meadows
Need to sort the above as,
101 an apartment in Pillars
Durga Hotel International Zuzu
Fresh Garden Meadows Wyndham
Try this:
DECLARE #tbl TABLE(YourString VARCHAR(100));
INSERT INTO #tbl VALUES
('Pillars 101 in an apartment')
,('Zuzu Durga International Hotel')
,('Wyndham Garden Fresh Meadows');
SELECT CAST('<x>' + REPLACE((SELECT YourString AS [*] FOR XML PATH('')),' ','</x><x>') + '</x>' AS XML).query
('
for $x in /x
order by $x
return
concat($x/text()[1], " ")
').value('.','varchar(max)')
FROM #tbl;
The code will frist transfer your text in an XML like <x>Pillars</x><x>101</x> ....
Then a FLWOR XQuery is used to return the text parts sorted.
The last call to .value() will return the sorted fragments as text again.
The result
101 Pillars an apartment in
Durga Hotel International Zuzu
Fresh Garden Meadows Wyndham
Final statement
This code is kind of an exercise. Your design is really bad and should be changed...
So there's nothing that you can do natively. If you want to sort the values just as a return value, i.e. not update the database itself, you can transform the results with either a stored procedure or perhaps a view.
So let's construct an answer.
Let's just assume you want to do it visually, for a single row. If you have SQL 2016 you can use STRING_SPLIT but SQL Fiddle doesn't, so I used a common UDF fnSplitString
http://sqlfiddle.com/#!6/7194d/2
SELECT value
FROM fnSplitString('Pillars 101 in an apartment', ' ')
WHERE RTRIM(value) <> '';
That gives me each word, split out. What about ordering it?
SELECT value
FROM fnSplitString('Pillars 101 in an apartment', ' ')
WHERE RTRIM(value) <> ''
ORDER BY value;
And if I want to do it for each row in the DB table I have? http://sqlfiddle.com/#!6/7194d/8
SELECT split.value
FROM [Data] d
CROSS APPLY dbo.fnSplitString(IsNull(d.Value,''), ' ') AS split
WHERE RTRIM(split.value) <> ''
ORDER BY value;
That's sort of helpful, except now all my words are jumbled. Let's go back to our original query and identify each row. Each row probably has an Identity column on it. If so, you've got your grouping there. If not, you can use ROW_NUMBER, such as:
SELECT
ROW_NUMBER() OVER(ORDER BY d.Value) AS [Identity] -- here, use identity instead of row_number
, d.Value
FROM [Data] d
If we then use this query as a subquery in our select, we get:
http://sqlfiddle.com/#!6/7194d/21
SELECT d.[Identity], split.value
FROM
(
SELECT
ROW_NUMBER() OVER(ORDER BY d.Value) AS [Identity] -- here, use identity instead of row_number
, d.Value
FROM [Data] d
) d
CROSS APPLY dbo.fnSplitString(IsNull(d.Value,''), ' ') AS split
WHERE RTRIM(split.value) <> ''
ORDER BY d.[Identity], value;
This query now sorts all rows within each identity. But now you need to reconstruct those individual words back into a single string, right? For that, you can use STUFF. In my example I use a CTE because of SQL Fiddle limitations but you could use a temp table, too.
WITH tempData AS (
SELECT d.[Identity], split.value
FROM
(
SELECT
ROW_NUMBER() OVER(ORDER BY d.Value) AS [Identity] -- here, use identity instead of row_number
, d.Value
FROM [Data] d
) d
CROSS APPLY dbo.fnSplitString(IsNull(d.Value,''), ' ') AS split
WHERE RTRIM(split.value) <> ''
)
SELECT grp.[Identity]
, STUFF((SELECT N' ' + [Value] FROM tempData WHERE [Identity] = grp.[Identity] ORDER BY Value FOR XML PATH(N''))
, 1, 1, N'')
FROM (SELECT DISTINCT [Identity] FROM tempData) AS grp
Here's the end result fiddle: http://sqlfiddle.com/#!6/7194d/27
As expressed in comments already, this is not a common case for SQL. It's an unnecessary burden on the server. I would recommend pulling data out of SQL and sorting it through your programming language of choice; or making sure it's sorted as you insert it into the DB. I went through the exercise because I had a few minutes to kill :)
Already +1 on Shnugo's solution. I actually watch for his posts.
Just another option use a parse UDF in concert with a Cross Apply.
Example
Select B.*
From YourTable A
Cross Apply ( Select Sorted=Stuff((Select ' ' +RetVal From [dbo].[tvf-Str-Parse](A.SomeCol,' ') Order By RetVal For XML Path ('')),1,1,'') )B
Returns
Sorted
101 an apartment in Pillars
Durga Hotel International Zuzu
Fresh Garden Meadows Wyndham
The UDF if Interested
CREATE FUNCTION [dbo].[tvf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Thanks Shnugo for making this XML safe
--Select * from [dbo].[tvf-Str-Parse]('Dog,Cat,House,Car',',')