views:

137

answers:

2

I have the following recursive table-valued function in MS SQL, in order to retrieve a hierarchy of objects from the database:


WITH tmpField (ParentNum, ChildNum, FieldNum, FieldDescr, Iteration) AS
(
  SELECT Field.ParentNum, Field.ChildNum, Field.FieldNum, Field.FieldDescr, 1
  FROM Field
  WHERE Field.ParentNum = @ParentNum

  UNION ALL 

  SELECT Field.ParentNum, Field.ChildNum, Field.FieldNum, Field.FieldDescr, tmpField.Iteration + 1
  FROM Field INNER JOIN 
  tmpField on Field.ParentNum = tmpField.ChildNum
)
SELECT DISTINCT ParentNum AS ParentNum, ChildNum AS ChildNum, FieldNum, FieldDescr
FROM tmpField

I want to modify it in the following way:

In the last iteration, when there are no more 'children', I want the ChildNum field to have the value of FieldNum. In all previous iterations, ChildNum should have the value of the ChildNum field, as it is now.

Can anyone suggest a method to achieve this, using the above query as a starting point?

Please note: despite its name, the field ChildNum does not reference any children of a row, but it should be interpreted as the identifier of that row.

+2  A: 

when there are no more children it means that ChildNum is NULL so:

...

UNION ALL 

  SELECT Field.ParentNum, 
         COALESCE(Field.ChildNum, Field.FieldNum) ChildNum,
         Field.FieldNum,
         ...


EDIT: (following Daan comment)

ok, in that case, we could check on ChildNum 'children' count :

    ...

    UNION ALL 

      SELECT F1.ParentNum, 
             CASE WHEN (SELECT COUNT(1) 
                          FROM FIELD F2 
                         WHERE F2.ParentNum = F1.ChildNum) = 0 
                  THEN F1.FieldNum
                  ELSE F1.ChildNum
             END ChildNum,
             F1.FieldNum, F1.FieldDescr, tmpField.Iteration + 1
      FROM Field F1 INNER JOIN 
      tmpField on F1.ParentNum = tmpField.ChildNum

...


EDIT2:

let's move the check outside:

WITH tmpField (ParentNum, ChildNum, FieldNum, FieldDescr, Iteration) AS
(
  SELECT Field.ParentNum, Field.ChildNum, Field.FieldNum, Field.FieldDescr, 1
  FROM Field
  WHERE Field.ParentNum = @ParentNum

  UNION ALL 

  SELECT Field.ParentNum, Field.ChildNum, Field.FieldNum, Field.FieldDescr, tmpField.Iteration + 1
  FROM Field INNER JOIN 
  tmpField on Field.ParentNum = tmpField.ChildNum
)
SELECT DISTINCT ParentNum AS ParentNum, 
                CASE WHEN EXISTS (SELECT NULL 
                                    FROM Field f 
                                   WHERE tmpField.ChildNum = f.ParentNum) 
                      THEN tmpField.ChildNum
                      ELSE tmpField.FieldNum
                 END ChildNum,
                FieldNum,
                FieldDescr
FROM tmpField
najmeddine
No, unfortunately, ChildNum is not null in this case. You should read ChildNum as the identifier of the current row, not as a refernce to another row. The naming is awkward, I know, but that is out of my control unfortunately :)
Daan
Your edit looks like a nice way to accomplish this. Unfortunatly, when trying to alter my function, it now gives the following error: GROUP BY, HAVING, or aggregate functions are not allowed in the recursive part of a recursive common table expression 'tmpField'. Any suggestions for a workaround?
Daan
Instead of CASE WHEN (SELECT COUNT(1)...)=0 THEN FieldNum ELSE ChildNum ENDyou should useCASE WHEN EXIST (Select * from ...) THEN ChildNum ELSE FieldNum ENDThe second one performs faster
Niikola
Why not. I worked with Daan on the logic to obtain what he wants (you can see the iterations).
najmeddine
Logic is OK, it returns what he wants.The problem is in efficiency (performance of the query). Compare execution plans of your solution with two posted below. You will see difference.EXISTS performs faster as it is stop searching as soon as it finds one occurence, while COUNT has to scan all rows to make coount. In this case the exact number of occurences is irellevant - you need to know if child EXISTS or not.
Niikola
+1  A: 

This should return data you need. I removed iteration as you don't use it later

JOIN VERSION

;WITH tmpField (ParentNum, ChildNum, FieldNum, FieldDescr) AS
(
  SELECT f.ParentNum, f.ChildNum, f.FieldNum, f.FieldDescr
    FROM Field f
   WHERE f.ParentNum = @ParentNum
  UNION ALL 
  SELECT f.ParentNum, f.ChildNum, f.FieldNum, f.FieldDescr
    FROM Field f 
   INNER JOIN tmpField on f.ParentNum = tmpField.ChildNum
)
SELECT t.ParentNum AS ParentNum, 
       Case When p.ParentNum is Null 
            Then t.FieldNum 
            Else t.ChildNum 
        End AS ChildNum, 
       t.FieldNum, 
       t.FieldDescr
FROM tmpField t
Left Join (Select distinct ParentNum From Field) p on t.ChildNum=p.ParentNum

or

SUBQUERY VERSION (modified to use EXISTS instead of COUNT)

;WITH tmpField (ParentNum, ChildNum, FieldNum, FieldDescr) AS
(
  SELECT f.ParentNum, f.ChildNum, f.FieldNum, f.FieldDescr
    FROM Field f
   WHERE f.ParentNum = @ParentNum
  UNION ALL 
  SELECT f.ParentNum, f.ChildNum, f.FieldNum, f.FieldDescr
    FROM Field f 
   INNER JOIN tmpField on f.ParentNum = tmpField.ChildNum
)
SELECT t.ParentNum AS ParentNum, 
       Case When Exists(Select * from Field p Where t.ChildNum=p.ParentNum)
            Then t.ChildNum 
            Else t.FieldNum 
        End AS ChildNum, 
       t.FieldNum, 
       t.FieldDescr
FROM tmpField t
Niikola
Both above posted queries perfores twice faster than solution with COUNT
Niikola