How to handle recursion in a flat table? - sql

I have two tables that keep track of permissions for groups of users. The first table is just two columns, an identifier and a name, used solely for the names of the permissions. The second table is where the permissions are applied and parent permissions are assigned to create an hierarchy. My problem is that I'm using joins to create a permission hierarchy "string" based on parent permissions and, without knowing how deep that parent recursion might go, I have no way of knowing how many joins to make. My questions is, is there a more correct way to solve this problem?
I've included a complete working script, but I stripped unnecessary columns:
CREATE TABLE #TempPermissions
(
Permission_ID INT IDENTITY,
Permission VARCHAR(50)
)
CREATE TABLE #TempAppPermissions
(
AppPermission_ID INT IDENTITY,
Permission_ID INT,
Parent_ID INT
)
INSERT INTO #TempPermissions VALUES ('Users')
INSERT INTO #TempPermissions VALUES ('Add')
INSERT INTO #TempPermissions VALUES ('Edit')
INSERT INTO #TempPermissions VALUES ('Remove')
INSERT INTO #TempPermissions VALUES ('Permissions')
INSERT INTO #TempPermissions VALUES ('Configure')
INSERT INTO #TempAppPermissions VALUES (1, -1)
INSERT INTO #TempAppPermissions VALUES (2, 1)
INSERT INTO #TempAppPermissions VALUES (3, 1)
INSERT INTO #TempAppPermissions VALUES (4, 1)
INSERT INTO #TempAppPermissions VALUES (5, 1)
INSERT INTO #TempAppPermissions VALUES (6, 5)
SELECT app.AppPermission_ID,
(CASE WHEN NOT child3.Permission IS NULL THEN '/' + child3.Permission ELSE '' END)+
(CASE WHEN NOT child2.Permission IS NULL THEN '/' + child2.Permission ELSE '' END)+
'/' + child1.Permission AS PermissionString
FROM #TempAppPermissions app
INNER JOIN #TempPermissions child1
ON child1.Permission_ID = app.Permission_ID
LEFT JOIN #TempAppPermissions parent1
ON parent1.AppPermission_ID = app.Parent_ID
LEFT JOIN #TempPermissions child2
ON child2.Permission_ID = parent1.Permission_ID
LEFT JOIN #TempAppPermissions parent2
ON parent2.AppPermission_ID = parent1.Parent_ID
LEFT JOIN #TempPermissions child3
ON child3.Permission_ID = parent2.Permission_ID
DROP TABLE #TempPermissions, #TempAppPermissions
This provides me with the results:
AppPermission_ID PermissionString
1 /Users
2 /Users/Add
3 /Users/Edit
4 /Users/Remove
5 /Users/Permissions
6 /Users/Permissions/Configure
This works fine as is, but if I were to go another parent deep with:
INSERT INTO #TempPermissions VALUES ('Reports')
INSERT INTO #TempAppPermissions VALUES (7, 6)
I would have to compensate for it with another set of joins and another case expression in the select statement:
(CASE WHEN NOT child4.Permission IS NULL THEN '/' + child4.Permission ELSE '' END)+
...
LEFT JOIN #TempAppPermissions parent3
ON parent3.AppPermission_ID = parent2.Parent_ID
LEFT JOIN #TempPermissions child4
ON child4.Permission_ID = parent3.Permission_ID
If I do not, I will end up losing the topmost parent on the last result:
1 /Users
2 /Users/Add
3 /Users/Edit
4 /Users/Remove
5 /Users/Permissions
6 /Users/Permissions/Configure
7 /Permissions/Configure/Reports
Technically, I could repeat this any number of times to compensate for how deep that structure may go, but I have the feeling there is probably a better approach this problem. Thanks in advance.

I would use CTE (Common Table Expressions).
;WITH t AS (
SELECT 1 AS iteration, p.Permission_ID AS PermissionID, p.Permission_ID, CAST(N'/' + p.Permission AS NVARCHAR(MAX)) AS Permission
FROM #TempPermissions AS p
UNION ALL
SELECT iteration + 1, t.PermissionID, p.Parent_ID, COALESCE(N'/' + (SELECT s.Permission FROM #TempPermissions AS s WHERE s.Permission_ID = p.Parent_ID), N'') + t.Permission
FROM t INNER JOIN #TempAppPermissions AS p ON t.Permission_ID = p.Permission_ID
)
SELECT PermissionID, Permission FROM t
WHERE Permission_ID = -1
ORDER BY PermissionID, Iteration
Let me know if this helps!

Supplementing the top answer here since I can't post the code in a comment. After playing with JoeFletch's code a bit, I realized the actual recursion should only be happening with the table #TempAppPermissions. With JoeFletch's code, I would hit the maximum recursion of 100 on a larger table. Also the iteration is unimportant.
One thing to note is "SELECT r.AppPermission_ID," on line 13 in the recursion, because I need that child's ID (not the parent's) from #TempAppPermissions to reference back to the user to see if they have that permission. The "Permission_ID" from #TempPermissions is omitted from the select because it is only necessary to get the actual permission name and "Parent_ID" is only used to filter out single instances of dependent permissions.
Thanks again, JoeFletch.
;WITH r AS
(
SELECT p.AppPermission_ID,
p.Parent_ID,
CAST('/' + (SELECT s.Permission
FROM #TempPermissions AS s
WHERE s.Permission_ID = p.Permission_ID)
AS NVARCHAR(MAX)) AS Permission
FROM #TempAppPermissions p
UNION ALL
SELECT r.AppPermission_ID,
p.Parent_ID,
COALESCE(N'/' + (SELECT s.Permission
FROM #TempPermissions AS s
WHERE s.Permission_ID = p.Permission_ID), N'')
+ r.Permission
FROM r
INNER JOIN #TempAppPermissions p
ON p.AppPermission_ID = r.Parent_ID
)
SELECT r.AppPermission_ID, r.Permission
FROM r
WHERE r.Parent_ID = -1
ORDER BY r.AppPermission_ID ASC

Related

Dynamically update table with column from another table

I have a table customer like this:
CREATE TABLE tbl_customer (
id INTEGER,
name VARCHAR(16),
voucher VARCHAR(16)
);
and a voucher table like this:
CREATE TABLE tbl_voucher (
id INTEGER,
code VARCHAR(16)
);
Now imagine that the customer table always has rows with id and name filled in, however the voucher needs to be inserted periodically from the tbl_voucher table.
Important: every voucher may only be assigned to one specific customer (i.e. must be unique)
I wrote a query like this:
UPDATE tbl_customer
SET voucher = (
SELECT code
FROM tbl_voucher
WHERE code NOT IN (
SELECT voucher
FROM tbl_customer
WHERE voucher IS NOT NULL
)
LIMIT 1
)
WHERE voucher IS NULL;
However this is not working as expected, since the part that looks for an unused voucher is executed once and said voucher is then applied to every customer.
Any ideas on how I can solve this without using programming structures such as loops?
Also, some example data so you can imagine what I would like to happen:
INSERT INTO tbl_customer VALUES (1, 'Sara', 'ABC');
INSERT INTO tbl_customer VALUES (1, 'Simon', 'DEF');
INSERT INTO tbl_customer VALUES (1, 'Andy', NULL);
INSERT INTO tbl_customer VALUES (1, 'Alice', NULL);
INSERT INTO tbl_voucher VALUES (1, 'ABC');
INSERT INTO tbl_voucher VALUES (2, 'LOL');
INSERT INTO tbl_voucher VALUES (3, 'ZZZ');
INSERT INTO tbl_voucher VALUES (4, 'BBB');
INSERT INTO tbl_voucher VALUES (5, 'CCC');
After the wanted query is executed, I'd expect Andy to have the voucher LOL and Alice should get ZZZ
I am going to guess this is MySQL. The answer is that this is a pain. The following assigns the values in a select:
select c.*, v.voucher
from (select c.*, (#rnc := #rnc + 1) as rn
from tbl_customer c cross join
(select #rnc := 0) params
where c.voucher is null
) c join
(select v.*, (#rnv := #rnv + 1) as rn
from tbl_vouchers v cross join
(select #rnv := 0) params
where not exists (select 1 from tbl_customers c where c.voucher = v.voucher)
) v
on c.rn = v.rn;
You can now use this for the update:
update tbl_customer c join
(select c.*, v.voucher
from (select c.*, (#rnc := #rnc + 1) as rn
from tbl_customer c cross join
(select #rnc := 0) params
where c.voucher is null
) c join
(select v.*, (#rnv := #rnv + 1) as rn
from tbl_vouchers v cross join
(select #rnv := 0) params
where not exists (select 1 from tbl_customers c where c.voucher = v.voucher)
) v
on c.rn = v.rn
) cv
on c.id = cv.id
set c.voucher = cv.voucher;

SQL Inverse JOIN Query

MS-SQL Server
Table A (Study_ID,Issue_Id)
XX,1
BB,2
Table B (Study_ID,System_Id)
XX,User1
BB,User2
XX,User2
View V : (Issue_Id,System_Id)
2,User1
View V should give all Issues from Table A, for System_Id X, which are not in Table B for the combination of Study and SytemID
The purpose is, The table A has Issues(Issue_Id), which are tied to Study(Study_id). If A user User1 logs in into system he should be able to see all issues in table A apart from the ones which have study_id for which the user isn't having rights. Table B indicates the StudyId's for which the user has no rights
How can I achieve this in an efficient way?
You can try to make a list of all the combinations of Study_IDs AND System_IDs and then by a left join you can see whether or not the combination exists.
I'm a bit confused as to your comment about user rights. Is this more of a database issue, or is using a AND System_ID = 'User1' in the WHERE statement a solution?
WITH T_A AS (SELECT *
FROM ( VALUES ('XX', 1)
, ('BB', 2)
) x (Study_ID, Issue_ID)
)
, T_B AS (SELECT *
FROM ( VALUES ('XX', 'User1')
, ('BB', 'User2')
, ('XX', 'User2')
) x ( Study_ID, System_ID)
)
SELECT Issue_ID, USERS.System_ID
FROM T_A
INNER JOIN (SELECT DISTINCT System_ID FROM T_B) USERS
ON 1 = 1
LEFT JOIN T_B
ON T_A.Study_ID = T_B.Study_ID
AND USERS.System_ID = T_B.System_ID
WHERE T_B.Study_ID IS NULL
This is one way to do it:
select * from a
except
select * from b

Recursively sum the nodes of a tree using Postgresql WITH clause

(Using Postgresql 9.1)
I have a tree structure in the database and I need to sum the node's values. There are two caveats:
Not all nodes have a value.
If a parent node has a value, ignore the child values.
While recursing the tree is easy with the powerful recursive WITH clause, it's enforcing these two caveats that is breaking my code. Here's my setup:
CREATE TABLE node (
id VARCHAR(1) PRIMARY KEY
);
INSERT INTO node VALUES ('A');
INSERT INTO node VALUES ('B');
INSERT INTO node VALUES ('C');
INSERT INTO node VALUES ('D');
INSERT INTO node VALUES ('E');
INSERT INTO node VALUES ('F');
INSERT INTO node VALUES ('G');
CREATE TABLE node_value (
id VARCHAR(1) PRIMARY KEY,
value INTEGER
);
INSERT INTO node_value VALUES ('B', 5);
INSERT INTO node_value VALUES ('D', 2);
INSERT INTO node_value VALUES ('E', 0);
INSERT INTO node_value VALUES ('F', 3);
INSERT INTO node_value VALUES ('G', 4);
CREATE TABLE tree (
parent VARCHAR(1),
child VARCHAR(1)
);
INSERT INTO tree VALUES ('A', 'B');
INSERT INTO tree VALUES ('B', 'D');
INSERT INTO tree VALUES ('B', 'E');
INSERT INTO tree VALUES ('A', 'C');
INSERT INTO tree VALUES ('C', 'F');
INSERT INTO tree VALUES ('C', 'G');
This gives me the following tree (nodes and values):
A
|--B(5)
| |--D(2)
| |--E(0)
|
|--C
|--F(3)
|--G(4)
Given the rules above, here are the expected sum values:
A = (5 + 3 + 4) = 12
B = 5
D = 2
E = 0
C = (3 + 4) = 7
F = 3
G = 4
I have written the following SQL, but I can't integrate the recursive UNION and JOIN logic to enforce rule #1 and #2:
WITH recursive treeSum(root, parent, child, total_value) AS (
SELECT tree.parent root, tree.parent, tree.child, node_value.value total_value
FROM tree
LEFT JOIN node_value ON node_value.id = tree.parent
UNION
SELECT treeSum.root, tree.parent, tree.child, node_value.value total_value
FROM tree
INNER JOIN treeSum ON treeSum.child = tree.parent
LEFT JOIN node_value ON node_value.id = tree.parent
)
SELECT root, sum(total_value) FROM treeSum WHERE root = 'A' GROUP BY root
The query returns 10 for root A, but it should be 12. I know the UNION and/or JOIN logic is what's throwing this off. Any help would be appreciated.
EDIT: To clarify, the sum for A is 12, not 14. Given the rules, if a node has a value, grab that value and ignore its children. Because B has a value of 5 we ignore D and E. C has no value, so we grab its children, thus the sum of A = 5(B) + 3(F) + 4(G) = 12. I know it's odd but that's the requirement. Thanks.
EDIT 2: These results will be joined with external datasets so I can't hardcode the root in the WITH clause. For example, I might need something like this:
SELECT root, SUM(total_value) FROM treeSUM GROUP BY root WHERE root = 'A'
This tree is one of many so that means there's multiple roots, specified by calling code--not within the recursive clause itself. Thanks.
EDIT 3: An example of how this will be used in production is the roots will be specified by another table, so I can't hardcode the root into the recursive clause. There might be many roots from many trees.
SELECT id, SUM(COALESCE(value,0)) FROM treeSUM
INNER JOIN roots_to_select rts ON rts.id = treeSUM.id GROUP BY id
SOLUTION (Cleaned up from koriander's answer below)! The following allows roots to be specified by outside sources (either using roots_to_select or WHERE criteria:
WITH recursive roots_to_select AS (
SELECT 'A'::varchar as id
),
treeSum(root, id, value) AS (
select node.id as root, node.id, node_value.value
from node
inner join roots_to_select rts on (node.id = rts.id)
left join node_value on (node.id = node_value.id)
union
select treeSum.root, node.id, node_value.value
from treeSum
inner join tree on (treeSum.id = tree.parent)
inner join node on (tree.child = node.id)
left join node_value on (node.id = node_value.id)
where treeSum.value is null
)
select root, sum(coalesce(value, 0))
from treeSum
group by root
OUTPUT: 12
tested here:
with recursive treeSum(id, value) AS (
select node.id, node_value.value
from node
left join node_value on (node.id = node_value.id)
where node.id = 'A'
union
select node.id, node_value.value
from treeSum
inner join tree on (treeSum.id = tree.parent)
inner join node on (tree.child = node.id)
left join node_value on (node.id = node_value.id)
where treeSum.value is null
)
select sum(coalesce(value, 0)) from treeSum
Edit 1: to combine the result with other table, you can do:
select id, (select sum(coalesce(value, 0)) from treeSum) as nodesum
from node
inner join some_table on (...)
where node.id = 'A'
Edit 2: to support multiple roots based on your Edit 3, you can do (untested):
with recursive treeSum(root, id, value) AS (
select node.id as root, node.id, node_value.value
from node
inner join roots_to_select rts on (node.id = rts.id)
left join node_value on (node.id = node_value.id)
union
select treeSum.root, node.id, node_value.value
from treeSum
inner join tree on (treeSum.id = tree.parent)
inner join node on (tree.child = node.id)
left join node_value on (node.id = node_value.id)
where treeSum.value is null
)
select root, sum(coalesce(value, 0))
from treeSum
group by root

SQL Query to select the COUNT from another SQL query

I'm having a little trouble making an SQL SERVER 2000 query. Here is my scenario:
I have a table called Folders with 3 columns: pk_folderID, folderName and fk_userID.
Also, I have another table called FolderMedia which stores what media (whatever) belong to a certain folder. There are 2 columns: fk_folderID, fk_media.
And the last, I have a table called Media which stores some media details. It has a primary key pk_media and among other columns, it has a MediaType column which tells the type of that media: image or video.
Now, I would like a query that does the following:
Select all folders that belong to a certain fk_userID, and then also get the number of media in that folder. I've seen a query like this here on StackOverflow, but I didn't manage to upgrade it to get 2 counts of media (based on their type)
Basically, get the folder details (name, etc) for all folders that belong to a user(fk_userID) and also, for each folder get the number of images and videos in it (as separate values).
The select would basically return:
folderName, count(images in folder), count(videos in folder), other folder details.
One obvious solution would be to just get all folders and then manually calculate the number of images/videos in them... but I would first like to try with a query.
Thank you,
Basically something like this:
SELECT
f.pk_folderID,
f.folderName,
VideoCount = COUNT(CASE m.MediaType WHEN 'Video' THEN 1 END),
ImageCount = COUNT(CASE m.MediaType WHEN 'Image' THEN 1 END)
FROM Folder f
LEFT JOIN FolderMedia fm ON f.pk_folderID = fm.fk_folderID
LEFT JOIN Media m ON fm.fk_media = m.pk_media
WHERE f.fk_userID = #UserID
GROUP BY
f.pk_folderID,
f.folderName
UPDATE (based on the additional request):
To include a sort of TOP 1 Media.Name into the result set, the above query could be changed like this:
SELECT
f.pk_folderID,
f.folderName,
VideoCount = COUNT(CASE m.MediaType WHEN 'Video' THEN 1 END),
ImageCount = COUNT(CASE m.MediaType WHEN 'Image' THEN 1 END),
MediaName = MAX(CASE fm.timestamp WHEN t.timestamp THEN m.Name END)
FROM Folder f
LEFT JOIN FolderMedia fm ON f.pk_folderID = fm.fk_folderID
LEFT JOIN Media m ON fm.fk_media = m.pk_media
LEFT JOIN (
SELECT
fk_folderID,
timestamp = MIN(timestamp)
FROM FolderMedia
GROUP BY fk_folderID
) t ON fm.fk_folderID = t.fk_folderID AND fm.timestamp = t.timestamp
WHERE f.fk_userID = #UserID
GROUP BY
f.pk_folderID,
f.folderName
In cases where minimal FolderMedia.timestamp values are not unique within their folders, the ultimate value of the corresponding Media.Name will be decided by its alphabetical sorting. In particular, the above query selects the last one of the set (with MAX()).
Get all data you need from Folders table left join it with FolderMedia and Media
use sum with case inside to count all videos and images.
SUM(CASE WHEN mediaTypeId = videoId THEN 1 ELSE 0 END) as videoCount
//select folderName ,count(case when Folders.pk_folderID then 1 else null end)
count(SELECT FolderMedia.fk_media from FolderMedia JOIN Media on fk_media=pk_media where image is not null) as nrImg
etc..
Here is a query with some example data. Hope it helps.
declare #Folders table (pk_folderID int, folderName varchar(32), fk_userID int)
declare #Media table (pk_media int, name varchar(50), type varchar(32))
declare #FolderMedia table (fk_folderID int, fk_media int)
insert into #Folders values (1, 'Folder1', 1000)
insert into #Folders values (2, 'Folder2', 1000)
insert into #Folders values (3, 'Folder1', 2000)
insert into #Folders values (4, 'Folder1', 2000)
insert into #Media values (1, 'graph.jpg', 'image')
insert into #Media values (2, 'timer.jpg', 'image')
insert into #Media values (3, 'timer1.jpg', 'image')
insert into #Media values (4, 'harry_potter.mpeg', 'video')
insert into #Media values (5, 'harry_potter1.mpeg', 'video')
insert into #Media values (6, 'harry_potter2.mpeg', 'video')
insert into #FolderMedia values (1, 1)
insert into #FolderMedia values (1, 3)
insert into #FolderMedia values (1, 6)
insert into #FolderMedia values (2, 2)
insert into #FolderMedia values (2, 4)
select folderName, fk_userID, imageData.imgCount, videoData.videoCount from
#Folders
left outer join
(
select fk_folderID, COUNT(*) as imgCount
from #FolderMedia
inner join #Media
on fk_media = pk_media
and type = 'image'
group by fk_folderID
) as imageData
on imageData.fk_folderID = pk_folderID
left outer join
(
select fk_folderID, COUNT(*) as videoCount
from #FolderMedia
inner join #Media
on fk_media = pk_media
and type = 'video'
group by fk_folderID
) as videoData
on videoData.fk_folderID = pk_folderID
where fk_userID = 1000

Recursive SQL to Create DNN Navigation Menu

I'm working on a DotNetNuke module that includes a tree-style navigation menu.
So far, I have it working, in the sense that child-nodes are connected to their correct parent-nodes, but the node-siblings are still out of order. There's a field called TabOrder, used to determine the order of siblings, but due to the recursion, I can't get them sorted properly.
I'm trying to do this in a SQL Server stored procedure, which may be a mistake, but I feel I'm so close that there must be a solution. Does anyone have any idea what I'm doing wrong?
I'd appreciate any ideas you have. Thanks in advance.
Solution:
I finally found a solution to my question. The key was to recursively create a Tab Lineage (TabLevel + TabOrder) from the Root Tab to the Leaf Tabs. Once that was created, I was able to order the returned records properly.
However, as I was coming back to post this I saw MarkXA's answer, which is probably the best solution. I didn't know the method GetNavigationNodes even existed.
I think he is correct that using GetNavigationNodes is a more future-proof solution, but for the time being I'll use my SQL-based solution. --What can I say? I learn the hard way.
Here it is:
ALTER procedure [dbo].[Nav_GetTabs]
#CurrentTabID int = 0
AS
--============================================================
--create and populate #TabLineage table variable with Tab Lineage
--
--"Lineage" consists of the concatenation of TabLevel & TabOrder, concatenated recursively from the root to leaf.
--The lineage is VERY important, making it possible to properly order the Tab links in the navigation module.
--This will be used as a lookup table to match Tabs with their lineage.
--============================================================
DECLARE #TabLineage table
(
TabID int,
Lineage varchar(100)
);
WITH TabLineage AS
(
--start with root Tabs
SELECT T.TabID, T.ParentID, CAST(REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
FROM Tabs T
INNER JOIN Tabs T2 ON T.TabID = T2.TabID
INNER JOIN TabPermission TP ON T.TabID = TP.TabID
WHERE T.ParentID IS NULL
AND T.IsDeleted = 0
AND T.IsVisible = 1
AND TP.RoleID = -1
UNION ALL
--continue recursively, from parent to child Tabs
SELECT T.TabID, T.ParentID, CAST(TL.Lineage + REPLICATE('0', 5 - LEN(CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)))) + CAST(T2.[Level] as varchar(10)) + CAST(T2.TabOrder as varchar(10)) as varchar(100)) AS Lineage
FROM Tabs T
INNER JOIN Tabs T2 ON T.TabID = T2.TabID
INNER JOIN TabPermission TP ON T.TabID = TP.TabID
INNER JOIN TabLineage TL ON T.ParentID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
AND TP.RoleID = -1
)
--insert results of recursive query into temporary table
INSERT #TabLineage
SELECT TL.TabID, TL.Lineage FROM TabLineage TL ORDER BY TL.Lineage
OPTION (maxrecursion 10); --to increase number of traversed generations, increase "maxrecursion"
--============================================================
--create and populate #Ancestor table variable with #CurrentTab ancestors
--
--"Ancestors" are Tabs following the path from #CurrentTab to the root Tab it's descended from (inclusively).
--These are Tab links we want to see in the navigation.
--============================================================
DECLARE #Ancestor table
(
TabID int
);
WITH Ancestor AS
(
--start with #CurrentTab
SELECT T.TabID, T.ParentID FROM Tabs T WHERE T.TabID = #CurrentTabID
UNION ALL
--continue recursively, from child to parent Tab
SELECT T.TabID, T.ParentID
FROM Ancestor A INNER JOIN Tabs T ON T.TabID = A.ParentID
)
--insert results of recursive query into temporary table
INSERT #Ancestor
SELECT A.TabID FROM Ancestor A
OPTION (maxrecursion 10); --to increase number of traversed generations, increase "maxrecursion"
--============================================================
--retrieve Tabs to display in navigation
--This section UNIONs three query results together, giving us what we want:
-- 1. All Tabs at Level 0.
-- 2. All Tabs in #CurrentTab's lineage.
-- 3. All Tabs which are children of Tabs in #CurrentTab's lineage.
--============================================================
WITH TabNav (TabID, TabLevel, TabName, Lineage) AS
(
--retrieve all Tabs at Level 0 -- (Root Tabs)
(SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN #TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
AND T.[Level] = 0
UNION
--retrieve Tabs in #CurrentTab's lineage
SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN #Ancestor A ON T.TabID = A.TabID
INNER JOIN #TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1
UNION
--retrieve Tabs which are children of Tabs in #CurrentTab's lineage
SELECT T.TabID, T.[Level] AS TabLevel, T.TabName, TL.Lineage
FROM Tabs T
INNER JOIN TabPermission TP ON (T.TabID = TP.TabID AND TP.RoleID = -1)
INNER JOIN #Ancestor A ON T.ParentID = A.TabID
INNER JOIN #TabLineage TL ON T.TabID = TL.TabID
WHERE T.IsDeleted = 0
AND T.IsVisible = 1)
)
--finally, return the Tabs to be included in the navigation module
SELECT TabID, TabLevel, TabName FROM TabNav ORDER BY Lineage;
--============================================================
The answer is "don't use SQL". There's already a method DotNetNuke.UI.Navigation.GetNavigationNodes that does this for you, and if you use it then your module won't break if and when the database schema changes. Even if you need to do something that GetNavigationNodes won't handle, you're still better off retrieving the pages via the API to be futureproof. Going directly to the database is just asking for trouble :)
here is a boiler plate (not based on the given OP's code) example of a recursive tree CTE, which shows how to sort a tree:
DECLARE #Contacts table (id varchar(6), first_name varchar(10), reports_to_id varchar(6))
INSERT #Contacts VALUES ('1','Jerome', NULL ) -- tree is as follows:
INSERT #Contacts VALUES ('2','Joe' ,'1') -- 1-Jerome
INSERT #Contacts VALUES ('3','Paul' ,'2') -- / \
INSERT #Contacts VALUES ('4','Jack' ,'3') -- 2-Joe 9-Bill
INSERT #Contacts VALUES ('5','Daniel','3') -- / \ \
INSERT #Contacts VALUES ('6','David' ,'2') -- 3-Paul 6-David 10-Sam
INSERT #Contacts VALUES ('7','Ian' ,'6') -- / \ / \
INSERT #Contacts VALUES ('8','Helen' ,'6') -- 4-Jack 5-Daniel 7-Ian 8-Helen
INSERT #Contacts VALUES ('9','Bill ' ,'1') --
INSERT #Contacts VALUES ('10','Sam' ,'9') --
DECLARE #Root_id varchar(6)
--get all nodes 2 and below
SET #Root_id=2
PRINT '#Root_id='+COALESCE(''''+#Root_id+'''','null')
;WITH StaffTree AS
(
SELECT
c.id, c.first_name, c.reports_to_id, c.reports_to_id as Manager_id, cc.first_name AS Manager_first_name, 1 AS LevelOf
FROM #Contacts c
LEFT OUTER JOIN #Contacts cc ON c.reports_to_id=cc.id
WHERE c.id=#Root_id OR (#Root_id IS NULL AND c.reports_to_id IS NULL)
UNION ALL
SELECT
s.id, s.first_name, s.reports_to_id, t.id, t.first_name, t.LevelOf+1
FROM StaffTree t
INNER JOIN #Contacts s ON t.id=s.reports_to_id
WHERE s.reports_to_id=#Root_id OR #Root_id IS NULL OR t.LevelOf>1
)
SELECT * FROM StaffTree ORDER BY LevelOf, first_name
OUTPUT:
#Root_id='2'
id first_name reports_to_id Manager_id Manager_first_name LevelOf
------ ---------- ------------- ---------- ------------------ -----------
2 Joe 1 1 Jerome 1
6 David 2 2 Joe 2
3 Paul 2 2 Joe 2
5 Daniel 3 3 Paul 3
8 Helen 6 6 David 3
7 Ian 6 6 David 3
4 Jack 3 3 Paul 3
(7 row(s) affected)
The key is the LevelOf column. See how it is just a literal 1 when selecting the main parent in the CTE. The LevelOf column is then incremented in the UNION ALL portion of the recursive CTE. Each recursive call (not row) to the CTE will hit that UNION ALL one time and the increment. Not a whole lot more to it than that.