T-SQL: Single Table contains Permissions Tree - sql

I have a SQL Server Table that contains a 'Hierarchy/Tree' of User Permissions.
Each Individual Permission can have values: 1 [Allowed], Blank [Not Allowed] & 0 [specifically Cancelled].
Each Individual Permission can be in one or more 'Permission Groups' & a User can be assigned all the Individual Permissions in one or more Permission Groups.
Each of the 'Permission Groups', in turn, can be in one or more higher level permission groups ... and eventually, all Permissions Groups are under a Master Group named 'Main Menu'.
This SQL Code:
Select
'Main Menu' Base,
Description Level1,
ParentId,
SecurityNodesId,
ListOrder,
Category,
LastModified
From SecurityNodes
Where ParentId = 1
Order By Description
Produces the following Output:
'Main Menu' has a ParentId of NULL [Not Shown in screenshot].
The 'Level1' 'Folders' contain other folders or Individual Permissions which are 'Referenced' by the Values under SecurityNodesId.
For instance, a search for SecurityNodesId 102 [Level1 - Administration] in the ParentId column returns this list of Sub Folders under 'Level2':
So ... I can access each of these sub folders by writing separate queries.
But what I want is to have an end result that displays every Node of this Permissions Tree in Table form like this:
Main Menu Level1 Level2 Level3 Level4 PermissionName PermissionValue
I have never had to do something this complex before, though I have done plenty of self-joins.
I am currently thinking that I would need to do a self join to each self join ... to get to successive Levels of the Tree ... but I believe there may be a 'recursive' approach to this that might be more efficient?
I would appreciate any help I can get with this.
Thanks in advance!

The way to solve this is with a Recursive CTE.
These are definitely more advanced than your usual SQL, but once you have your head wrapped around them, they are pretty easy to put together and VERY useful for hierarchical data (any table that stores a parent/child relationship).
A recursive CTE has two parts, separated by a UNION ALL.
The recursive seed which is ran only once and determines the starting result set for the recursion. For you, this is likely any record with a parentId of 1.
The recursive term (or member) which joins the cte (itself) to the table that holds the parent/child relationship. It will run over and over and over and over again until the Join or a WHERE filter causes it to return no new records.
In your case, it will look something like below. Note that I don't know what your starting table looks like. Namely the Level1 column from your original SQL isn't clear if that is the column name or an alias you call Level1. Furthermore it's not at all clear how you derive a "Permission Group" or "Permission Value" from this data. But... at any rate this should get you in the ballpark:
WITH reccte as (
/*
* To start the recursion we need a "Seed"... or a set of data
* that defines the starting point on which we iterate after
* the UNION ALL below.
*
* The seed here is all records with a parentid of 1
*/
SELECT Base,
ParentID,
SecurityNodesID,
Level as Level1,
NULL as Level2,
NULL as Level3,
NULL as Level4,
'?' as PermissionName,
Category as PermissionValue,
1 as depth, --track how deep we recurse
Base + '>' + Level as path --keep track of where we've been and what has led us to this point in recurssion
FROM SecurityNodes
UNION ALL
/*
* This section is the part that iterates. It continues to join
* all rows that have been collected up that point with the Security
* Nodes table until that join fails.
*/
SELECT
reccte.Base,
SecurityNodes.ParentID,
SecurityNodes.SecurityNodesID,
reccte.Level1,
/*
* Depending on how deep we are in the security hierarchy
* capture the level string to the appropriate column
*/
CASE WHEN depth = 1 THEN SecurityNodes.Level ELSE reccte.Level2,
CASE WHEN depth = 2 THEN SecurityNodes.Level ELSE reccte.Level3,
CASE WHEN depth = 3 THEN SecurityNodes.Level ELSE reccte.Level4,
'?' as PermissionName,
SecurityNodes.Category as PermissionValue,
reccte.depth + 1, --increment depth
reccte.path + '>' + SecurityNodes.Level --add to the path so we know how we got here
FROM reccte
INNER JOIN SecurityNodes
/*Join parent to child*/
ON reccte.SecurityNodesId = SecurityNodes.parentId
WHERE depth < 5 --Stop looking up if we go deeper than 4 levels.
)
SELECT *
FROM reccte
While we track depth here and stop the recursion if we hit a depth of 4, you could stop the recursion with the MAXRECURSIVE option/hint. That would just go at the end of your query:
SELECT *
FROM reccte
OPTION (MAXRECURSION 4);
It's important to add either/or to your recursive CTE otherwise you risk causing an infinite loop should a security node have a child that is also one of its ancestors which would cause it to cycle endlessly.
OPTION (MAXRECURSION 2);

I followed through on an idea I mentioned in my original post and it looks like I have achieved what I was wanting.
I don't think it is the best possible solution because I know how many total levels there currently are. If we suddenly add another level or two, the SQL will not capture everything and I'll manually have to add one or more Left Joins.
Select
'Main Menu' Base,
sn.Description Level1,
sn2.Description Level2,
sn3.Description Level3,
sn4.Description Level4,
sn.ParentId,
sn.SecurityNodesId,
sn.ListOrder,
sn.Category,
sn.LastModified
From
SecurityNodes sn
Left Join SecurityNodes sn2 On sn2.ParentId = sn.SecurityNodesId
Left Join SecurityNodes sn3 On sn3.ParentId = sn2.SecurityNodesId
Left Join SecurityNodes sn4 On sn3.ParentId = sn3.SecurityNodesId
Order By sn.ParentId, sn.Description
I would still appreciate any suggestions for a more elegant/dynamic way of achieving what I need ... but for now, the above SQL is doing the job.

Related

How to correctly order a recursive DB2 query

Hello friendly internet wizards.
I am attempting to extract a levelled bill of materials (BOM) from a dataset, running in DB2 on an AS400 server.
I have constructed most of the query (with a lot of help from online resources), and this is what I have so far;
#set item = '10984'
WITH BOM (origin, PMPRNO, PMMTNO, BOM_Level, BOM_Path, IsCycle, IsLeaf) AS
(SELECT CONNECT_BY_ROOT PMPRNO AS origin, PMPRNO, PMMTNO,
LEVEL AS BOM_Level,
SYS_CONNECT_BY_PATH(TRIM(PMMTNO), ' : ') BOM_Path,
CONNECT_BY_ISCYCLE IsCycle,
CONNECT_BY_ISLEAF IsLeaf
FROM MPDMAT
WHERE PMCONO = 405 AND PMFACI = 'M01' AND PMSTRT = 'STD'
START WITH PMPRNO = :item
CONNECT BY NOCYCLE PRIOR PMMTNO = PMPRNO)
SELECT 0 AS BOM_Level, '' AS BOM_Path, MMITNO AS Part_Number, MMITDS AS Part_Name,
IFSUNO AS Supplier_Number, IDSUNM AS Supplier_Name, IFSITE AS Supplier_Part_Number
FROM MITMAS
LEFT OUTER JOIN MITVEN ON MMCONO = IFCONO AND MMITNO = IFITNO AND IFSUNO <> 'ZGA'
LEFT OUTER JOIN CIDMAS ON MMCONO = IDCONO AND IDSUNO = IFSUNO
WHERE MMCONO = 405
AND MMITNO = :item
UNION ALL
SELECT BOM.BOM_Level, BOM_Path, BOM.PMMTNO AS Part_Number, MMITDS AS Part_Name,
IFSUNO AS Supplier_Number, IDSUNM AS Supplier_Name, IFSITE AS Supplier_Part_Number
FROM BOM
LEFT OUTER JOIN MITMAS ON MMCONO = 405 AND MMITNO = BOM.PMMTNO
LEFT OUTER JOIN MITVEN ON IFCONO = MMCONO AND IFITNO = MMITNO AND IFSUNO <> 'ZGA' AND MMMABU = '2'
LEFT OUTER JOIN CIDMAS ON MMCONO = IDCONO AND IDSUNO = IFSUNO
;
This is correctly extracting the components for a given item, as well as the sub-components (etc).
Current data looks like this (I have stripped out some columns that aren't relevant to the issue);
https://pastebin.com/LUnGKRqH
My issue is the order that the data is being presented in.
As you can see in the pastebin above, the first column is the 'level' of the component. This starts with the parent item at level 0, and can theoretically go down as far as 99 levels.
The path is also show there, so for example the second component 853021 tells us that it's a 2nd level component, the paths up to INST363 (shown later in the list as a 1st level), then up to the parent at level 0.
I would like for the output to show in path order (for lack of a better term).
Therefore, after level 0, it should be showing the first level 1 component, and then immediately be going into it's level 2 components and so on, until no further level is found. Then at that point, it returns back up the path to the next valid record.
I hope I have explained that adequately, but essentially the data should come out as;
Level
Path
Item
0
10984
1
: INST363
INST363
2
: INST363 : 853021
853021
1
: 21907
21907
Any help that can be provided would be very much appreciated!
Thanks,
This is an interesting query. Frankly I am surprised it works as well as it does since it is not structured the way I usually structure queries with a recursive CTE. The main issue is that while you have the Union in there, it does not appear to be within the CTE portion of the query.
When I write a recursive CTE, it is generally structured like this:
with cte as (
priming select
union all
secondary select)
select * from cte
So to get a BOM from an Item Master that looks something like:
CREATE TABLE item (
ItemNo Char(10) PRIMARY KEY,
Description Char(50));
INSERT INTO item
VALUES ('Item0', 'Root Item'),
('Item1a', 'Second Level Item'),
('Item1b', 'Another Second Level Item'),
('Item2a', 'Third Level Item');
and a linkage table like this:
CREATE TABLE linkage (
PItem Char(10),
CItem Char(10),
Quantity Dec(5,0),
PRIMARY KEY (PItem, CItem));
INSERT INTO linkage
VALUES ('Item0', 'Item1a', 2),
('Item0', 'Item1b', 3),
('Item1b', 'Item2a', 5)
The recursive CTE to list a BOM for 'Item0' looks like this:
WITH bom (Level, ItemNo, Description, Quantity)
AS (
-- Load BOM with root item
SELECT 0,
ItemNo,
Description,
1
FROM Item
WHERE ItemNo = 'Item0'
UNION ALL
-- Retrieve all child items
SELECT a.Level + 1,
b.CItem,
c.Description,
a.Quantity * b.Quantity
FROM bom a
join linkage b ON b.pitem = a.itemno
join item c ON c.itemno = b.citem)
-- Set the list order
SEARCH DEPTH FIRST BY itemno SET seq
-- List BOM
SELECT * FROM bom
ORDER BY seq
Here are my results:
LEVEL
ITEMNO
DESCRIPTION
QUANTITY
0
Item0
Root Item
1
1
Item1a
Second Level Item
2
1
Item1b
Another Second Level Item
3
2
Item2a
Third Level Item
15
Notice the search clause, that generates a column named seq which you can use to sort the output either depth first or breadth first. Depth first is what you want here.
NOTE: This isn't necessarily an optimum query since the description is in the CTE, and that increases the size of the CTE result set without really adding anything to it that couldn't be added in the final select. But it does make things a bit simpler since the 'priming query' retrieves the description.
Note also: the column list on the with clause following BOM. This is there to remove the confusion that DB2 had with the expected column list when the explicit column list was omitted. It is not always necessary, but if DB2 complains about an invalid column list, this will fix it.

SQL Modeling Pyramid or Binary Tree

I'm building a sql server project that have a pyramid or binary tree concept...
I gonna try to explain using some tables!
The first table is
TB_USER(ID, ID_FATHER, LEFT/RIGHT TREE POSITION)
User can sell producs! So when they sell they earn points. Then, the second table is
TB_SELL (ID_USER, ID_PRODUCT, POINT)
As a result I'd like to see in the report format of points of each client below me in the binary model tree. How can I design these tables to make my life easier in this kind of search ? I will always get my soons up to 9 levels down.
I know that with procedure I can solve this problem , however I would like to know an elegant and simple solution.
Thank you
I solve this using a with a recursive query:
with with_user_earns as (
-- get father information (start)
select father.id, father.str_name, father.id_father, father.ind_father_side_type, 1 as int_user_level from tb_user father where id = 9
union all
-- get all soons (stop condition)
select son.id, son.str_name, son.id_father, son.ind_father_side_type, WUE.int_user_level + 1 from tb_user as son inner join with_user_earns as WUE on son.id_father = WUE.id where son.id_father is not null /*and WUE.int_user_level < 9*/
)
-- show result
select with_user_earns.id, with_user_earns.str_name, with_user_earns.id_father, with_user_earns.ind_father_side_type, with_user_earns.int_user_level from with_user_earns order by with_user_earns.int_user_level, with_user_earns.id

subquery returning more than one value

SELECT CG.SITEID,
CR.COLLECTIONID,
CG.COLLECTIONNAME,
CASE
WHEN CR.ARCHITECTUREKEY = 5
THEN
N'vSMS_R_System'
WHEN CR.ARCHITECTUREKEY = 0
THEN
(SELECT BASETABLENAME
FROM DISCOVERYARCHITECTURES
JOIN
COLLECTION_RULES
ON DISCOVERYARCHITECTURES.DISCARCHKEY =
COLLECTION_RULES.ARCHITECTUREKEY
JOIN
COLLECTIONS_G
ON COLLECTION_RULES.COLLECTIONID =
COLLECTIONS_G.COLLECTIONID
WHERE COLLECTIONS_G.SITEID = (SELECT TOP 1 SOURCECOLLECTIONID FROM VCOLLECTIONDEPENDENCYCHAIN WHERE DEPENDENTCOLLECTIONID = CG.SITEID ORDER BY LEVEL DESC))
ELSE (SELECT DA.BASETABLENAME FROM DISCOVERYARCHITECTURES DA WHERE DA.DISCARCHKEY=CR.ARCHITECTUREKEY) END AS TABLENAME
FROM COLLECTIONS_G CG
JOIN COLLECTIONS_L CL ON CG.COLLECTIONID=CL.COLLECTIONID
JOIN COLLECTION_RULES CR ON CG.COLLECTIONID=CR.COLLECTIONID
WHERE (CG.FLAGS&4)=4 AND CL.CURRENTSTATUS!=5
I am having a problem with the code above, around the line:
when cr.ArchitectureKey=0 then...
The problem is that the sub-query returns more than one value, and I'm not too sure how to invert the query so that I get rid of the error.
To make matters worse, cr.ArchitectureKey would normally join with da.DiscArchKey, but while cr.ArchitectureKey can have a value of 0, that does not exist in da.DiscArchKey, meaning if I join the two directly I lose data.
EDIT
More information regarding the problem itself:
This is a stored procedure for a Microsoft product that has a 'bug' (probably considered a feature though) which I'm trying to fix. Don't worry, this is only in my own little test server.
Anyway, there's the concept of a Collection. All Collections must have a parent (determined through VCOLLECTIONDEPENDENCYCHAIN), with the exception of the very top level Collection that is a system collection and cannot be modified.
Each collection can have 0 or more rules, and each rule has a rule type, where the ID of the rule type is saved onto COLLECTION_RULES and the matching string for that ID is saved onto DISCOVERYARCHITECTURES.
In most cases, a rule is a WQL query, and the rule type is determined by what tables are queried on the WQL query.
However, and this is where the problem lies, collections can also have a query of type 'include' or 'exclude', which basically forces it to borrow the query of another Collection. So effectively you include the results of another Collection's query onto your own Collection, and that's the query.
As far as COLLECTION_RULES is concerned, when that happens, the ID of the rule type is 0, which is a value that doesn't exist in DISCOVERYARCHITECTURES.
What I was trying to modify was so that when the rule type is 0, get and use the rule type(s) of the highest up parent (not the direct parent since the parent Collection could also have a single include rule, in which case the rule type would still be 0).
The problem is that because each rule can have multiple rule types, it returns multiple rows in some instances.
I tried to invert the query to remove the SELECT and use joins only, but failed because I found I always needed to join it to DISCOVERYARCHITECTURES and I have nothing to join it on when the rule type = 0.
EDIT2
Sample data:
Collections_G
Collections_L
Collection_Rules
DiscoveryArchitectures
vCollectionDependencyChain
Original Query and Original Results
SELECT cg.SiteID,
CASE
WHEN da.DiscArchKey=5
THEN N'vSMS_R_System'
ELSE da.BaseTableName END AS TableName
FROM Collections_G cg
JOIN Collections_L cl ON cg.CollectionID=cl.CollectionID
JOIN Collection_Rules cr ON cg.CollectionID=cr.CollectionID
JOIN DiscoveryArchitectures da ON cr.ArchitectureKey=da.DiscArchKey
WHERE (cg.Flags&4)=4 AND cl.CurrentStatus!=5
As you can see from the results picture above, some collections appear multiple times but with different TableNames. This is because each collection have have several rules, and each rule has one cr.ArchitectureKey
Also, and more importantly, collections PS10000B and PS10000C do not show up because their cr.ArchitectureKey = 0 which is a value that doesn't exist in da.DiscArchKey.
My goal is to have collections that have a cr.ArchitectureKey appear, but I need to assign them a cr.ArchitectureKey
My thought (which is slightly flawed, but don't know enough SQL to make it better, so if someone could help with that it would be appreciated too) was to get use the da.DiscArchKey from the top level parent. But the top level parent can have multiple DiscArchKeys, which is what is causing the problem.
As mentioned above getting the top level parent is slightly flawed, and ideally I would get the top level cr.ReferencedCollectionID. In other words, if PS10000B has a cr.ReferencedCollectionID of PS10000C and PS10000C has a cr.ReferencedCollectionID of SMS00002 but because SMS00002 has no cr.ReferencedCollectionID then SMS00002 is the top level cr.ReferencedCollectionID and both PS10000B and PS10000C should have da.DiscArchKey(s) equal to those of SMS00002.
Please have a look at a wired solution that comes into mind. You may face some syntax errors(most probably in 2nd and 3rd CTE) but it just an idea.
Get each case values in separate CTEs and then combine them at the end.
;WITH CTE
AS
(
SELECT CG.SITEID,
CR.COLLECTIONID,
CG.COLLECTIONNAME
FROM COLLECTIONS_G CG
JOIN COLLECTIONS_L CL ON CG.COLLECTIONID=CL.COLLECTIONID
JOIN COLLECTION_RULES CR ON CG.COLLECTIONID=CR.COLLECTIONID
WHERE (CG.FLAGS&4)=4 AND CL.CURRENTSTATUS!=5
),
ARCHITECTUREKEY5
AS
(
SELECT C.SITEID,
C.COLLECTIONID,
C.COLLECTIONNAME,
N'vSMS_R_System' as TABLENAME
FROM CTE C WHERE C.ARCHITECTUREKEY = 5
),
ARCHITECTUREKEY0
AS
(
SELECT C.SITEID,
C.COLLECTIONID,
C.COLLECTIONNAME,
BASETABLENAME as TABLENAME
FROM CTE C,
DISCOVERYARCHITECTURES
JOIN
COLLECTION_RULES
ON DISCOVERYARCHITECTURES.DISCARCHKEY =
COLLECTION_RULES.ARCHITECTUREKEY
JOIN
COLLECTIONS_G
ON COLLECTION_RULES.COLLECTIONID =
COLLECTIONS_G.COLLECTIONID
WHERE COLLECTIONS_G.SITEID = (SELECT TOP 1 SOURCECOLLECTIONID FROM VCOLLECTIONDEPENDENCYCHAIN WHERE DEPENDENTCOLLECTIONID = C.SITEID ORDER BY LEVEL DESC))
and C.ARCHITECTUREKEY = 0
),
ARCHITECTUREKEYOTHER
AS
(
SELECT C.SITEID,
C.COLLECTIONID,
C.COLLECTIONNAME,
DA.BASETABLENAME as TABLENAME
FROM DISCOVERYARCHITECTURES DA, CTE C WHERE DA.DISCARCHKEY=CR.ARCHITECTUREKEY AND C.ARCHITECTUREKEY not in (0,1)
)
Select * from ARCHITECTUREKEY5
UNION
Select * from ARCHITECTUREKEY0
UNION
Select * from ARCHITECTUREKEYOTHER

Recursive cte sql with for hierarchy level

I have a little problem with this recursive CTE, it works fine except when I have a user without root readable rights means no entry for this element. So if I run this query on a user with rights just on the leaves inside the tree the level part of this query won't work correctly.
It will show the real level hierarchy for example 6 but its the top first readable element for him so it should be 1.
WITH Tree
AS (
SELECT
id,
parent,
0 AS Level,
id AS Root,
CAST(id AS VARCHAR(MAX)) AS Sort,
user_id
FROM SourceTable
WHERE parent IS NULL
UNION ALL
SELECT
st.id,
st.parent,
Level + 1 AS Level,
st.parent AS Root,
uh.sort + '/' + CAST(st.id AS VARCHAR(20)) AS Sort,
st.user_id
FROM SourceTable AS st
JOIN Tree uh ON uh.id = st.parent
)
SELECT * FROM Tree AS t
JOIN UserTable AS ut ON ut.id = t.user_id AND ut.user_id = '141F-4BC6-8934'
ORDER BY Sort
the level is as follows
id level
5 0
2 1
7 2
4 2
1 2
6 1
3 2
8 2
9 3
When a user now just have read rights to id 8 and 9 the level from CTE stays at 2 for id 8 and 3 for id 9 but I need for id 8 level 1 if there is no one before
You haven't told us how you know whether a user has rights to a given id. That is a necessary piece of information. I'm going to put some code below that assumes you add a column to your query called hasRights and that this column will have a zero value if the user does not have rights and a value of one if they do. You may need to tweak this, since I have no data to test with but hopefully it will get you close.
Basically, the query is altered to only add 1 to the level if the user has rights. It also only adds to the sort path if the user has rights, otherwise an empty string is appended. So, if ids 8 and 9 are the only items the user has access to, you should see levels of 1 and 2 and sort paths similar to '5/8/9' rather than '5/6/8/9'. If you still aren't able to get it working, it would help us tremendously if you posted a sample schema on SqlFiddle.
WITH Tree
AS (
SELECT
id,
parent,
0 AS Level,
id AS Root,
hasRights AS HasRights,
CAST(id AS VARCHAR(MAX)) AS Sort,
user_id
FROM SourceTable
WHERE parent IS NULL
UNION ALL
SELECT
st.id,
st.parent,
Level + st.hasRights AS Level,
st.parent AS Root,
st.hasRights AS HasRights,
uh.sort + CASE st.hasRights WHEN 0 THEN '' ELSE '/' + CAST(st.id AS VARCHAR(20)) END AS Sort,
st.user_id
FROM SourceTable AS st
JOIN Tree uh ON uh.id = st.parent
)
SELECT * FROM Tree AS t
JOIN UserTable AS ut ON ut.id = t.user_id AND ut.user_id = '141F-4BC6-8934'
ORDER BY Sort
You requite something like if the higher level(0 or 1 ) is not in existence, then the next level become the higher level..
If yes, then you have to do this when the final result
insert all the results in temp table lets say #info (with same characteristics of data)
Now after all final data ready in the table,
Please check from the top.
Select * from #info where level= 0
if this returns 0 rows then you have to update each records level. to (level = level -1)
Now again same for Level=0, then level 1, then level 2 , then level 3 in recursion. this will be easy but not easy to code. So try without recursion then try final update.
I hope this will help :)
Please reply if you are looking for something else.
Try to perform the following select and let me know if it is your desired result:
SELECT *,
DENSE_RANK() OVER (PARTITION BY t.user_id ORDER BY t.LEVEL ASC) -1 as RelativeUserLevel
FROM Tree AS t
JOIN UserTable AS ut ON ut.id = t.user_id AND ut.user_id = '141F-4BC6-8934'
ORDER BY Sort
Is conversion of your table into hierarchical types an option:
hierarchyid data type (http://technet.microsoft.com/en-us/library/bb677290(v=sql.105).aspx)
or
xml data type
SOME TIMES option (maxrecursion 10000); is very useful
I don't have time to read your problem but here snipped
declare cursorSplit Cursor for
select String from dbo.SplitN(#OpenText,'~')
where String not in (SELECT [tagCloudStopWordText]
FROM [tagCloudStopList] where [langID]=#_langID)
option (maxrecursion 10000);
open cursorSplit
I'm sorry to be a party pooper and spoil the fun of creating such an interesting piece of SQL, but perhaps you should load all relevant access data into your application and determine the user levels in the application?
I bet it would result in more maintainable code..

Microsoft SQL Parent Problem

I am rather stuck, I've had a good look around but I am not exactly sure how I can do this.
I've got to build a SP (TSQL) to bring back a navigation, but I am having a few issues with ordering the navigation correctly.
Table Example
NavID OrderID ParentID NavName
1 1 0 Home
2 2 0 About
3 3 0 Contact Us
4 1 2 About Us Page
5 2 2 About Us Page 2
6 1 4 Another SubPage
All I need to bring back is the navigation above, and one navigation below.
So if I passed NavigationID 2 I would expect the results to come back like this
Home
About
About Us Page
About Us Page 2
Contact Us
If I passed in NavigationID 6 I would expect to see ..
Home
About
About Us Page
Another SubPage
About Us Page 2
Contact Us
As you can see it takes in account the OrderID, but make's sure the Child's are in order first.
How can I achieve this?
Here's a complete script which does what you need (includes your test data):
DECLARE #nav TABLE (
NavID INT NOT NULL PRIMARY KEY,
OrderID INT NOT NULL,
ParentID INT,
NavName nvarchar(MAX) NOT NULL
);
INSERT #nav
SELECT 1, 1, 0, 'Home' UNION ALL
SELECT 2, 2, 0, 'About' UNION ALL
SELECT 3, 3, 0, 'Contact Us' UNION ALL
SELECT 4, 1, 2, 'About Us Page' UNION ALL
SELECT 5, 2, 2, 'About Us Page 2' UNION ALL
SELECT 6, 1, 4, 'Another SubPage';
DECLARE #NavigationID int;
SET #NavigationID = 2;
WITH Ancestors AS (
SELECT #NavigationID NavID
UNION ALL
SELECT n.ParentID
FROM #nav n
JOIN Ancestors a ON (n.NavID = a.NavID)
),
VisibleNav AS (
SELECT n.*, CONVERT(FLOAT, 1)/SUM(1) OVER (PARTITION BY n.ParentID) Mul, ROW_NUMBER() OVER (PARTITION BY n.ParentID ORDER BY n.OrderID)-1 Pos
FROM #nav n
JOIN Ancestors a ON n.ParentID = a.NavID
),
SortedNav AS (
SELECT vn.*, vn.Pos*vn.Mul Sort, 1 Depth
FROM VisibleNav vn
WHERE vn.ParentID = 0
UNION ALL
SELECT vn.NavID, vn.OrderID, vn.ParentID, vn.NavName, vn.Mul*sn.Mul, vn.Pos, vn.Pos*(vn.Mul*sn.Mul)+sn.Sort, sn.Depth + 1
FROM VisibleNav vn
JOIN SortedNav sn ON sn.NavID = vn.ParentID
)
SELECT sn.NavID, sn.OrderID, sn.ParentID, sn.NavName
FROM SortedNav sn
ORDER BY sn.Sort, sn.Depth;
Basically, I have a recursive CTE to create a list of all parents which need to be used in your navigation including the depth of the parent (so that the order is not dependent on the IDs), and then I join the navigation entries on that.
I'd be tempted to create a table variable, run the query for NavID <= the parent parameter where parentID = 0 and add to the table. Then get the child items, insert to table variable and finally get the > NavID Where ParentID = 0.
A fudge but it should work.
DECLARE #Table TABLE (NavName nvarchar(50), OrderID int)
INSERT #Table (NavName, OrderID) (SELECT NavName, OrderID FROM #Table1 WHERE (ParentID = 0) AND (NavID <= #ParentID))
INSERT #Table (NavName,OrderID) (SELECT NavName, OrderID+ 500 FROM #Table1 WHERE ParentID = #ParentID )
INSERT #Table (NavName,OrderID) (SELECT NavName, OrderID + 1000 FROM #Table1 WHERE ParentID = 0 AND NavID > #ParentID)
SELECT * FROM #Table ORDER BY OrderID
Common Table Expressions permit recursive queries. However, they can be very slow and can seriously confuse the query optimizer, particularly if you use (and you generally should) parametrized queries. For small tables (such as navigation) that will not participate in large joins with other (large) tables, CTE's work fine. You can also cache the result of the common table expression in a table and rerun the caching query anytime the navigation table changes (generally not frequently, I'm guessing) - the query optimizer deals with simple many-to-many relation tables much better.
However, there are also other ways of representing tree's in SQL server.
You could look at
lft/rgt style columns whereby a ordering is defined in as the order of the lft column and a node X is a descendant of Y whenever Y.lft < X.lft < Y.rgt. See: http://articles.sitepoint.com/article/hierarchical-data-database/2
HierarchyID's. Sql Server 2008 introduced a special data type for exactly this purpose; however updating the tree structure isn't always trivial with these. See: http://msdn.microsoft.com/en-us/magazine/cc794278.aspx
Finally, when I need to use CTE's I generally start with something along the lines of...
with nav_tree (ParentID, ChildID, depth_delta) as (
SELECT basenode.NavID, basenode.NavID, 0
FROM NavTable AS basenode
UNION ALL
SELECT treenode.ParentID, basenode.NavID, depth_delta+1
FROM NavTable AS basenode
JOIN nav_tree AS treenode ON basenode.ParentID=treenode.ChildID
)
--select statement here joining the nav_tree with the original table and whatnot
Note that your exact order requirements are quite tricky; in particular where you want the children of an element to immediately be listed inline; i.e. the "About Us Page, Another SubPage, About Us Page 2" segment of your second example. In particular, that means you cannot just order by depth_delta and secondarily by orderid - you'll need a path-based sort. You might want to do this in code, rather than in sql, but you can construct a path in a CTE as follows:
with nav_tree (ParentID, ChildID, depth_delta,orderpath) as (
SELECT basenode.NavID, basenode.NavID, 0, convert(varchar(MAX), basenode.OrderID)
FROM NavTable AS basenode
UNION ALL
SELECT treenode.ParentID, basenode.NavID, depth_delta+1, treenode.orderpath+','+basenode.OrderID
FROM NavTable AS basenode
JOIN nav_tree AS treenode ON basenode.ParentID=treenode.ChildID
)
...and then you can order by that orderpath. Since you also want "one level below" each ancestor-or-self unfolded, you'll still need to join with NavTable to get those.
However, given your sorting requirements, I'd recommend HierarchyIDs: they have your sorting semantics built-in, and they also avoid the potential performance issues CTE's can expose.
I am a little unclear as to precisely which nodes you wish to display... but if I understand correctly the main question here is how to order the resulting nodes. The principal difficulty is that the ordering criteria are variable length: a node must be ordered based upon the entire sequence of OrderId values for the node and all of its ancestors. For example, the ordering sequence for node 6 is '2, 1, 1'.
SQL does not handle such variable-length sequences well. I propose that we use an NVARCHAR(MAX) value for the ordering sequence. For node 6, we will use '0002.0001.0001'. In this form, the nodes can be ordered trivially using string comparison. Note that the identifier values must be zero-padded in order to ensure correct ordering (I arbitrarily chose to pad to 4 digits -- the real application may require a different choice).
So this brings us to the nuts and bolts. We'll start by creating a table called NavigationData to hold our test data:
SELECT NULL AS NavId, NULL AS OrderId, NULL AS ParentId, NULL AS NavName
INTO NavigationData WHERE 1=0
UNION SELECT 1, 1, 0, 'Home'
UNION SELECT 2, 2, 0, 'About'
UNION SELECT 3, 3, 0, 'Contact Us'
UNION SELECT 4, 1, 2, 'About Us Page'
UNION SELECT 5, 2, 2, 'About Us Page 2'
UNION SELECT 6, 1, 4, 'Another SubPage'
Now, we'll create a helper view that, for every possible desired node, lists all of the related nodes along with their computed path strings. As I said at the beginning, I feel the criteria for selecting the related nodes are underspecified, so the desired/related JOIN expression may need to be adjusted for datasets with more nodes than the simple example. With that caveat, here is the view:
CREATE VIEW NavigationHierarchy AS
WITH
hierarchy AS (
SELECT
NavId AS RootId
, 1 AS Depth
, NavId
, RIGHT('0000' + CAST(OrderId AS NVARCHAR(MAX)), 4) AS Path
, ParentId
, NavName
FROM NavigationData
WHERE ParentId = 0
UNION ALL
SELECT
parent.RootId
, parent.Depth + 1 AS Depth
, child.NavId
, parent.Path + '.'
+ RIGHT('0000' + CAST(child.OrderId AS NVARCHAR(MAX)), 4) AS Path
, child.ParentId
, child.NavName
FROM hierarchy AS parent
INNER JOIN NavigationData AS child
ON child.ParentId = parent.NavId
)
SELECT
desired.NavId AS DesiredNavId
, related.*
FROM hierarchy AS desired
INNER JOIN hierarchy AS related
ON related.Depth <= desired.Depth + 1
AND related.RootId IN (desired.RootId, related.RootId)
Most of the query is a straight-forward recursive descent of the hierarchy using a common table expression. The heart of the solution is the generation of the Path columns. Naturally, you may prefer to bake this query directly into a larger query or stored proc rather than creating a view. The view is convenient for testing, however.
Armed with the view, we can now generate the desired results, in the requested order. I have included the generated path in the query result for illustrative purposes. Here is the query for node 2:
SELECT NavName, Path
FROM NavigationHierarchy
WHERE DesiredNavId = 2
ORDER BY Path
yielding:
Home 0001
About 0002
About Us Page 0002.0001
About Us Page 2 0002.0002
Contact Us 0003
and for node 6:
SELECT NavName, Path
FROM NavigationHierarchy
WHERE DesiredNavId = 6
ORDER BY Path
yielding:
Home 0001
About 0002
About Us Page 0002.0001
Another SubPage 0002.0001.0001
About Us Page 2 0002.0002
Contact Us 0003