131

Just looking at my XML field, my rows look like this:

<person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
<person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
<person><firstName>Bob</firstName><lastName>Burns</lastName></person>

Note that these are three rows in my table.

I'd like to return a SQL result as a table as in

Jon  | Johnson
Kathy| Carter
Bob  | Burns

What query will accomplish this?

Larsenal
  • 49,878
  • 43
  • 152
  • 220
  • Is there no way to just get ALL the elements in the xml? You have to specify one by one? That gets really tedious fast. You can do "select * from table", seems like you should be able to do "select xml.* from xml" without having to specify every single element you want. – Keith Tyler Jul 31 '20 at 22:23

12 Answers12

174

Given that the XML field is named 'xmlField'...

SELECT 
[xmlField].value('(/person//firstName/node())[1]', 'nvarchar(max)') as FirstName,
[xmlField].value('(/person//lastName/node())[1]', 'nvarchar(max)') as LastName
FROM [myTable]
Larsenal
  • 49,878
  • 43
  • 152
  • 220
  • 21
    You must use .nodes() and cross apply if xmlField contains more than one elements. – Remus Rusanu May 22 '09 at 18:45
  • SQL Server 2008 R2 Express, returned me this error with your solution: `The XQuery syntax '/function()' is not supported.`; On the other hand @Remus Rusanu seems to do it :) – RASMiranda Apr 19 '13 at 20:25
  • 2
    Bizarre. This has been voted up 102 times, but this answer only returns data from the *first* XML record. And it refers to some [myTable] table... where did that come from ?! – Mike Gledhill Sep 08 '16 at 09:05
  • 1
    I have tried this so many times and never had it working. My XML is `Electrical7330`, my select is `select e.MessageData.value('(/BAM/Type)[1]', 'varchar(100)')`. I have also tried select `e.MessageData.value('(/BAM/Type/node())[1]', 'varchar(100)')`, and `'(//Type/node())[1]'`, `'(./Type)[1]'`, and every other combination I can think of. All I ever get is `NULL`. – JonathanPeel Nov 15 '16 at 14:13
  • If the xml has given a namespace `xmlns="http://bayerbbs.com/AMUNI"` you have to map it to deafutl namespace `WITH XMLNAMESPACES(DEFAULT 'http://bayerbbs.com/AMUNI') SELECT ...` see https://stackoverflow.com/questions/5467387/ignore-xml-namespace-in-t-sql ` – fbehrens Dec 15 '16 at 15:41
  • 1
    @MikeGledhill it returns values from multiple XML records fine for me. Also the only name to the table the OP gives is "my table" :) – Paul Feb 28 '17 at 08:55
  • @MikeGledhill The original question says "these are three rows in my table" so [myTable] refers to whatever that table is called. If you are querying some single XML variable,then [1] will only give you one row. The syntax is not straight forward, but for a table [myTable] that has a column [xmlField] of type xml, this is one of the two correct answers, the other being Remus Rusanu's. – Davos Jun 22 '17 at 06:55
134

Considering that XML data comes from a table 'table' and is stored in a column 'field': use the XML methods, extract values with xml.value(), project nodes with xml.nodes(), use CROSS APPLY to join:

SELECT 
    p.value('(./firstName)[1]', 'VARCHAR(8000)') AS firstName,
    p.value('(./lastName)[1]', 'VARCHAR(8000)') AS lastName
FROM table 
    CROSS APPLY field.nodes('/person') t(p)

You can ditch the nodes() and cross apply if each field contains exactly one element 'person'. If the XML is a variable you select FROM @variable.nodes(...) and you don't need the cross apply.

shA.t
  • 16,580
  • 5
  • 54
  • 111
Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • 1
    I wonder how efficient this method is and whether there's a better way. The CROSS APPLY combiend with XPath results seems like it might result in quite a resource hungry query. – redcalx Jun 21 '10 at 10:38
  • 1
    @thelocster: this is no different from ordinary data access. Techniques for improving XML performance are well documented. http://msdn.microsoft.com/en-us/library/ms345118%28SQL.90%29.aspx – Remus Rusanu Jun 21 '10 at 20:46
  • 2
    keep in mind that if your XML has xmlns namespaces defined, you'll need to define those in the XQuery (XPath) expression above. See http://stackoverflow.com/a/1302150/656010 for an example. – Tom Wayson Mar 27 '12 at 07:34
  • Slightly different to what I was needing, but this was a perfect solution to a problem I was having which was multiple rows with an XML column - I wanted to loop through rows and pull out the data fields from within the XML column and put them in an insert statement. So 5 rows, each for 3 columns of data in the XML field = 15 inserts, perfect. – dan richardson Apr 25 '12 at 15:44
  • As @Tom said the namespace is very important. Even if you do `SELECT TOP 1` without the correct namespace it'll take FOREVER! – Simon_Weaver Jan 13 '21 at 00:31
19

This post was helpful to solve my problem which has a little different XML format... my XML contains a list of keys like the following example and I store the XML in the SourceKeys column in a table named DeleteBatch:

<k>1</k>
<k>2</k>
<k>3</k>

Create the table and populate it with some data:

CREATE TABLE dbo.DeleteBatch (
    ExecutionKey INT PRIMARY KEY,
    SourceKeys XML)

INSERT INTO dbo.DeleteBatch ( ExecutionKey, SourceKeys )
SELECT 1, 
    (CAST('<k>1</k><k>2</k><k>3</k>' AS XML))

INSERT INTO dbo.DeleteBatch ( ExecutionKey, SourceKeys )
SELECT 2, 
    (CAST('<k>100</k><k>101</k>' AS XML))

Here's my SQL to select the keys from the XML:

SELECT ExecutionKey, p.value('.', 'int') AS [Key]
FROM dbo.DeleteBatch
    CROSS APPLY SourceKeys.nodes('/k') t(p)

Here's the query results...

ExecutionKey    Key
1   1
1   2
1   3
2   100
2   101
Monte
  • 191
  • 1
  • 3
10

This may answer your question:

select cast(xmlField as xml) xmlField into tmp from (
select '<person><firstName>Jon</firstName><lastName>Johnson</lastName></person>' xmlField
union select '<person><firstName>Kathy</firstName><lastName>Carter</lastName></person>'
union select '<person><firstName>Bob</firstName><lastName>Burns</lastName></person>'
) tb

SELECT
    xmlField.value('(person/firstName)[1]', 'nvarchar(max)') as FirstName
    ,xmlField.value('(person/lastName)[1]', 'nvarchar(max)') as LastName
FROM tmp

drop table tmp
Marquinho Peli
  • 4,795
  • 4
  • 24
  • 22
5

Blimey. This was a really useful thread to discover.

I still found some of these suggestions confusing. Whenever I used value with [1] in the string, it would only retrieved the first value. And some suggestions recommended using cross apply which (in my tests) just brought back far too much data.

So, here's my simple example of how you'd create an xml object, then read out its values into a table.

DECLARE @str nvarchar(2000)

SET @str = ''
SET @str = @str + '<users>'
SET @str = @str + '  <user>'
SET @str = @str + '     <firstName>Mike</firstName>'
SET @str = @str + '     <lastName>Gledhill</lastName>'
SET @str = @str + '     <age>31</age>'
SET @str = @str + '  </user>'
SET @str = @str + '  <user>'
SET @str = @str + '     <firstName>Mark</firstName>'
SET @str = @str + '     <lastName>Stevens</lastName>'
SET @str = @str + '     <age>42</age>'
SET @str = @str + '  </user>'
SET @str = @str + '  <user>'
SET @str = @str + '     <firstName>Sarah</firstName>'
SET @str = @str + '     <lastName>Brown</lastName>'
SET @str = @str + '     <age>23</age>'
SET @str = @str + '  </user>'
SET @str = @str + '</users>'

DECLARE @xml xml
SELECT @xml = CAST(CAST(@str AS VARBINARY(MAX)) AS XML) 

--  Iterate through each of the "users\user" records in our XML
SELECT 
    x.Rec.query('./firstName').value('.', 'nvarchar(2000)') AS 'FirstName',
    x.Rec.query('./lastName').value('.', 'nvarchar(2000)') AS 'LastName',
    x.Rec.query('./age').value('.', 'int') AS 'Age'
FROM @xml.nodes('/users/user') as x(Rec)

And here's the output:

enter image description here

It's bizarre syntax, but with a decent example, it's easy enough to add to your own SQL Server functions.

Speaking of which, here's the correct answer to this question.

Assuming your have your xml data in an @xml variable of type xml (as demonstrated in my example above), here's how you would return the three rows of data from the xml quoted in the question:

SELECT 
    x.Rec.query('./firstName').value('.', 'nvarchar(2000)') AS 'FirstName',
    x.Rec.query('./lastName').value('.', 'nvarchar(2000)') AS 'LastName'
FROM @xml.nodes('/person') as x(Rec)

enter image description here

Mike Gledhill
  • 27,846
  • 7
  • 149
  • 159
  • 1
    I don't see how this is the correct answer. The OP is asking for querying a column from a table which is of type XML, and in that case you have to either use `[1]` , the index ordinal to force it to return 1 row, or you have to cross apply the column with `nodes()` to get a structure that can have xpath run against it. Your code doesn't translate to that scenario without a lot of modifications. You're using a variable, not a table column. You're also overusing `query()` function which returns xml. e.g. you could have just `x.Rec.value('(./firstName)[1]', 'nvarchar(2000)') AS FirstName` – Davos Jun 22 '17 at 06:27
4
SELECT 
cast(xmlField as xml).value('(/person//firstName/node())[1]', 'nvarchar(max)') as FirstName,
cast(xmlField as xml).value('(/person//lastName/node())[1]', 'nvarchar(max)') as LastName
FROM [myTable]
empiric
  • 7,825
  • 7
  • 37
  • 48
shaheer
  • 67
  • 7
4

If you are able to wrap your XML in a root element - say then the following is your solution:

DECLARE @PersonsXml XML = '<persons><person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
<person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
<person><firstName>Bob</firstName><lastName>Burns</lastName></person></persons>'

SELECT  b.value('(./firstName/text())[1]','nvarchar(max)') as FirstName, b.value('(./lastName/text())[1]','nvarchar(max)') as LastName
FROM @PersonsXml.nodes('/persons/person') AS a(b)

enter image description here

Moiz Tankiwala
  • 6,070
  • 7
  • 38
  • 51
3

MSSQL uses regular XPath rules as follows:

  • nodename Selects all nodes with the name "nodename"
  • / Selects from the root node
  • // Selects nodes in the document from the current node that match the selection no matter where they are
  • . Selects the current node
  • .. Selects the parent of the current node
  • @ Selects attributes

W3Schools

Arthur
  • 2,601
  • 1
  • 22
  • 19
0

/* This example uses an XML variable with a schema */

IF EXISTS (SELECT * FROM sys.xml_schema_collections 
           WHERE name = 'OrderingAfternoonTea')
BEGIN
    DROP XML SCHEMA COLLECTION dbo.OrderingAfternoonTea 
END
GO

CREATE XML SCHEMA COLLECTION dbo.OrderingAfternoonTea AS
N'<?xml version="1.0" encoding="UTF-16" ?>
  <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
     targetNamespace="http://Tfor2.com/schemas/actions/orderAfternoonTea"
     xmlns="http://Tfor2.com/schemas/actions/orderAfternoonTea"
     xmlns:TFor2="http://Tfor2.com/schemas/actions/orderAfternoonTea"
     elementFormDefault="qualified"
     version="0.10"
   > 
    <xsd:complexType name="AfternoonTeaOrderType">
       <xsd:sequence>
         <xsd:element name="potsOfTea" type="xsd:int"/>
         <xsd:element name="cakes" type="xsd:int"/>
         <xsd:element name="fruitedSconesWithCream" type="xsd:int"/>
         <xsd:element name="jams" type="xsd:string"/>
      </xsd:sequence>
      <xsd:attribute name="schemaVersion" type="xsd:long" use="required"/>
    </xsd:complexType>

    <xsd:element name="afternoonTeaOrder"
                 type="TFor2:AfternoonTeaOrderType"/>

  </xsd:schema>' ;
GO

DECLARE @potsOfTea int;
DECLARE @cakes int;
DECLARE @fruitedSconesWithCream int;
DECLARE @jams nvarchar(128);

DECLARE @RequestMsg NVARCHAR(2048);
DECLARE @RequestXml XML(dbo.OrderingAfternoonTea);

set @potsOfTea = 5;
set @cakes = 7;
set @fruitedSconesWithCream = 25;
set @jams = N'medlar jelly, quince and mulberry';

SELECT @RequestMsg = N'<?xml version="1.0" encoding="utf-16" ?>
<TFor2:afternoonTeaOrder schemaVersion="10"
    xmlns:TFor2="http://Tfor2.com/schemas/actions/orderAfternoonTea">
    <TFor2:potsOfTea>' + CAST(@potsOfTea as NVARCHAR(20)) 
        + '</TFor2:potsOfTea>
    <TFor2:cakes>' + CAST(@cakes as NVARCHAR(20)) + '</TFor2:cakes>
    <TFor2:fruitedSconesWithCream>' 
        + CAST(@fruitedSconesWithCream as NVARCHAR(20))
        + '</TFor2:fruitedSconesWithCream>
    <TFor2:jams>' + @jams + '</TFor2:jams>
</TFor2:afternoonTeaOrder>';

SELECT @RequestXml  = CAST(CAST(@RequestMsg AS VARBINARY(MAX)) AS XML) ;

with xmlnamespaces('http://Tfor2.com/schemas/actions/orderAfternoonTea'
                    as tea)
select
    cast( x.Rec.value('.[1]/@schemaVersion','nvarchar(20)') as bigint )
        as schemaVersion,
    cast( x.Rec.query('./tea:potsOfTea')
               .value('.','nvarchar(20)') as bigint ) as potsOfTea,
    cast( x.Rec.query('./tea:cakes')
               .value('.','nvarchar(20)') as bigint )  as cakes,
    cast( x.Rec.query('./tea:fruitedSconesWithCream')
               .value('.','nvarchar(20)') as bigint ) 
      as fruitedSconesWithCream,
    x.Rec.query('./tea:jams').value('.','nvarchar(50)')  as jams
from @RequestXml.nodes('/tea:afternoonTeaOrder')  as x(Rec);

select @RequestXml.query('/*')
0

you can use this sample from here:

DECLARE @myDoc XML  
DECLARE @ProdID INT  
SET @myDoc = '<Root>  
<ProductDescription ProductID="1" ProductName="Road Bike">  
<Features>  
  <Warranty>1 year parts and labor</Warranty>  
  <Maintenance>3 year parts and labor extended maintenance is available</Maintenance>  
</Features>  
</ProductDescription>  
</Root>'  
  
SET @ProdID =  @myDoc.value('(/Root/ProductDescription/@ProductID)[1]', 'int' )  
SELECT @ProdID 
mjyazdani
  • 2,110
  • 6
  • 33
  • 64
0

In my case, xml shredding works good: https://dzone.com/articles/dude-lets-xml-shred

XML:

DECLARE @x XML
SET @x = '
<Library>
  <Books>
    <Book Type="Paperback">
      <Author>Robert Jordan</Author>
      <Id>26</Id>
      <PublicationDate>01/15/1990</PublicationDate>
      <Series>
        <Name>The Wheel of Time</Name>
        <Number>1</Number>
      </Series>
      <Title Chapters="53">The Eye of the World</Title>
    </Book>
    <Book Type="Hardback">
      <Author>Robert Jordan</Author>
      <Id>87</Id>
      <PublicationDate>09/15/1992</PublicationDate>
      <Series>
        <Name>The Wheel of Time</Name>
        <Number>4</Number>
      </Series>
      <Title Chapters="58">The Shadow Rising</Title>
    </Book>
    <Book Type="eBook">
      <Author>Robert Jordan</Author>
      <Id>43</Id>
      <PublicationDate>05/15/1996</PublicationDate>
      <Series>
        <Name>The Wheel of Time</Name>
        <Number>7</Number>
      </Series>
      <Title Chapters="41">A Crown of Swords</Title>
    </Book>
  </Books>
  <Id>51</Id>
  <Name>We Have Books... Read Them or Else!</Name>
</Library>'

Query:

SELECT Books.Book.value('(./Id)[1]', 'INT') AS Id,
       Books.Book.value('(./Title)[1]', 'VARCHAR(MAX)') AS Title,
       Books.Book.value('(./Author)[1]', 'VARCHAR(MAX)') AS Author,
       Books.Book.value('(./PublicationDate)[1]', 'DATE') AS PublicationDate,
       Books.Book.value('(./Series/Name)[1]', 'VARCHAR(MAX)') AS SeriesName,
       Books.Book.value('(./Series/Number)[1]', 'VARCHAR(MAX)') AS SeriesNumber,
       Books.Book.value('@Type', 'VARCHAR(MAX)') AS BookType,
       Books.Book.value('(./Title/@Chapters)[1]', 'INT') AS Chapters
FROM @x.nodes('(/Library/Books/Book)') AS Books(Book)

Output

0

you can write very easily, Please find the following details

DECLARE @xmlData XML='
<person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
<person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
<person><firstName>Bob</firstName><lastName>Burns</lastName></person>'

SELECT 
Col.value('(firstName)[1]','NVARCHAR(50)') AS [FirstName], 
Col.value('(lastName)[1]','NVARCHAR(50)') AS [LastName]
FROM @xmlData.nodes('person') AS tbl(Col) 

Click to see the result

Govind
  • 186
  • 5