0

I apologize for the vague title. I couldn't think how best to summarize the problem. I have a hierarchical table (e.g., ID int, ParentID int) and need to generate a sub-tree for an ID. This is trivially done with a recursive CTE. The difficulty is that, for each node, I need to compute a running bitwise-OR of a set of corresponding values, and then bit-OR that result with the same value for the parent node. This means every node inherits its parent's bitmask and may set its own additional bits. I can compute this value in the anchor member of the CTE using OUTER APPLY and a technique mentioned in an earlier question I asked. Unfortunately, I can't compute it the same way in the recursive part of the CTE because it uses SUM and aggregates are not allowed there.

Is there a way to restructure this to do what I want?

declare @ID int
set @ID = 1

;with _Bits_(RowNum, BitMask) as
(
  select
    1,
    1
  union all select
    RowNum + 1,
    BitMask * 2
  from
    _bits_
  where
    RowNum < 31
),
_Tree_ as
(
  select
    a.ID,
    a.ParentID,
    b.BitMask
  from
    Tree a
    outer apply
    (
      select
        sum(distinct y.BitMask) as BitMask
      from
        BitValues x
        inner join _Bits_ y
          on (x.Value & y.BitMask) <> 0
      where
        x.ID = a.ID
    ) b
  where
    a.ID = @ID
  union all select
    a.ID,
    a.ParentID,
    c.BitMask | b.BitMask
  from
    Tree a
    inner join _Tree_ b
      on b.ID = a.ParentID
    outer apply
    (
      select
        sum(distinct y.BitMask) as BitMask
      from
        BitValues x
        inner join _Bits_ y
          on (x.Value & y.BitMask) <> 0
      where
        x.ID = a.ID
    ) c
)
select * from _Tree_

EDIT

If it helps to conceptualize the problem: the hierarchy is much like a directory structure, and the bitmasks are like permissions that are inherited from parent folders.

Example data

create table Tree (ID int primary key, ParentID int null foreign key references Tree (ID))

insert Tree values (1, null)
insert Tree values (2, 1)
insert Tree values (3, 1)

create table BitValues (ID int not null foreign key references Tree (ID), BitMask int not null)

insert BitValues values (1, 1)
insert BitValues values (2, 2)
insert BitValues values (2, 4)
insert BitValues values (3, 8)
insert BitValues values (3, 16)
insert BitValues values (3, 32)

For @ID 1, I would expect the query to return:

+----+----------+---------+
| ID | ParentID | BitMask |
+----+----------+---------+
|  1 |   NULL   |       1 |
|  2 |        1 |       7 |
|  3 |        1 |      57 |
+----+----------+---------+
Community
  • 1
  • 1
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • can you give us some example data? – Hogan Apr 28 '11 at 17:19
  • @Hogan: I updated the question with test data. – Daniel Apr 28 '11 at 18:44
  • I have an idea how to solve this -- I should be able to post in in a couple of hours -- have to do the commute thing. – Hogan Apr 28 '11 at 21:40
  • @Hogan - Cool. I look forward to seeing your idea. I came up with a solution as well, but it's a _lot_ of code. I can't post it because it because it contains confidential data, and it's too much to clean up. If you or someone else doesn't post an answer in the next day or two, I may post the general steps I took. – Daniel Apr 28 '11 at 21:48

2 Answers2

0
declare @ID int;
set @ID = 1;

with extrarows as
(
   select t.id, null as parent, v.BitMask as total
   from tree t
   join BitValues v on t.id = v.id
   where t.id = @ID

   union all 

   select t.id, r.id, v.BitMask | r.total
   from extrarows r
   join Tree t on r.id = t.parentid
   join BitValues v on t.id = v.id
)
select id, parent, 
  MAX(total & 1) +
  MAX(total & 2) +
  MAX(total & 4) +
  MAX(total & 8) +
  MAX(total & 16) +
  MAX(total & 32) +
  MAX(total & 128) +
  MAX(total & 256) +
  MAX(total & 512) +
  MAX(total & 1024) +
  MAX(total & 2048)  -- more if you want em.
     as BitMask 
from extrarows   
group by id, parent

Some notes:

  • I make an assumption the incoming @id is the "root" of the tree. (Feel free to crawl up the tree to find the starting bit mask of the root if this does not meet your needs.)

  • While summing MAX of the bits does work it might not be performant for large bit strings over many records. I don't know how many bits you have but it is less then 16 or so it should be fine -- like to hear about your findings.

  • To improve performance switch to a custom C# aggregate.

Hogan
  • 69,564
  • 10
  • 76
  • 117
  • Thanks. I think this can be shortened even further by replacing `MAX(total & 1) + ...` with `SUM(distinct BitMask)` etc, like I did in the `OUTER APPLY`. – Daniel Apr 29 '11 at 15:42
  • That won't work -- if two items have the same bit twice it will be added not ored (is ored a word?) -- It works in yours because you are not lazy on the recurse like I am -- have have extra items with the or of items above -- so in your example data id 2 would end up with a result of 8 (two of the 1 bits from parent would be added.) – Hogan Apr 29 '11 at 15:54
  • You would have to use a CTE like `_Bits_`, and join that to `extrarows` the way I joined it to `BitValues` in the `OUTER APPLY`. That effectively bit-OR's the unique list of bits that are set. – Daniel Apr 29 '11 at 15:58
  • If I understand you correctly: it doesn't matter if bits are duplicated, since bit-OR-ing 0..N of any one bit produces the same result. – Daniel Apr 29 '11 at 16:02
  • With my query that is correct, with the `sum(distinct bitmask)` it fails. You can see this even in this small test case. If you replace the select with `select * from extrarows` this should be clear. For id 2 it will add 3 and 5 to get 8. – Hogan Apr 29 '11 at 16:06
  • Maybe I'm not explaining it well. I wasn't sure how to show you the code, so I posted it as an answer. I get the same results using this code and yours. – Daniel Apr 29 '11 at 16:42
0

A slight refinement (IMO) of Hogan's answer:

declare @ID int;
set @ID = 1;

with _Bits_(RowNum, BitMask) as
(
  select
    1,
    1
  union all select
    RowNum + 1,
    BitMask * 2
  from
    _bits_
  where
    RowNum < 31
),
extrarows as
(
   select t.id, null as parent, v.BitMask as total
   from tree t
   join BitValues v on t.id = v.id
   where t.id = @ID

   union all 

   select t.id, r.id, v.BitMask | r.total
   from extrarows r
   join Tree t on r.id = t.parentid
   join BitValues v on t.id = v.id
)
select a.id, a.parent, sum(distinct y.BitMask) as BitMask
from extrarows a
  inner join _Bits_ y
    on (a.total & y.BitMask) <> 0  
group by a.id, a.parent
Daniel
  • 47,404
  • 11
  • 101
  • 179
  • I'd be interested to find out which is faster, this is less typing, but the work should be the same `inner join _Bits_ y on (a.total & y.BitMask) <> 0` should be the same number of operations as the ugly `MAX()` calls in my code. Please let us (me) know if one was more performent. – Hogan Apr 29 '11 at 17:00
  • Your version is about 60% faster, but we're talking about a difference of 8 milliseconds for the given test data. – Daniel Apr 29 '11 at 17:08
  • I think the expense will not scale with the data set. It seems to incur a one-time hit for building the `_Bits_` CTE. – Daniel Apr 29 '11 at 17:15
  • I had an idea how to combine the two versions, but I won't be able to test it till I get home tonight. – Hogan Apr 29 '11 at 19:38