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
Related
It feels like there is a very simple solution but I am new to SQL and have been struggling with this for a while.
I am working on joining two tables so that I can map the cost center in one to its associated product group in another. There is a parent child relationship here with product group as parent and cost center as child.
I am ultimately trying to see the spend data, currently only available by cost center in table 1, by the parent product group categorization. Prior to the join, I see the correct dollar value by cost center. After the join, the number dramatically increases and is incorrect.
Table 1 (purchase orders): cost_center_id, amount_ordered
Table 2 (employee plus): cost center_id, product_group_name
Below is a simplified sample of the query I am working with.
SELECT
po.po_cost_center_id,
ep.product_group_name,
SUM(po.amount_ordered)
FROM purchase_orders po
LEFT JOIN d_employee_plus ep on po.cost_center_id = ep.cost_center_id and ep.ds =
po.ds
WHERE
po.ds = (select max(ds) from purchase_orders)
GROUP BY 1,2
My take here is that you have duplicate rows because you're not aggregating on both the columns you're joining on. I may be wrong, though try using the following:
SELECT po.po_cost_center_id,
ep.ds,
ep.product_group_name,
SUM(po.amount_ordered)
FROM purchase_orders po
LEFT JOIN d_employee_plus ep
ON po.cost_center_id = ep.cost_center_id
AND ep.ds = po.ds
WHERE po.ds = (SELECT max(ds) FROM purchase_orders)
GROUP BY po.po_cost_center_id,
ep.ds,
ep.product_group_name
For more troubleshooting, if you can provide some sample data, I may help you further.
Does it work for you?
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.
Disclaimer: I'm helping a person learn SQL by using an online tutorial. So you can consider this as a homework question.
There are 3 SQL Server tables that we are dealing with here:
Ships (name, class)
Classes - different classes of the ship (class)
Outcomes - what has happened to that ship (ship, result)
Now there are several crazy things in the layout of the database, the biggest one being that the ships in the Outcomes table may not be present in the Ships table when they are named the same way as the class in Class.
The point is to get the number of sunk ships of each class. I have helped the student to get to the following SQL:
Select
dbo.Classes.[class], Count(dbo.Outcomes.ship) as [count]
from
dbo.Classes
left join
dbo.Ships on dbo.Ships.[class] = dbo.Classes.[class]
left join
dbo.Outcomes on (dbo.Outcomes.ship = dbo.Classes.[class] or
dbo.Outcomes.ship = dbo.Ships.name)
and dbo.Outcomes.result = 'sunk'
Group by
dbo.Classes.[class]
However, it, apparently, is the incorrect solution, as it can on some occasions return incorrect results. On the web i have managed to find the following solution to this tutorial:
select
classes.class, count(T.ship)
from
classes
left join
(select
ship, class
from
outcomes
left join
ships on ship = name
where
result = 'sunk'
union
select
ship, class
from
outcomes
left join
classes on ship = class
where
result = 'sunk') as T on classes.class = T.class
group by
classes.class
But I cannot understand under which conditions will the results be different. Doesn't the Union operation of using two different join paths serve the exactly same function as does OR in the condition of the join?
P.S. This particular question in the tutorial is actually marked as 2 on the scale from 1-5 for difficulty. So i feel myself quite stupid.
With this dataset:
with Ships as (
select * from (values
('HMS Prince of Wales','King George V')
,('King George V','King George V')
)Ships(name,class)
),
Classes as (
select * from (values
('King George V')
)Classes(class)
),
Outcomes as (
select * from (values
('HMS Prince of Wales','sunk')
,('King George V','sunk')
)Outcomes(ship,result)
)
the two queries you provide respectively yield:
class count
------------- -----------
King George V 3
class
------------- -----------
King George V 2
The reason for the difference is that UNION is a set operator that eliminates duplicates (unlike UNION ALL) while the OR operator doesn't. We can test this by replacing UNION in the second query with UNION ALL, which now yields:
class
------------- -----------
King George V 3
just as in your first proposed solution.
The queries might return different results in many cases. One obvious case is when a sunk ship matches both class and name in outcomes. The union in the second query will return one row in this case. The join in the first query will return two rows. Hence the counts will be different.
I think you could fix this particular issue by using count(distinct) in the first query.
Needed goal :
"The point is to get the number of sunk ships of each class. I have helped the student to get to the following SQL:"
Sudo code should be created in three steps. You would need to break them into working steps
select * from class
//create a list of all of the classes that exists in your db
select * from [working previous statement] where results = "sunk"
//create another select statement that uses the previous select statement to refine results
//then refine it to have ships that have been sunk
select count from [[previous statement with the two selects]]
//This create the count of all of the sunk ships.
select count from [[previous statement with the two selects]] group by [previous statement with one select]
//this one should create the individual count of all of the sunken ships based on it's class
select sum from [[your pick of which statement]]
//do a sum
naturally you will need to do the linking so the code should be implemented as
select ship from ships sh, classes cl, outcome oc where sh.class = cl.class and oc.ship = sh.ship and oc.result = sunk group by cl.classes
then add the count steps in the mix.
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..
Our Bill of Materials table has 3 main columns: Part Number, Parent Part, and Level Code (0 being no parent, 3 being a child of a child of a child, etc).
I'd am trying to query based on a single part and get all of its children and their children, so on.
Its seems so simple but I have been struggling with this one. Anyone have a solution they can offer?
As always, I appreciate the help.
Per Randy's request, here is some more info:
PARPRT = Parent
COMPRT = Component
In the screen shot, all the component parts are part of the Bill of Materials for '101002'.
Later down the table, you will see each of those components in COMPRT listed in the PARPRT column with the components that make each of them up.
I want to query the BOM for '101002' and not only get the 4 parts in COMPRT where PARPRT = '101002', but also their COMPRTs and so on.
Thank you for those who suggested CTE's. That was exactly what I was looking for.
Here is the query I ended up with after some trial and error.
USE PartDatabase
GO
DECLARE #TheSinglePart CHAR(30) = '100001';
WITH BOM (PARPRT_02, COMPRT_02, QTYPER_02)
AS
(
-- Anchor member definition
SELECT e.PARPRT_02, e.COMPRT_02, e.QTYPER_02
FROM dbo.Product_Structure AS e
WHERE e.PARPRT_02 = #TheSinglePart
UNION ALL
-- Recursive member definition
SELECT e.PARPRT_02, e.COMPRT_02, e.QTYPER_02
FROM dbo.Product_Structure AS e
INNER JOIN BOM AS d
ON e.PARPRT_02 = d.COMPRT_02
)
SELECT *
FROM BOM;