"Column "parent_id" for "cte" is specified more than once" in SQL recursive query - sql

I have 5 SQL tables with columns as follows:
tbl_request_listEmpB
listEmpB_id request_id
tbl_request_listEmpD
listEmpD_id request_id
tbl_employee
id, parent_id (this one refers to id in tbl_department)
tbl_department
id, parent_id (that one referes to id of parent department)
tbl_department_manager
department_id, manager_employee_id
As input data I have employee_id and request_id.
I need to figure out whether the employee has access to the request (whether he's a manager or not)
Here's the query which is supposed to return 1 if the current user is a manager, 0 otherwise
with reqEmployees as (
select listEmpB_id as employee_id
from tbl_request_listEmpB
where request_id = ${request_id}
union all --concatenate the two tables
select listEmpD_id
from tbl_request_listEmpD
where request_id = ${request_id}
),
cte as (
select e.parent_id, null as parent_id
from reqEmployees r
join tbl_employee e on e.id = r.employee_id -- get these employees' departments
union all
select d.id, d.parent_id
from cte
join tbl_department d on d.id = cte.parent_id -- and get parent departments
)
select case when exists (select 1
from cte
join tbl_department_manager dm on dm.department_id = cte.id
where dm.manager_employee_id = ${employee_id})
then 1 else 0 end;
Finally, there's the logic that I believe is implemented in the query above:
First we need to identify whether the employee_id is a manager or not. If he is - find in which departments. So we query to tbl_department_manager based on manager_employee_id(=employee_id from input data) to get a list of corresponding department_id and store them in a variable. If the query returned 0 departments - terminate and return false
Based on request_id we collect ids of employees from both tbl_request_listEmpB and tbl_request_listEmpD. Later we refer to them as employee_id from reqEmployees
Query to tbl_employee based on ids retrieved from p.2 to get parent_id (list of unique departments employees belong to)
If there's a match between at least one department from p.1 and a one from p.3 return true
If not, there's a need to query to tbl_department and recursively search for a match between at least one element from p.1 and one element in p.3.parent_id
Here's what I mean
Consider the following chart
And here's the corresponding SQL table:
tbl_department (id, parent_id)
dep0 null
dep1 dep0
dep2 dep1
dep3 dep1
dep4 dep2
dep5 dep0
So, if we have a departments list returned from p.1 of ['dep1'] (there might be more than one element, we have to iterate through each element) we need to return true ONLY if from p.3 we've got dep1|dep2|dep3|dep4 - (dep1 descendants including dep1). If ['dep2'] return true if dep2|dep4. So there should at least one match of at least one element from p.1 and recursive result from p.5. I hope I illustrated it in the clearest way possible
Almost forgot - the query above gives me
"Column "parent_id" for "cte" is specified more than once"
But I don't think that it does what it's supposed to do, I need to rewrite it
Any help would be greatly appreciated

Without some sample data (and parameter values) and expected output for that data (with those parameter values), it's difficult to verify this solution.
I have assumed that your tbl_ou and tbl_department are in fact the same table.
Other than the CTE, it looks like the rest of your code should work. The CTE below now travels "both" directions through the hierarchy (upwards and downwards), finding both parents and children. Note that it only finds parents of parents and children of children, it doesn't find children of parents, for example, so no "siblings", "uncles" or whatever these records should be called!
You may need to cast both fields in the CTE seed record as the relevant data type. Based on the supplied data I have assumed that the datatype for department id (and therefore also for parent_id) is varchar(10).
cte as (
select
cast(e.parent_id as varchar(10)) as id,
cast(o.parent_id as varchar(10)) as parent_id,
0 as iteration
from
reqEmployees r
join tbl_employee e on e.id = r.employee_id
join tbl_department o on e.parent_id = o.id
--extra table here compared to earlier versions to allow us
--to traverse hierarchy in both directions
union all
select --This one finds "child" departments
o.id,
o.parent_id,
cte.iteration + 1
from
cte
join tbl_department o on o.id = cte.parent_id
where
cte.iteration >=0 --prevents siblings/uncles etc
union all
select --This one finds "parent" departments
o.id,
o.parent_id,
cte.iteration - 1
from
cte
join tbl_department o on o.parent_id = cte.id
where
cte.iteration <=0 --prevents siblings/uncles etc
)
You can test my script using this SQL Fiddle (updated).

Related

SQL statement : average

My question: What is the average age to become the first grandpa. The solution should be given out as average_age. The day a person becomes grandpa is where his first grandchild was born.
Relations:
human (name, gender, age)
parent (ParentName, ChildName) -> is subset of human(name).
Table:
I do know that grandpa is the person which has a parentname and a child in childname which is also a person(father) in parentname which has children in childname (grandchildren). The problem is now how do I get the average age to become grandpa.
What I got so far:
SELECT AVG(age) as average_age
FROM human h JOIN
parent p
ON h.name = p.parentname
WHERE h.gender = 'm' AND p.parentname = p.childname AND h.name = p.parentname
Expected outcome:
average_age : 52
It is extremely unusually to be storing the AGE of people in a table, because that changes -- every day. The data should be stored with a date of birth.
This is an aggregation query, but you have to join the tables multiple times. To get grandparents, you need a join on the parents table. Then you need to bring in humans for filtering:
select avg(min_age * 1.0)
from (select min(h_grandparent.age - h.grandchild.age) as min_age
from parent p join -- p.parentname is the grandparent
parent pchild
on p.childname = pchild.parentname join
human h_grandparent
on p.parentname = h_grandparent.name join
human h_grandchild
on pchild.childname = h_grandchild.name
where h_grandparent.gender = 'm'
group by h_grandparent.name
) a
I would address this with an exists condition that filters on humans that have grandchilds:
select avg(age) avg_age_of_grandpas
from human h
where
gender = 'm'
and exists (
select 1
from parent p1
inner join parent p2 on p2.parentName = p1.childName
where p1.parentName = h.name
)
The exists condition ensures that the person has at least one child and one grand child. The the outer query computes the average of such humans. Given the information available in your table structure, this seems to me like the most logical approach. Unlike joins, using exists avoids duplicating the records (and getting wrong results in the average) when a person has more than one line of descendants.
If you want the age of the grand parent at the date when their first grand child was born, then it is a bit complicated. This should get you close to what you expect:
select avg(h.age - g.maxGrandChildAge) avg_age_of_grandpas
from human h
inner join (
select
p1.parentName grandParentName,
max(h1.age) maxGrandChildAge
from parent p1
inner join parent p2 on p2.parentName = p1.childName
inner join human h1 on h1.name = p2.childName
) g
on g.grandParentName = h.name
You can do this with 2 more joins and subtraction the age of biggest grandchild:
SELECT AVG(p_age) average_age
FROM
(
SELECT h.name, h.age-MAX(h2.age) as p_age
FROM parent p1
LEFT JOIN parent p2 ON P1.childname = P2.parentname
INNER JOIN human h ON P1.parentname = h.name
INNER JOIN human h2 ON P2.ChildName = h2.name
WHERE h.gender = 'm' AND p2.childname IS NOT NULL
GROUP BY h.name, h.age
)pAges
Please consider that the name is not appropriate data for doing this task.

Hierarchical query with join. Performance issue

I am trying to form a query that fetch the details of subordinates under multiple level of supervisors. The hierarchy is as follows:
General manager- regional manager- district manager - supervisor - employee
My requirement is that, any manager in this hierarchy should be able to see the details of the employee at the bottom of the hierarchy.
So far I have tried the following options:
1: 102 seconds to execute
WITH w AS
( select personId from assignments start with supervisorId = :inputId
Connect by prior personId = supervisorId)
Select columns FROM a,b
where a.column = b.column
And a.personId = w.personId;
2: 92 seconds
Select columns FROM a,b
where a.column = b.column
And a.personId IN ( select personId from assignments start with supervisorId = :inputId
Connect by prior personId = supervisorId);
3:0.75 seconds[no data fetched]
WITH w AS ( select columns FROM a,b
where a.column = b.column)
Select w.columns from w, assignments
Where t.personId = assignments.personId
Start with supervisorId = :inputId
Connect by prior personId = supervisorId.
In option 3, since join condition is executed first before connect by, that could be why it does not return any rows. Please note that when fetching data for a single employee, it takes less than a second to execute, ie by removing the hierarchical query part and the assignments table and giving personId of a single person.
So I am looking for an efficient solution to meet this requirement. Any method, even not including hierarchical queries are welcome.
Thanks,
Anoop

SELECT Statement in CASE

Please don't downgrade this as it is bit complex for me to explain. I'm working on data migration so some of the structures look weird because it was designed by someone like that.
For ex, I have a table Person with PersonID and PersonName as columns. I have duplicates in the table.
I have Details table where I have PersonName stored in a column. This PersonName may or may not exist in the Person table. I need to retrieve PersonID from the matching records otherwise put some hardcode value in PersonID.
I can't write below query because PersonName is duplicated in Person Table, this join doubles the rows if there is a matching record due to join.
SELECT d.Fields, PersonID
FROM Details d
JOIN Person p ON d.PersonName = p.PersonName
The below query works but I don't know how to replace "NULL" with some value I want in place of NULL
SELECT d.Fields, (SELECT TOP 1 PersonID FROM Person where PersonName = d.PersonName )
FROM Details d
So, there are some PersonNames in the Details table which are not existent in Person table. How do I write CASE WHEN in this case?
I tried below but it didn't work
SELECT d.Fields,
CASE WHEN (SELECT TOP 1 PersonID
FROM Person
WHERE PersonName = d.PersonName) = null
THEN 123
ELSE (SELECT TOP 1 PersonID
FROM Person
WHERE PersonName = d.PersonName) END Name
FROM Details d
This query is still showing the same output as 2nd query. Please advise me on this. Let me know, if I'm unclear anywhere. Thanks
well.. I figured I can put ISNULL on top of SELECT to make it work.
SELECT d.Fields,
ISNULL(SELECT TOP 1 p.PersonID
FROM Person p where p.PersonName = d.PersonName, 124) id
FROM Details d
A simple left outer join to pull back all persons with an optional match on the details table should work with a case statement to get your desired result.
SELECT
*
FROM
(
SELECT
Instance=ROW_NUMBER() OVER (PARTITION BY PersonName),
PersonID=CASE WHEN d.PersonName IS NULL THEN 'XXXX' ELSE p.PersonID END,
d.Fields
FROM
Person p
LEFT OUTER JOIN Details d on d.PersonName=p.PersonName
)AS X
WHERE
Instance=1
Ooh goody, a chance to use two LEFT JOINs. The first will list the IDs where they exist, and insert a default otherwise; the second will eliminate the duplicates.
SELECT d.Fields, ISNULL(p1.PersonID, 123)
FROM Details d
LEFT JOIN Person p1 ON d.PersonName = p1.PersonName
LEFT JOIN Person p2 ON p2.PersonName = p1.PersonName
AND p2.PersonID < p1.PersonID
WHERE p2.PersonID IS NULL
You could use common table expressions to build up the missing datasets, i.e. your complete Person table, then join that to your Detail table as follows;
declare #n int;
-- set your default PersonID here;
set #n = 123;
-- Make sure previous SQL statement is terminated with semilcolon for with clause to parse successfully.
-- First build our unique list of names from table Detail.
with cteUniqueDetailPerson
(
[PersonName]
)
as
(
select distinct [PersonName]
from [Details]
)
-- Second get unique Person entries and record the most recent PersonID value as the active Person.
, cteUniquePersonPerson
(
[PersonID]
, [PersonName]
)
as
(
select
max([PersonID]) -- if you wanted the original Person record instead of the last, change this to min.
, [PersonName]
from [Person]
group by [PersonName]
)
-- Third join unique datasets to get the PersonID when there is a match, otherwise use our default id #n.
-- NB, this would also include records when a Person exists with no Detail rows (they are filtered out with the final inner join)
, cteSudoPerson
(
[PersonID]
, [PersonName]
)
as
(
select
coalesce(upp.[PersonID],#n) as [PersonID]
coalesce(upp.[PersonName],udp.[PersonName]) as [PersonName]
from cteUniquePersonPerson upp
full outer join cteUniqueDetailPerson udp
on udp.[PersonName] = p.[PersonName]
)
-- Fourth, join detail to the sudo person table that includes either the original ID or our default ID.
select
d.[Fields]
, sp.[PersonID]
from [Details] d
inner join cteSudoPerson sp
on sp.[PersonName] = d.[PersonName];

Select highest association when given record's level is random

There are three tables involved. DBMS is Oracle 10g.
Employees = individual employee records
Emp_id (PK)
Emp_name
various detail fields
Department = contain hierarchical org structure
Dept_code (PK)
Department Name
Parent_id (refers to dept_code in same table for parent dept)
depth_level (1=highest level, 2=sub-depts of 1, etc ... max = 6)
various details fields
Association = mapping employees to departments
Assoc_id (pk)
emp_id (fk)
dept_code (fk)
other fields that relate different association types
Where the association maps employees to departments at various depths, I want to run a query that counts all employees grouped at depth = 2. If an employee works in a dept at level 6, I would need to resolve level 5, then level 4, then level 3 to get to level 2, but if they work in a dept at level 3, I only need to resolve to level 2.
Trying to figure out the most efficient way. So far, I'm looking at running 5 separate queries, one for each depth with varying numbers of subqueries to resolve the depth levels and then combining with union. My second thought was to create a static reference table to map each department code to a level 2 label, but maintaining that table would be problematic.
Anybody got any better ideas?
Recursive CTE saved the day. I apologize if my question wasn't clear, here is my solution although I may have changed some of the field names from the original post. I plan to replace the static value for U.ID in the first part of the union query with a parameter that will any department code and retrieve its respective subordinate departments.
In this case dept code '5000002' is the IT department, the results display all employees in various levels of the IT department hierarchy.
select r.full_name, r.id, u.dept_name, u.dept_id, u.dept_level
from clarity.srm_resources r,
clarity.PRJ_OBS_ASSOCIATIONS a,
(with DIRECT_DEPT (Parent_ID, Dept_ID, Dept_Name, Dept_Level)
as
(
SELECT U.PARENT_ID, U.ID AS DEPT_ID, U.NAME AS DEPT_NAME, 0 AS Dept_Level
FROM clarity.prj_obs_units u
where u.type_id = '5000001'
AND U.ID = '5000002'
UNION ALL
SELECT U.PARENT_ID, U.ID AS DEPT_ID, U.NAME AS DEPT_NAME, Dept_Level +1
FROM clarity.prj_obs_units u
INNER JOIN DIRECT_DEPT D
ON U.PARENT_ID = D.DEPT_ID
where u.type_id = '5000001'
)
SELECT Parent_ID, Dept_ID, Dept_Name, Dept_Level
FROM DIRECT_DEPT) u
where a.record_id = r.id
and a.unit_id = u.dept_id
and a.table_name ='SRM_RESOURCES'
and r.is_active = '1'
;

How to self JOIN recursively in SQL?

I have a table:
Series
========
ID
SeriesName
ParentSeriesID
A series can be a "root" series, (ParentSeriesID is 0 or null) or it can have a Parent. A series can also be several levels down, i.e. its Parent has a Parent, which has a Parent, etc.
How can I query the table to get a Series by it's ID and ALL descendant Series' ?
So far I have tried:
SELECT child.*
FROM Series parent JOIN Series child ON child.ParentSeriesID = parent.ID
WHERE parent.ID = #ParentID
But this only returns the first level of children, I want the parent node, and all "downstream" nodes. I am not sure how to progress from here.
If you are using SQL Server 2005+, you can use common-table expressions
With Family As
(
Select s.ID, s.ParentSeriesId, 0 as Depth
From Series s
Where ID = #ParentID
Union All
Select s2.ID, s2.ParentSeriesId, Depth + 1
From Series s2
Join Family
On Family.ID = s2.ParentSeriesId
)
Select *
From Family
For more:
Recursive Queries Using Common Table Expressions
I just enhance the work of Thomas. If you need to get the depth of the hierarchy and getting the parentid here is the code.
This was almost the same with Thomas' work.
With Family As
(
Select s.ID, s.ParentSeriesId, 0 as Depth
From Series s
Where ID = #ParentID <--- this was removed if you intend to get all hierarchy of the record. You can retain this if you want
Union All
Select s2.ID, s2.ParentSeriesId < --- change to **Family.ParentID**, Depth + 1
From Series s2
Join Family
On Family.ID = s2.ParentSeriesId
)
Select *
From Family
That's all. I know it's too late but I hope anyone who encounter this may help them. Thanks Thomas for the original code. :)
Make use of CTE feature avaiable in slq server 2005 onwards for recurisve query
USE AdventureWorks
GO
WITH Emp_CTE AS (
SELECT EmployeeID, ContactID, LoginID, ManagerID, Title, BirthDate
FROM HumanResources.Employee
WHERE ManagerID IS NULL
UNION ALL
SELECT e.EmployeeID, e.ContactID, e.LoginID, e.ManagerID, e.Title, e.BirthDate
FROM HumanResources.Employee e
INNER JOIN Emp_CTE ecte ON ecte.EmployeeID = e.ManagerID
)
SELECT *
FROM Emp_CTE
GO
You can view example over here :
SQL SERVER – Simple Example of Recursive CTE