views:

73

answers:

3

Given the following sample of XML and the select statement that shreds the xml into a relation, what I need is the second column of the select to be the ordinal of the category (ie 1 for the directions and 2 for the colours in this case).

Note: The literal value 'rank()' in the select is left a placeholder. I was poking around with using the rank, but with no success.

declare @x xml
set @x = '
    <root>
     <category>
      <item value="north"/>
      <item value="south"/>
      <item value="east"/>
      <item value="west"/>
     </category>
     <category>
      <item value="red"/>
      <item value="green"/>
      <item value="blue"/>
     </category>
    </root>'

select c.value('./@value', 'varchar(10)') as "ItemValue", 
       'rank()' as "CategoryNumber"
from @x.nodes('//item') as t(c)
+1  A: 

Maybe like this: you get first element of each category and use it as an id.

This:

select c.value('./@value', 'varchar(10)') as "ItemValue", 
    c.value('../item[1]/@value', 'varchar(10)') as "CategoryNumber"
from @x.nodes('//item') as t(c)

Returns:

Item Value | CategoryNumber
---------------------------
north      | north
south      | north
east       | north
west       | north
red        | red
green      | red
blue       | red

And then just

select c.value('./@value', 'varchar(10)') as "ItemValue", 
   RANK() OVER (ORDER BY c.value('../item[1]/@value', 'varchar(10)')) as "CategoryNumber"
from @x.nodes('//item') as t(c)

It however returns:

Item Value | CategoryNumber
---------------------------
north      | 1
south      | 1
east       | 1
west       | 1
red        | 5
green      | 5
blue       | 5

But it is still step ahead.

Lukasz Lysik
As it turns out this will work just fine. I need to have the output grouped by category with some arbitrary value, but it doesn't have to be contiguous.
Ralph Shillington
+1  A: 

You cannot use position() to produce output (why??), but you can use it as XPath filters:

 with numbers (n) as (
  select 1
  union all select 2
  union all select 3
  union all select 4
  union all select 5)
 select i.x.value('@value', 'varchar(10)') as [ItemValue],
    n.n as [rank]
  from numbers n
  cross apply @x.nodes('/root/category[position()=sql:column("n.n")]') as c(x)
  cross apply c.x.nodes('item') as i(x);

You can use a real numbers table for higher ranks. For very large number of categories in a single document is not going to be efficient, but for moderate numbers (tens, hundreds) will work just fine.

Remus Rusanu
I'm glad you figured this out; I was trying to come up with something similar, but failing miserably.
Stuart Ainsworth
+2  A: 

Jacob Sebastian also has an interesting solution presented in his blog post:

XQuery Lab 23 - Retrieving values and position of elements

With Jacob's suggestion, I can rewrite your query to be:

SELECT
    x.value('@value','VARCHAR(10)') AS 'ItemValue',        
    p.number as 'CategoryNumber'
FROM
    master..spt_values p
CROSS APPLY 
    @x.nodes('/root/category[position()=sql:column("number")]/item') n(x) 
WHERE
    p.type = 'p'

and I get the desired output:

ItemValue   CategoryNumber
---------   --------------
north           1
south           1
east            1
west            1
red             2
green           2
blue            2

Unfortunately, none of the more obvious solutions like the position() or fn:id() functions seem to a) work in SQL Server or b) be supported in SQL Server at all :-(

Hope this helps

Marc

marc_s
So *this* is how you do it...
gbn