Why is CROSS APPLY needed when using XPath queries?

The answer to your question is in your question.

The result of the nodes() method is a rowset

You can't do this either

WITH T(X) AS
(
SELECT 1
)
SELECT X, (SELECT 'A' AS Y UNION ALL SELECT 'B' AS Y)
FROM T

But you can do

WITH T(X) AS
(
SELECT 1
)
SELECT X, Y
FROM T
CROSS APPLY (SELECT 'A' AS Y UNION ALL SELECT 'B' AS Y) C

A straight SELECT ... FROM T can't add or subtract rows to the resultset no matter what functions you call in the SELECT list. That just isn't how SQL works.


Query:

SELECT x.i.value('(./text())[1]', 'VARCHAR(10)')
FROM MyTable.SomeXmlColumn.nodes('./people/person/firstName') AS x(i);

doesn't work, for the same reason why this query doesn't work:

SELECT *
FROM Person.Person.FirstName;

but this does:

SELECT FirstName
FROM Person.Person;

-

FROM clause expects rowset, so this is valid, since nodes() returns rowset:

DECLARE @xml AS XML = 
'<people>
   <person><firstName>Jon</firstName><lastName>Johnson</lastName></person>
   <person><firstName>Kathy</firstName><lastName>Carter</lastName></person>
   <person><firstName>Bob</firstName><lastName>Burns</lastName></person>
</people>';

SELECT x.i.value('(./text())[1]', 'VARCHAR(10)')
FROM @xml.nodes('./people/person/firstName') AS x(i);

If xml is not a variable but value in table, we first need to extract rows from this value, and this is when CROSS APPLY comes in handy:

SELECT x.i.value('(./text())[1]', 'VARCHAR(10)')
FROM MyTable as t
CROSS APPLY 
   t.SomeXmlColumn.nodes('./people/person/firstName') AS x(i);

CROSS APPLY operator applies the right expression to each record from the left table (MyTable).

  • In MyTable table there is one record containing xml.
  • CROSS APPLY fetches this record and exposes it to expression in the right.
  • Right expression extracts records using nodes() function.
  • As a result there are 1 x 3 = 3 records (xml nodes) which are then processed by SELECT clause.

Compare to 'normal' CROSS APPLY query:

SELECT c.CustomerID, soh.TotalDue, soh.OrderDate
FROM Sales.Customer AS c
CROSS APPLY
    (SELECT TOP(2) TotalDue, OrderDate
    FROM Sales.SalesOrderHeader
    WHERE CustomerID = c.CustomerID
ORDER BY TotalDue DESC) AS soh;

c.CustomerID is our t.SomeXmlColumn