views:

84

answers:

3

I'm using the nested set model that'll later be used to build a sitemap for my web site. This is my table structure.

create table departments (
    id int identity(0, 1) primary key
    , lft int
    , rgt int
    , name nvarchar(60)
);

insert into departments (lft, rgt, name) values (1, 10, 'departments');
insert into departments (lft, rgt, name) values (2, 3, 'd');
insert into departments (lft, rgt, name) values (4, 9, 'a');
insert into departments (lft, rgt, name) values (5, 6, 'b');
insert into departments (lft, rgt, name) values (7, 8, 'c');

How can I sort by depth as well as name? I can do

select
    replicate('----', count(parent.name) - 1) + ' ' + node.name
    , count(parent.name) - 1 as depth
, node.lft
from
    departments node
    , departments parent
where
    node.lft between parent.lft and parent.rgt
group by
    node.name, node.lft
order by
    depth asc, node.name asc;

However, that does not match children with their parent for some reason.

department      lft     rgt
---------------------------
 departments    0       1
---- a        1        4
---- d        1        2
-------- b    2        5
-------- c    2        7

As you can see, department 'd' has department 'a's children!

Thank you.

+1  A: 

The below will work with your example, although if a name contains the "-" character then it might break down. It might serve as a starting point though. This uses CTEs, which are specific to SQL Server I believe. If I think of a more generic ANSI SQL method, I'll post that as well.

;WITH Tree_Path AS (
    SELECT
        lft,
        rgt,
        name,
        CAST(name + '-' AS VARCHAR(MAX)) AS tree_path,
        1 AS depth
    FROM
        dbo.departments
    WHERE
        lft = 1
    UNION ALL
    SELECT
        c.lft,
        c.rgt,
        c.name,
        CAST(tp.tree_path + c.name + '-' AS VARCHAR(MAX)),
        tp.depth + 1
    FROM
        Tree_Path tp
    INNER JOIN dbo.departments AS c ON
        c.lft > tp.lft AND
        c.lft < tp.rgt AND
        NOT EXISTS (SELECT * FROM dbo.departments d WHERE d.lft < c.lft AND d.rgt > c.lft AND d.lft > tp.lft AND d.lft < tp.rgt))
SELECT
    REPLICATE('----', depth - 1) + name,
    depth - 1,
    lft
FROM
    Tree_Path
ORDER BY
    tree_path,
    name
Tom H.
Looks like the WITH clause is SQL Server >2005. Any way to replicate the functionality of your query in maybe an inline view?
Mike
I don't think so. Unfortunately, this basically involves recursion because you have to get the name(s) from all descendants. Ironic, since that's one of the biggest reasons for using the nested-set model - to get rid of recursion. Of course, the linked-list model wouldn't help with avoiding recursion either.
Tom H.
BTW, you've probably noticed this already, but a simple "ORDER BY lft" almost gets you there - children sit nicely under parents, but within a depth it's not sorted alphabetically.
Tom H.
+1  A: 

I think I finally came up with an ANSI SQL solution. The basic gist is that it counts the number of rows that either have parents with a lower valued name on the same level as one of the node's own parent or is itself on the same level as the node and has a lower valued name. You'll need to make a minor adjustment to it in order to add the indenting if you want it. Also, I don't know how performance on a large data set will be due to all of the subqueries:

SELECT
    N1.name
FROM
    dbo.departments N1
ORDER BY
    (
    SELECT
        COUNT(DISTINCT N2.lft)
    FROM
        dbo.departments N2
    INNER JOIN (
                SELECT
                    N.name,
                    N.lft,
                    N.rgt,
                    (SELECT COUNT(*) FROM dbo.departments WHERE lft < N.lft AND rgt > N.lft) AS depth
                FROM
                    dbo.departments N) SQ1 ON
        SQ1.lft <= N2.lft AND SQ1.rgt >= N2.lft
    INNER JOIN (
                SELECT
                    N3.name,
                    N3.lft,
                    N3.rgt,
                    (SELECT COUNT(*) FROM dbo.departments WHERE lft < N3.lft AND rgt > N3.lft) AS depth
                FROM
                    dbo.departments N3) SQ2 ON
        SQ2.lft <= N1.lft AND SQ2.rgt >= N1.lft AND
        SQ2.depth = SQ1.depth AND
        SQ2.name > SQ1.name
    )

Let me know if you come up with any situations where it breaks.

Tom H.
+1  A: 

There is a mismatch in the question. The query returns: node.name, depth, and node.lft -- yet the results table is labeled with:

department      lft     rgt

Anyway, that query is returning the correct results for department level -- which is apparently not what you want depth to mean in this case. Both a and d are top-level departments.

If you want the number of sub-departments, and the nested-set is kept properly maintained, then the query is simply:

SELECT 
    D1.name,
    (D1.rgt - D1.lft - 1) / 2    AS SubordinateDepartments
FROM
    departments AS D1
ORDER BY
    SubordinateDepartments DESC,
    D1.name
Brock Adams
I don't think he wants a count of children. He's looking for a listing of all nodes, but in a tree structure so that children are listed after parents, but within any given level the nodes are alphabetized by name.
Tom H.
The question needs clarification from the OP. Ideally a table of his desired results.
Brock Adams