Reputation: 19953
In SQL Server, how can I guarantee that returned data is ordered by the sequence of nodes in an XML column, when using CROSS APPLY
in the following?
SELECT MC.Id, MC.Name
FROM MyParent MP
CROSS APPLY MP.ChildrenXml.nodes('/d/i') AS CX(CX)
INNER JOIN MyChildren MC
ON MC.Id = CX.value('text()[1]','int')
-- MyParent table...
ChildrenXML
<d><i>1</i><i>3></i><i>2</i></d>
-- MyChildren table...
Id Name
1 Tom
2 Richard
3 Harry
-- Expected output...
Id Name
1 Tom
3 Harry
2 Richard
The best I can come up with (but doesn't feel like a great solution) is...
; WITH ChildIds AS (
SELECT CX.value('text()[1]','int') AS Id,
ROW_NUMBER() OVER (ORDER BY CX.CX) AS RowNum
FROM MyParent MP
CROSS APPLY MP.ChildrenXml.nodes('/d/i') AS CX(CX)
)
SELECT MC.Id, MC.Name
FROM ChildIds CI
INNER JOIN MyChildren MC
ON MC.Id = CI.Id
ORDER BY CI.RowNum
Upvotes: 0
Views: 671
Reputation: 67291
One more approach: Pre-Computing the element's position
DECLARE @tbl TABLE(theXml XML);
INSERT INTO @tbl(TheXml) VALUES(N'<d><i>1</i><i>2</i><i>3</i></d>')
,(N'<d><i>10</i><i>20</i><i>30</i><i>40</i></d>')
,(N'<d><i>5</i><i>3</i><i>3</i><i>-1</i></d>')
,(N'<d><i>0</i><i>1</i></d>')
;
SELECT A.theIndex
,t.theXml.value('(/d/i[sql:column("A.theIndex")]/text())[1]','int')
FROM @tbl t
CROSS APPLY(SELECT TOP(t.theXml.value(N'count(/d/i)','int')) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(theIndex);
The idea in short:
CROSS APPLY
to create a tally for each row separatelyTOP()
-clause using the row's count of <i>
-elementstheIndex
to fetch the element's value by its position using sql:column()
Upvotes: 1
Reputation: 19953
@Charlieface pointed me to the answer I really wanted... although I really appreciate their time/effort with their answer.
I never considered using ROW_NUMBER
as the ORDER BY
itself...
SELECT MC.Id, MC.Name
FROM MyParent MP
CROSS APPLY MP.ChildrenXml.nodes('/d/i') AS CX(CX)
INNER JOIN MyChildren MC
ON MC.Id = CX.value('text()[1]','int')
ORDER BY ROW_NUMBER() OVER (ORDER BY CX.CX)
Upvotes: 0
Reputation: 71253
Unfortunately, SQL Server does not allow you to use position()
in a .value
call.
Instead you can hack it by counting nodes.
You can do it without a CTE, by putting .value
into the ORDER BY
SELECT
CI.Id,
CI.Name
FROM MyParent MP
CROSS APPLY MP.ChildrenXml.nodes('/d/i') AS CX(CX)
INNER JOIN MyChildren CI
ON CI.Id = CX.value('text()[1]','int')
ORDER BY CX.value('let $i := . return count(../i[. << $i]) + 1','int')
Alternatively leave it in the SELECT
and you can still ORDER BY
that value
SELECT
CI.Id,
CI.Name,
CX.value('let $i := . return count(../i[. << $i]) + 1','int') RowNumber
FROM MyParent MP
CROSS APPLY MP.ChildrenXml.nodes('/d/i') AS CX(CX)
INNER JOIN MyChildren CI
ON CI.Id = CX.value('text()[1]','int')
ORDER BY RowNumber
The way this works is as follows:
.
to $i
..
and count all its i
child nodes which...<<
before the $i
nodeUpvotes: 0