Get values from XML field with SQL Server - sql

I would like to get the value from a xml column with T-SQL.
My xml column named XML contains :
<SIMBATCHJOB ONFAIL="2" ID="4" ONPASS="0"/>
How can I get the second parameter? For example here ID.
I have tried a request like this:
DECLARE #xml XML
SELECT #xml = N'<SIMBATCHJOB ONFAIL="2" ID="4" ONPASS="0"/>'
SELECT
T.C.value('(SIMBATCHJOB)[1]', 'int') as test1
FROM
#xml.nodes('SIMBATCHJOB') T(C)
My table is structured like this:
ID | XML
If I try this query:
Select ID
,Value = cast([xml] as xml).value('SIMBATCHJOB[1]/#ID','int')
From SIM_JOBS
where id = 179
This query return me the ID and the Value is empty
Do you understand this ?
Thanks in advance

Jeroen is 100% correct, but one option is to use a CROSS APPLY and normalize your attributes.
The Sequence will hold true to the sequence in the actual XML. I suspect you could filter as needed.
Example
Declare #XML xml = '<SIMBATCHJOB ONFAIL="2" ID="4" ONPASS="0"/>'
Select Seq = row_number() over (Order By (Select NULL))
,Attribute = a.value('local-name(.)','varchar(100)')
,Value = a.value('.','varchar(max)')
From #XML.nodes('/SIMBATCHJOB ') as C1(n)
Cross Apply C1.n.nodes('./#*') as C2(a)
Returns
Seq Attribute Value
1 ONFAIL 2
2 ID 4
3 ONPASS 0
EDIT - Update for Comments ---
This will return the SECOND Attribute/Value regardles of what is is
Declare #YourTable table (ID int,[xml] xml)
Insert Into #YourTable values
(1,'<SIMBATCHJOB ONFAIL="2" ID="4" ONPASS="0"/>'),
(2,'<SIMBATCHJOB ONFAIL="2" ID="99" ONPASS="1"/>')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select *
From (
Select Seq = row_number() over (Order By (Select NULL))
,Attribute = a.value('local-name(.)','varchar(100)')
,Value = a.value('.','varchar(max)')
From A.xml.nodes('/SIMBATCHJOB ') as C1(n)
Cross Apply C1.n.nodes('./#*') as C2(a)
) B1
Where B1.Seq=2
) B
Returns
ID Seq Attribute Value
1 2 ID 4
2 2 ID 99
EDIT 2: - Get JUST ID regardless of Sequence
Declare #YourTable table (ID int,[xml] xml)
Insert Into #YourTable values
(1,'<SIMBATCHJOB ONFAIL="2" ID="4" ONPASS="0"/>'),
(2,'<SIMBATCHJOB ONFAIL="2" ID="99" ONPASS="1"/>')
Select A.ID
,Value = [xml].value('SIMBATCHJOB[1]/#ID','int')
From #YourTable A
Returns
ID Value
1 4
2 99

Related

Can I select all XML attributes in SQL without knowing the attributes name?

Say I have a column containing an XML string that looks something like:
<column type="someValue" action="anotherValue" />
Is there a way I can extract data from this column XML to where the results will look something like:
key
value
type
someValue
action
anothervalue
I've Googled around for the better part of an hour now with no luck. The other solutions I've come across seem to all assume the user know the attribute name beforehand. Is this possible?
Assuming your your XMLCol is a string and not XML.
Example
Declare #YourTable table (id int,XMLCol varchar(max))
Insert Into #YourTable values
(1,'<column type="someValue" action="anotherValue" />')
Select A.ID
,C.*
From #YourTable A
Cross Apply ( values ( try_convert(xml,XMLCol) ))B(XMLData)
Cross Apply (
Select [Key] = xAttr.value('local-name(.)', 'varchar(100)')
,Value = xAttr.value('.','varchar(max)')
From XMLData.nodes('//#*') xNode(xAttr)
) C
Returns
ID Key Value
1 type someValue
1 action anotherValue
IF your Column is XML
Declare #YourTable table (id int,XMLCol xml)
Insert Into #YourTable values
(1,'<column type="someValue" action="anotherValue" />')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select [Key] = xAttr.value('local-name(.)', 'varchar(100)')
,Value = xAttr.value('.','varchar(max)')
From XMLCol.nodes('//#*') xNode(xAttr)
) B
declare #x xml = N'
<column type="someValue" action="anotherValue" />
<column typeX="someValueX" actionY="anotherValueX" att3="X123"/>
<column typeY="someValueY" actionY="anotherValueY" att3="Y123"/>
';
select
n.a.value('local-name(.)', 'nvarchar(100)') as attributename,
n.a.value('data(.)', 'nvarchar(100)') as attributevalue
from #x.nodes('//#*') as n(a);

How to SORT in order as entered in SQL Server?

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.

Updating Node structure in SQL XML data

I have some Data stored as XML in SQL Server that looks as follows:
<FormSearchFilter>
.......
<IDs>
<int>1</int>
<int>2</int>
</IDs>
.......
</FormSearchFilter>
This XML is mapped to a DTO and the data type for IDs is changing from a List to a string. As a result I now need to updae all existing XML: data to look as follows:
<FormSearchFilter>
.......
<IDs>1,2</IDs>
.......
</FormSearchFilter>
Whats the best way to achieve this via an update query
Besides the hint, that this is a very bad idea! you might try something like this:
DECLARE #t TABLE(
Id INT NOT NULL IDENTITY(1,1),
xml XML)
INSERT INTO #t(xml)
VALUES
('<FormSearchFilter><IDs><int>1</int><int>2</int></IDs></FormSearchFilter>'),
('<FormSearchFilter><IDs><int>1</int><int>2</int><int>3</int></IDs></FormSearchFilter>'),
('<FormSearchFilter><IDs><int>1</int><int>2</int><int>3</int><int>4</int></IDs></FormSearchFilter>');
UPDATE #t
SET [xml]= (SELECT REPLACE([xml].query('data(/FormSearchFilter/IDs/int)').value('.','nvarchar(max)'),' ',',') AS IDs
FOR XML PATH('FormSearchFilter'));
SELECT * FROM #t
Explanation:
XQuery function data() will return alle text() nodes (in your case the int values) separated by a blank. This can be replaced with a comma to get the list needed.
UPDATE: Preserve other elements (be aware, that the order changes)
INSERT INTO #t(xml)
VALUES
('<FormSearchFilter><test>x</test><IDs><int>1</int><int>2</int></IDs></FormSearchFilter>'),
('<FormSearchFilter><IDs><int>1</int><int>2</int><int>3</int></IDs><test>x</test></FormSearchFilter>'),
('<FormSearchFilter><IDs><int>1</int><int>2</int><int>3</int><int>4</int></IDs></FormSearchFilter>');
UPDATE #t
SET [xml]= (SELECT [xml].query('/FormSearchFilter/*[local-name()!="IDs"]') AS [*]
,REPLACE([xml].query('data(/FormSearchFilter/IDs/int)').value('.','nvarchar(max)'),' ',',') AS IDs
FOR XML PATH('FormSearchFilter'));
SELECT * FROM #t
A bit of a hack, and if you're open to a helper Table-Valued Function.
Example
Declare #XML xml = '
<FormSearchFilter>
<OtherContent>Some Content</OtherContent>
<IDs>
<int>1</int>
<int>2</int>
</IDs>
<IDs>
<int>11</int>
<int>12</int>
<int>13</int>
</IDs>
<IDs>
<int>99</int>
</IDs>
<MoreContent>Some MORE Content</MoreContent>
</FormSearchFilter>
'
Select #XML = replace(cast(#XML as varchar(max)),RetVal,NewVal)
From (
Select *
,NewVal = stuff(replace(replace(RetVal,'<int>',','),'</int>',''),1,1,'')
From [dbo].[tvf-Str-Extract](cast(#XML as varchar(max)),'<IDs>','</IDs>')
) A
Select #XML
Returns
<FormSearchFilter>
<OtherContent>Some Content</OtherContent>
<IDs>1,2</IDs>
<IDs>11,12,13</IDs>
<IDs>99</IDs>
<MoreContent>Some MORE Content</MoreContent>
</FormSearchFilter>
The TVF was created because I tired of extracting content (left,right,charindex,patindex,reverse,...). It is a modifed parse/split function which accepts two non-like delimiters. Just to illustrate, if you were to run:
Select * From [dbo].[tvf-Str-Extract](cast(#XML as varchar(max)),'<IDs>','</IDs>')
The results would be
RetSeq RetPos RetVal
1 65 <int>1</int><int>2</int>
2 100 <int>11</int><int>12</int><int>13</int>
3 150 <int>99</int>
The TVF if Interested
CREATE FUNCTION [dbo].[tvf-Str-Extract] (#String varchar(max),#Delimiter1 varchar(100),#Delimiter2 varchar(100))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 N1,cte1 N2,cte1 N3,cte1 N4,cte1 N5,cte1 N6) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter1) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter1)) = #Delimiter1),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter1,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By N)
,RetPos = N
,RetVal = left(RetVal,charindex(#Delimiter2,RetVal)-1)
From (
Select *,RetVal = Substring(#String, N, L)
From cte4
) A
Where charindex(#Delimiter2,RetVal)>1
)
/*
Max Length of String 1MM characters
Declare #String varchar(max) = 'Dear [[FirstName]] [[LastName]], ...'
Select * From [dbo].[tvf-Str-Extract] (#String,'[[',']]')
*/
Not particularly elegant but does end up with the required output:
DECLARE #t TABLE(
Id INT NOT NULL IDENTITY(1,1),
xml XML)
INSERT INTO #t(xml)
VALUES
('<FormSearchFilter><IDs><int>1</int><int>2</int></IDs></FormSearchFilter>'),
('<FormSearchFilter><IDs><int>1</int><int>2</int><int>3</int></IDs></FormSearchFilter>'),
('<FormSearchFilter><IDs><int>1</int><int>2</int><int>3</int><int>4</int></IDs></FormSearchFilter>');
DECLARE #updates TABLE(
Id INT,
UpdatedValue XML
)
INSERT INTO #updates
SELECT
Id,
(SELECT STUFF((
SELECT
',' + c.value('.', 'varchar')
FROM #t t1
CROSS APPLY t1.xml.nodes('//IDs/int') x(c)
WHERE t1.Id = t.Id
FOR XML PATH('')
), 1, 1, '') IDs
FOR XML PATH(''))
FROM #t t
-- remove existing IDs node
UPDATE #t
SET xml.modify('delete //IDs')
-- insert updated IDs node back in
UPDATE t
SET xml.modify('insert sql:column("u.UpdatedValue") into (/FormSearchFilter)[1]')
FROM #t t
JOIN #updates u ON t.Id = u.Id

SELECT only numeric without function in sql

I need sql query WITHOUT FUNCTION with SELECT only numeric characters.
For example, I have in sql table 0f-gh 14-2t-4 /// and I want get this -> 01424. How I can do it with sql query SELECT, without anything, only with SELECT
This is the logic from digitsonlyEE which is the fastest T-SQL based "digits only" function available today.
declare #table table (somestring varchar(50));
insert #table VALUES('abc123xxx555!!!999'),('##123ttt999'),('555222!');
SELECT *
FROM #table t
CROSS APPLY
(
SELECT DigitsOnly =
(
SELECT SUBSTRING(t.somestring,n,1)
FROM
(
SELECT TOP (LEN(ISNULL(t.somestring,CHAR(32))))
(CHECKSUM(ROW_NUMBER() OVER (ORDER BY (SELECT NULL))))
FROM
(VALUES ($),($),($),($),($),($),($),($),($),($)) a(x),
(VALUES ($),($),($),($),($),($),($),($),($),($)) b(x),
(VALUES ($),($),($),($),($),($),($),($),($),($)) c(x),
(VALUES ($),($),($),($),($),($),($),($),($),($)) d(x)
) iTally(n)
WHERE ((ASCII(SUBSTRING(t.somestring,N,1)) - 48) & 0x7FFF) < 10
FOR XML PATH('')
)
) digitsOnlyEE(digitsOnly);
Results:
somestring digitsOnly
--------------------- ----------
abc123xxx555!!!999 123555999
##123ttt999 123999
555222! 555222
Here is an inline approach
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'0f-gh 14-2t-4 ///')
Select A.ID
,B.*
From #YourTable A
Cross Apply (
Select NewValue = (Select substring(A.SomeCol,N,1)
From (Select Top (len(A.SomeCol)) N=Row_Number() Over (Order By (Select NULL)) From master..spt_values n1) S
Where substring(A.SomeCol,N,1) like '[0-9]%'
Order By N
For XML Path (''))
) B
Returns
ID NewValue
1 01424
Note: Use Outer Apply if you want to see null values in the event where the string has NO numerics.

SQL: How to create columns dynamically

I have a table which is created dynamically. So the number of columns is unknown at the time of creation. I want to create copies of each column in the same table with first column holding the first part of comma separated value, second column the second part and so on
For example,
ID Value1 Value2 .... Valuen
1 1;2;3 4;5;6
2 A;B;C D;E;F
I want to get the output like
ID Value1Copy1 Value1Copy2 Value1Copy3 Value2Copy1 Value2Copy2 Value2Copy3 .... ValuenCopy1
1 1 2 3 4 5 6
2 A B C D E F
I am unable to achieve this for variable number of columns
The following will dynamically unpivot your data. You may notice that the only field specified is ID.
The results are dropped into a #Temp table. From there we perform a dynamic pivot
Example
Declare #YourTable table (ID int,Value1 varchar(50),Value2 varchar(50))
Insert Into #YourTable values
( 1, '1;2;3','4;5;6'),
( 2, 'A;B;C','D;E;F')
Select A.ID
,Col = concat(C.Item,'Copy',D.RetSeq)
,Value = D.RetVal
Into #Temp
From #YourTable A --<< Replace with Your actual table
Cross Apply (Select XMLData = cast((Select A.* For XML Raw) as xml ) ) B
Cross Apply (
Select Item = attr.value('local-name(.)','varchar(100)')
,Value = attr.value('.','varchar(max)')
From B.XMLData.nodes('/row') as A(r)
Cross Apply A.r.nodes('./#*') AS B(attr)
Where attr.value('local-name(.)','varchar(100)') not in ('ID','Other2Exclude')
) C
Cross Apply (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(C.Value,';','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) D
Where A.ID is not null -- or any other WHERE statement
Declare #SQL varchar(max) = Stuff((Select Distinct ',' + QuoteName(Col) From #Temp Order by 1 For XML Path('')),1,1,'')
Select #SQL = '
Select *
From #Temp
Pivot (max(Value) For [Col] in (' + #SQL + ') ) p'
Exec(#SQL);
Returns