SQL Query to display heirarchical data - sql

I have two tables - 'Users' and 'Supervision'
For this example, my users table is very simple:-
Users
=====
ID (PK)
UserName
Some users manage other users, so I've built a second table 'Supervision' to manage this:-
Supervision
===========
UserID
SuperID - this is the ID of the staff member that the user supervises.
This table is used to join the Users table to itself to identify a particular users supervisor. It might be that a user has more than one supervisor, so this table works perfectly to this end.
Here's my sample data in 'Users':-
userID userName
1 Bob
2 Margaret
3 Amy
4 Emma
5 Carol
6 Albert
7 Robert
8 Richard
9 Harry
10 Arthur
And my data in 'Supervision':-
userID superID
1 2
1 3
2 4
2 5
3 4
3 5
6 1
6 7
7 8
7 9
9 10
If I want to see who directly reports to Bob, writing an SQL query is straightforward, and tells me that Margaret and Amy are his direct reports.
What I want to do however is to write a query that shows everybody who comes under Bob, so it would need to look at Bobs direct reports, and then their direct reports, and so on - it would give Margaret, Amy, Emma and Carol as the result in this case.
I'm assuming this requires some kind of recursion but I'm completely stuck..

You should use recursive CTE:
WITH RCTE AS
(
SELECT * FROM dbo.Supervision WHERE UserID = 1
UNION ALL
SELECT s.* FROM dbo.Supervision s
INNER JOIN RCTE r ON s.userID = r.superID
)
SELECT DISTINCT u.userID, u.userName
FROM RCTE r
LEFT JOIN dbo.Users u ON r.superID = u.userID
SQLFiddle DEMO

Sounds to me like you need a Recursive CTE. This article serves as a primer, and includes a fairly similar example to the one you have:
http://blog.sqlauthority.com/2012/04/24/sql-server-introduction-to-hierarchical-query-using-a-recursive-cte-a-primer/
Hope it helps.

WITH MyCTE
AS (
-- ID's and Names
SELECT SuperID, ID
FROM Users
join dbo.Supervision
on ID = dbo.Supervision.UserID
WHERE UserID = 1
UNION ALL
--Who Manages who...
SELECT s.SuperID, ID
FROM Supervision s
INNER JOIN MyCTE ON s.UserID = MyCTE.SuperID
WHERE s.UserID IS NOT NULL
)
SELECT distinct MyCTE.ID, NAMES.UserName, '<------Reports to ' as Hierarchy, res_name.UserName
FROM MyCTE
join dbo.Users NAMES on
MyCTE.ID = NAMES.ID
join dbo.Users res_name
on res_name.ID = MyCTE.SuperID
order by MyCTE.ID, NAMES.UserName, res_name.UserName

Related

SQL join query, want to get the latest record of joining table (or null if it doesn't match)

I have 2 tables
User:
ID
NAME
1
John
2
Jane
3
Jim
login:
id
date
1
2021-01-29
3
2021-02-27
1
2021-03-11
3
2021-04-18
I want to get the result like:
name
date
John
2021-03-11
Jane
null
Jim
2021-04-18
How shall I write the SQL query?
I tried quite a few join but never got the 2nd record (Jane/Null) out from the query. Need some help here, thanks a ton in advance!
You can use left join and group by:
select u.id, u.name, max(l.date)
from user u left join
login l
on l.id = u.id
group by u.id, u.name;
Note: This includes the id as well. If name is known to be unique that is not necessary.
complimenting #gordons answer another option is to use a subquery like follows
SELECT user.name, login.last_login
FROM user
LEFT JOIN (SELECT id, max(date) AS last_login FROM logins group by id) AS login
ON login.id = user.id

SQL join one table against multiple columns

I am having 2 tables in my DB
1) Report table
AppNo AppName AddedBy AssignedTo ModifiedBy
-------------------------------------------
1 App1 1 2 1
2 App2 1 2 2
3 App3 2 2 2
4 App4 1 2 3
2) Users table
UserId UserName Role
----------------------
1 Raj Manager
2 Sid Lead
3 KK Rep
So i want to join two tables so that i can get names in place of Id's
Result Needed:
AppNo AppName AddedBy AssignedTo ModifiedBy
-------------------------------------------
1 App1 Raj Sid Raj
2 App2 Raj Sid Sid
3 App3 Sid Sid Sid
4 App4 Raj Sid KK
My Query:
SELECT
R.AppNo, R.AppName,
u1.UserName as AddedBy,
u2.UserName as AssignedTo,
u3.UserName as ModifiedBy
FROM Report R
LEFT OUTER JOIN Users u1 on u1.UserID = R.AddedBy
LEFT OUTER JOIN Users u2 on u2.UserID = R.AssignedTo
LEFT OUTER JOIN Users u3 on u3.UserID = R.ModifiedBy
But i dont want to join multiple time with User table..
As in my original report table there are nearly 8 UserId columns are there so i cant join 8 times it reduces performance.
Can anyone suggest the best way.
Note: I cant change table schema
Thanks in advance
Joining multiple times is the only way; that is what the data and structure is.
There should be no performance issue, as the database is typically well designed to handle this. Just because a JOIN looks complex to humans doesn't mean it is complex or expensive for the optimizer or the database.
You could use a cartesian join for the columns, although performance will depend heavily on how well indexed the tables are...
select R.AppNo,
R.AppName,
(select UserName from Users where UserID = R.AddedBy) as AddedBy,
(select UserName from Users where UserID = R.AssignedTo) as AssignedTo,
(select UserName from Users where UserID = R.ModifiedBy) as ModifiedBy
from Report R

Given a specific user I want to find all users having at least all of the roles this specific user has

Suppose I have user_role table having user and role columns.
| User | Role |
---------------
| 100 | 10 |
| 101 | 10 |
| 101 | 11 |
| 102 | 11 |
I want to write a query that will return users with same or lesser roles. For example:
Return user 100 for user 100
Return user 100,101,102 for user 101
Return user 102 for user 102
Business requirement: Suppose User X belongs to Asia group only. So X should have access permission to users who belongs to Asia group only. But say Y belongs to Asia and Europe groups. So Y should have access permission to users who belongs to:
Asia group only
Europe group only
Asia and Europe group only
Now, X should not access the data of Y as X does not belong to all the groups that Y belongs to. Similarly, say Z belongs to Asia, Europe and America. So, Z should access all the data of X, Y and Z but the reverse is not true.
My initial SQL:
select distinct(user) from user_role where role in
(select role from user_role where user=?);
Above query returns all the users sharing at least one common groups and not all common groups.
Can anybody please help me with a SQL example?
This can be done with much more less effort. The idea is left join roles on roles of particular user and then filter only those users for which all roles are found in that particular user's roles:
;with c as(select roleid from userroles where userid=100)
select r.userid from userroles r left join c on r.roleid = c.roleid
group by r.userid
having sum(case when c.roleid is null then 1 else 0 end) = 0
Fiddle http://sqlfiddle.com/#!6/bca579/7
Try this:
-- Create a CTE that will help us know the number of roles any user have.
;WITH CTE (UserId, RoleId, NumberOfRoles)
AS (
SELECT T1.UserId, RoleId, NumberOfRoles
FROM UsersToRoles T1 INNER JOIN
(
-- Derived table is needed so that we can have
-- the user, he's roleId and he's number of roles in the CTE
SELECT UserId, COUNT(RoleId) As NumberOfRoles
FROM UsersToRoles
GROUP BY UserId
) T2 ON(T1.UserId = T2.UserId)
)
-- We join the CTE with itself on the RoleId to get only users that have the same roles,
-- and on the NumberOfRoles to ensure that the users we get back have at least the nomber of roles as the user we testing.
SELECT DISTINCT T1.UserId
FROM CTE T1
INNER JOIN CTE T2 ON(T1.RoleId = T2.RoleId AND T1.NumberOfRoles <= T2.NumberOfRoles)
WHERE T2.UserId = #UserId
Play with it yourself in this sql fiddle
CTE, or Common Table Expressions is a concept introduced in Sql Server 2008. basically, you define a select statement that the rest of your sql can refer to as if it was a view.
In this case, you could have this CTE written as a view and it would give you the same result.

What Join should I use in SQL

I have four tables in my Database:
Person that contains ID(PK) and Name.
Person_Skill that contains ID(PK), PID(FK), Skill(FK) and SkillLevel(FK).
Skill that contains ID(PK) and SkillLabel.
SkillLevel that contains ID(PK) and Name.
Every skill has level from 0 to 7
Now I want to display all the skill that the person has(Include the skilllevel = 0)
Select
[dbo].Person.Name as Name,
[Skill].SkillLabel as SkillName,
[Person_Skill].[SkillLevel] as SkillLevel
From
([dbo].Person inner join [dbo].[Person_Skill] ON [dbo].[Person_Skill].PID= Person.ID)
inner join [dbo].[Skill] ON [dbo].[Person_Skill].Skill=Skill.ID
The above code only display the skill that person has from level 1 to level 7.
I believe the reason I only get the skill from level 1 to 7 is because the person table only contains the skill from level 1 to 7, but I'm not sure about this. I got the database from other. If my assumption is correct, is there anyway to do this? To get all the skills in the skill table and display the skill level that the person has(Include skillllevel =0).
Sample Data:
Person
ID Name
----------
1 Michael
2 Alex
Person_Skill
ID PID SkillID Skill_Level
5 1 10 5
6 2 11 1
7 1 12 7
8 1 13 5
Skill
ID Name
10 java
11 C++
12 HTML
13 ASP
14 C
15 .NET
16 C#
17 Objective
The expect results are;
Name SkillName SkillLevel
----------------------------
Alex java 0
Alex C++ 1
Alex HTML 0
Alex ASP 0
Alex C 0
Alex .NET 0
Alex C# 0
Alex Objective C 0
Michael java 5
Michael C++ 0
Michael HTML 7
Michael ASP 0
Michael C 0
Michael .NET 5
Michael C# 0
Michael Objective C0
The current query only output
Alex C++ 1
Michael java 5
Michael HTML 7
Michael .NET 5
Edit, if you want to return all skill names for each person, then you will want to use:
select d.p_name,
d.s_name SkillName,
coalesce(ps.Skill_Level, 0) Skill_Level
from
(
select p.id p_id, p.name p_name, s.id s_id, s.name s_name
from person p
cross join skill s
) d
left join person_skill ps
on d.p_id = ps.pid
and d.s_id = ps.skillid
left join skill s
on ps.skillid = s.id
See SQL Fiddle with Demo
If you want to include all Skills 0-7, then you will want to use a LEFT JOIN. You query will be similar to the following:
select p.Name as Name,
s.SkillLabel as SkillName,
ps.[SkillLevel] as SkillLevel
from [dbo].[Skill] s
left join [dbo].[Person_Skill] ps
on ps.Skill=s.ID
left join [dbo].Person p
on ps.PID = p.ID
Edit, without seeing any data it is difficult to determine. But if you want to retrieve all SkillLevels, then you will need to include that table. You might need to use:
select
p.Name as Name,
s.SkillLabel as SkillName,
sl.[Name] as SkillLevel
from SkillLevel sl
left join [dbo].[Person_Skill] ps
on ps.SkillLevel=sl.ID
left join [dbo].[Skill] s
on ps.Skill = s.Id
left join [dbo].Person p
on ps.PID = p.ID
You would want to use a LEFT JOIN which when tableA is inner joined on tableb would return all records from tableA regardless of whether or not there was a match from tableB
Therefore, if there are no persons with a skill of 0, you will still get back all of the person records.
An INNER JOIN will only return rows where there is a match on both sides. So in your code if a person does not have a skill with level 0 it would not be returned.
You could do a LEFT or RIGHT join and these get all the rows from the table on either the left or the right side. I think you probably want to use a left join, but without knowing more about your schema it's hard to say for sure. See the answer given to the question Left join and Left outer join in SQL Server for more detail on the differences in different join types

Too many sub-queries, that I am already confused and I am still missing one column - Oracle

I have the following query that has no errors:
SELECT u.user_name, u.user_lastn, outer_s.movie_id, outer_s.times_rented
FROM users u,
(
SELECT * FROM
(
SELECT user_id, movie_id, count (movie_id) as times_rented
FROM movie_queue
GROUP BY (user_id, movie_id)
ORDER BY user_id, movie_id
) inner_s
WHERE times_rented>1
) outer_s
WHERE u.user_id= outer_s.user_id;
This is what it returns:
USER_NAME USER_LASTN MOVIE_ID TIMES_RENTED
------------------------ ------------------------ ---------- ------------
John Smith 1 3
John Smith 6 2
Mary Berman 4 2
Mary Berman 6 4
Elizabeth Johnson 1 2
Peter Quigley 2 2
What I still need to do is to show the name of the movie, instead of the movie_id, but
the name of the movies are located in another table named movies that is similar to the
following sample:
MOVIE_ID MOVIE_NAME
---------- ---------------------------------------------
1 E.T. the Extra-Terrestrial
2 Jurassic Park
3 Indiana Jones and the Kingdom of the Crystal
4 War of the Worlds
5 Signs
Desired result:
What I want to see in the final table are the following columns:
USER_NAME | USER_LASTN | MOVIE_NAME | TIMES_RENTED |
Question:
But after all the many subqueries I am very confused, how can I get the movie_name there instead of the movie_id?
Attempted:
I tried getting the desired result by changing the query to
SELECT u.user_name, u.user_lastn, m.movie_name, outer_s.times_rented
FROM users u, movie m (etc.....)
But It returned 120 rows instead of the 6 I should get.
Help please!!
SELECT u.user_name, u.user_lastn, m.movie_name, COUNT(q.movie_id)
FROM users AS u
JOIN movie_queue AS q ON q.user_id = u.user_id
JOIN movie AS m ON m.movie_id = q.movie_id
GROUP BY u.user_name, u.user_lastn, m.movie_name
HAVING COUNT(q.movie_id) > 1
You just need to join the results of your query to the other query. However, first, I'm going to rewrite the query to simplify it an use proper join syntax:
SELECT u.user_name, u.user_lastn, m.movie_name, outer_s.movie_id, outer_s.times_rented
FROM users u join
(SELECT user_id, movie_id, count (movie_id) as times_rented
FROM movie_queue
GROUP BY (user_id, movie_id)
having count (movie_id) > 1
) outer_s
on u.user_id= outer_s.user_id join
movies m
on outer_s.movie_id = m.move_id
Or you could use CTEs to make your the query readable:
WITH outer_s as (SELECT user_id, movie_id, count (movie_id) as times_rented
FROM movie_queue
GROUP BY (user_id, movie_id)
having count (movie_id) > 1
)
SELECT u.user_name, u.user_lastn, m.movie_name, outer_s.movie_id, outer_s.times_rented FROM users u join outer_s
on u.user_id= outer_s.user_id join
movies m
on outer_s.movie_id = m.move_id
Using a CTE offers the advantages of improved readability and ease in maintenance of complex queries. The query can be divided into separate, simple, logical building blocks. These simple blocks can then be used to build more complex, interim CTEs until the final result set is generated.