Retrieving multiple xml child node values - sql

I have a column of type varchar(max) populated with xml nodes and values; as an example, the column data starts with <tag1> <tag2>value1</tag2><tag3>value2</tag3>... </tag1>. What I need to get out of this string is "value1 value2 value3... valueN" within one cell for every row in the table using static SQL or a stored procedure. The node tree isn't always the same, sometimes the path is <tagX><tagY>valueY</tagY>...</tagX>.
All of my experience with shredding xml is only used to get one specific value, property, or tag, not all values while retaining the column and row count. Currently I query then loop through the result set on my product's end and shred everything, but that's no longer an option due to recent changes.
It's possible to change the column to be of type xml, but if possible I'd like to avoid having to do so.

Cast the column to XML (or change it in the table to XML) and shred the xml on //* to get all nodes in a table. Then you can use for xml path to concat the values back together.
select (
select ' '+X.N.value('text()[1]', 'varchar(max)')
from (select cast(T.XMLCol as xml)) as T1(XMLCol)
cross apply T1.XMLCol.nodes('//*') as X(N)
for xml path(''), type
).value('substring(text()[1], 2)', 'varchar(max)')
from T
SQL Fiddle

Related

SQL XML .node .query() returns empty string instead of NULL if table node exist

Why does .query() return empty string instead of NULL if the table doesn't exist? I am new to querying xml in SQL so I am just playing around and this was unexpected.
Here is an example of the problem.
declare #xml xml
set #xml = N'<xdoc><Header><OrderID>1234</OrderID><Detail><ProductID>12345</ProductID><Amount>12.50</Amount></Detail></Header></xdoc>'
SELECT
Tbl.Col.value('OrderID[1]', 'varchar(10)') as OrderID,
Tbl.Col.query('Detail') as Detail
FROM #xml.nodes('//Header') Tbl(Col)
set #xml = N'<xdoc><Header><OrderID>1234</OrderID></Header></xdoc>'
SELECT
Tbl.Col.value('OrderID[1]', 'varchar(10)') as OrderID,
Tbl.Col.query('Detail') as Detail
FROM #xml.nodes('//Header') Tbl(Col)
The first select statement returns as expected. The Detail field is the xml of the table detail within the header.
However, on the second select statement this header record did not have a detail table so I expected the field to be null instead of empty string.
Output
What you want is possible, but you must think a little bit differently. In XML the value NULL is indicated by a missing node.
Furthermore, the word "value" is very important in this sentence. The .value() method will return NULL if the element you are looking for is not present. The .query()-method will return a XML-typed fragment in any case.
XML consits of nodes. Elements (any <SomeElement> together with all nested nodes) are a special kind of node. The floating text within an element is a node in itself. Look at these examples:
Your XML
declare #xml xml
set #xml =
N'<xdoc>
<Header>
<OrderID>1234</OrderID>
<Detail>
<ProductID>12345</ProductID>
<Amount>12.50</Amount>
</Detail>
</Header>
</xdoc>'
--.query() can return the <Detail>-fragment
SELECT #xml.query('/xdoc/Header/Detail');
<Detail>
<ProductID>12345</ProductID>
<Amount>12.50</Amount>
</Detail>
--This node does not exist. What you get back is not a NULL-value but an empty XML node
SELECT #xml.query('/xdoc/Header/Dummy'); --<-- empty XML
--Now look at .value()
SELECT #xml.value('(/xdoc/Header/Dummy)[1]','nvarchar(100)'); --<-- NULL
SELECT #xml.value('(/xdoc/Header/Detail)[1]','nvarchar(100)'); --<-- 1234512.50
SELECT #xml.value('(/xdoc/Header/Detail/text())[1]','nvarchar(100)'); --<-- NULL
SELECT #xml.value('(/xdoc/Header/Detail/ProductID)[1]','nvarchar(100)'); --<-- 12345
SELECT #xml.value('(/xdoc/Header/Detail/ProductID/text())[1]','nvarchar(100)');--<-- 12345
The first call is clear, there is no <Dummy>, therefore, we get a NULL back.
The second call might surprise you. The value is everything within the element. In this case we get a combination of all the values. You might have seen this as a trick to simulate group_concat().
The third call returns NULL as there is no text() within <Detail>
... and the fourth and the fifth call return the same, but the last one with text() is most specific and therefore the best choice.
It is depending on your needs, what you want to query and what you want to get back. Furthermore, you can use predicates like empty(SomeNode) or not(empty(SomeNode)) to use XQuery-filters in your XPath.

SQL query update nvarchar with XML contents add missing attribute to node

I have an nvarchar column with XML contents. Not sure if it needs to be cast to XML or not. All the XML content is formatted the same except a number of records need to be modified where the first node needs an attribute added to it:
<MyFirstNode SomeAttribute="value" SomeOtherAttribute="anothervalue">
update to:
<MyFirstNode SomeAttribute="value" AddThisAttribute="nicevalue" SomeOtherAttribute="anothervalue">
How can i update all the required nodes with the AddThisAttribute="nicevalue" ? All need the same attribute and value.
If this is actually going to be interpreted as XML, you don't neccessarily have to insert AddThisAttribute="nicevalue" in the middle of the list of attributes; one option is to do a simple
UPDATE myTable
SET XMLColumn = REPLACE(XMLColumn, '<MyFirstNode ', '<MyFirstNode AddThisAttribute="nicevalue" ')
This might be easier than trying to insert the value, especially if SomeAttribute and SomeOtherAttribute change in each row.
Alternately,
UPDATE myTable
SET XMLColumn = REPLACE(XMLColumn, ' SomeOtherAttribute=', ' AddThisAttribute="nicevalue" SomeOtherAttribute=')
could work, though if "SomeOtherAttribute" appears in other node types other than MyFirstNode, this might make undesired changes.

SQL query to check for inclusion of any element from an array

I have a database column containing a string that might look something like this u/1u/3u/19/g1/g4 for a particular row.
Is there a performant way to get all rows that have at least one of the following elements ['u/3', 'g4'] in that column?
I know I can use AND clauses, but the number of elements to verify against varies and could become large..
I am using RoR/ActiveRecord in my project.
in sql server, you can use XML to convert your list of search params into a record set, then cross join that with the base table, and do charIndex() to see if the column contains the substring.
Since i don't know your table or column names, i used a table (persons) that i already had data in, which has a column 'phone_home'. To search for any phone number that contains '202' or '785' i would use this query:
select person_id,phone_home,Split.data.value('.', 'VARCHAR(10)')
from (select *, cast('<n>202</n><n>785</n>' as XML) as myXML
from persons) as data cross apply myXML.nodes('/n') as Split(data)
where charindex(Split.data.value('.', 'VARCHAR(10)'),data.phone_Home) > 0
you will get duplicate records if it matches more than one value, so throw a distinct in there and remove the Split from the select statement if that is not desired.
Using xml in sql is voodoo magic to me...i got the idea from this post http://www.sqljason.com/2010/05/converting-single-comma-separated-row.html
no idea what performance is like...but at least there aren't any cursors or dynamic sql.
EDIT: Casting the XML is pretty slow, so i made it a variable so it only gets cast once.
declare #xml XML
set #xml = cast('<n>202</n><n>785</n>' as XML)
select person_id,phone_home,Split.persons.value('.', 'VARCHAR(10)')
from persons cross apply #xml.nodes('/n') as Split(persons)
where charindex(Split.persons.value('.', 'VARCHAR(10)'),phone_Home) > 0

How to store and extract XML information from an nvarchar(max) type column, and use it in joins?

I have a column of type 'nvarchar(max)' that should now hold XML information instead of just a string.
Say: col1 has value 'abc'
Now it has values, with additional info:
<el1>abc</el2>
<el2>someotherinfo</el2>
Storing the information to the column is fine, since it can still be pushed in as a string.
However, extracting the same information and also using/replacing the same information 'abc' from this column that is being used in various other joins from other tables, is something I'm not able to figure out.
how can I also push in this information into abcd when it comes from another table's value 'abcd' without losing other information?
I am building an XML from the application side and updating it in a column of type nvarchar(). All the columns have been replaced to hold the XML, so the safe assumption is that the col1 only holds XML similar to that mentioned above. Just push the XML as is and it works fine. However, how should I extract the information to use it in joins?
How do I extract a particular element from this nvarchar() string to use it in a join??
Previously, this column 'Col1' was just used as a string, and a check was done like this:
where tablex.colx = table1.col1
or
Update Table2 where
Once you cast the NVARCHAR data to the XML data type, you can use XML functions to get element/attribute values for joining to:
WITH xoutput AS (
SELECT CONVERT(xml, t.nvarchar_column) AS col
FROM YOUR_TABLE t)
SELECT x.*
FROM TABLE x
JOIN xoutput y ON y.col.value('(/path/to/your/element)[1]', 'int') = x.id
It won't be able to use indexes, because of the data type conversion...
Alternate version, using IN:
WITH xoutput AS (
SELECT CONVERT(xml, t.nvarchar_column) AS col
FROM YOUR_TABLE t)
SELECT x.*
FROM TABLE x
WHERE x.id IN (SELECT y.col.value('(/path/to/your/element)[1]', 'int')
FROM xoutput)

XML XQUERY Problem with NTEXT data type

I want to use XQuery on a column of data type NTEXT (I have no choice!). I have tried converting the column to XML using CONVERT but it gives the error:
Incorrect syntax near the keyword 'CONVERT'.
Here's the query
SELECT
y.item.value('#UserID', 'varchar(50)') AS UnitID,
y.item.value('#ListingID', 'varchar(100)') AS #ListingID
FROM
dbo.KB_XMod_Modules
CROSS APPLY
CONVERT(xml, instancedata).nodes('//instance') AS y(item)
(instancedata is my column)
Can anyone think of a work around for this ?
Thanks
It seems that neither CONVERT nor CAST(... AS XML) work.
The XQuery stuff really only works on XML columns - sorry.
However: maybe there's an escape route :-)
You could do one of two things:
add a persisted computed column of type XML to your table and party on that new column
or - if you can't change the table structure -
create a view over that table and include a XML-casted column
So for solution #1 you'd do:
ALTER TABLE KB_XMod_Modules
ADD XmlCol AS CAST(instancedata AS XML) PERSISTED
and for solution #2, you'd use:
CREATE VIEW KBwithXML AS
SELECT instancedata, CAST(instancedata AS XML) 'XmlCol'
FROM KB_XMod_Modules
In both cases, you now have a XmlCol of type XML available, which you can query with XQuery to your heart's delight!