Recursive SQL query for finding matches - sql

I have 5 SQL Tables with the following columns:
tbl_department:
department_id, parent_id
tbl_employee
employee_id, department_id
tbl_department_manager
department_id, employee_manager_id
tbl_request_regular_employee
request_id, employee_id
tbl_request_special_employee
request_id, 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)
We cannot use ORM here since app's responsiveness is our priority and the script might be called a lot.
Here's the logic I want to implement:
First we query to tbl_department_manager based on employee_id to check whether the current employee is a manager or not (also the employee can be a manager in a few departments). If so, we get a list of department_id (if nothing is found, just return false)
If we got at least one id in tbl_department_manager we query to tbl_request_regular_employee AND tbl_request_special_employee based on request_id and get employee_id from both tables (they are the same)
Based on employee_id collected above we query to tbl_employee to get a unique list of department_id that the employee belongs to.
Finally have a list of unique department_id from p.3 which we can compare to the one (ones) that we got in p.1.
The catch is, however, that in tbl_department there might be departments which inherit from the one (ones) that we got from p.1 (so we might need to find it recursively based on parent_id until we find at least one match with one element from p.1). If there's at least one match between one element in p.1 and one element in p.3 return true. So there's a need to look for it recursively.
Could someone give a clue how to implement it in MSSQL? Any help would be greatly appreciated.

declare #employee_id int, #request_id int;
with reqEmployees as (
select regular_employee_id as employee_id
from tbl_request_regular_employee
where request_id = #request_id
union all --concatenate the two tables
select special_employee_id
from tbl_request_special_employee
where request_id = #request_id
),
cte as (
select e.department_id, null as parent_id
from reqEmployees r
join tbl_employee e on e.employee_id = r.employee_id -- get these employees' departments
union all
select d.department_id, d.parent_id
from cte -- recurse the cte
join tbl_department d on d.department_id = cte.parent_id -- and get parent departments
)
-- we only want to know if there is any manager row, so exists is enough
select case when exists (select 1
from cte --join on managers
join tbl_department_manager dm on dm.department_id = cte.department_id
where dm.employee_manager_id = #employee_id)
then 1 else 0 end;

Related

How do I select all id's that are present in one table but not in another

I am trying to get a list of department Ids are present in one table, (PS_Y_FORM_HIRE), but which don't exist in another table (PS_DEPARTMENT_VW).
Here is the basics of what I have which isn't working:
SELECT h.DEPTID FROM PS_Y_FORM_HIRE h, PS_DEPARTMENT_VW d WHERE NOT EXISTS (
SELECT d1.DEPTID FROM PS_DEPARTMENT_VW d1 WHERE d1.DEPTID = h.DEPTID
and d1.SETID_GL_DEPT = 'IDBYU'
);
I'm trying to form this query in SQL Developer, but it just returns a long list of blanks (after spinning/running the query for a very long time).
In addition, I need this to be effective dated, so that it only grabs the correct effective-dated row, but I was unsure how and where to incorporate this into the query.
EDIT I neglected to mention that only the department table is effective dated. The form hire table is not. I need to get the current effectively dated row from that in this query (to make sure the data is accurate).
Also note that DEPTID isn't a key on PS_Y_FORM_HIRE, but is on PS_DEPARTMENT_VW. (Along with SETID_GL_DEPT and EFFDT).
So again, ideally, I will have a list of all the department ids that appear in PS_Y_FORM_HIRE, but which are not in PS_DEPARTMENT_VW.
SELECT DEPTID
FROM PS_Y_FORM_HIRE
MINUS
SELECT DEPTID
FROM PS_DEPARTMENT_VW
WHERE SETID_GL_DEPT = 'IDBYU';
or
SELECT DEPTID
FROM PS_Y_FORM_HIRE
WHERE DEPTID NOT IN (
SELECT DEPTID
FROM PS_DEPARTMENT_VW
WHERE SETID_GL_DEPT = 'IDBYU'
)
or
SELECT DEPTID
FROM PS_Y_FORM_HIRE h
WHERE NOT EXISTS (
SELECT 1
FROM PS_DEPARTMENT_VW d
WHERE SETID_GL_DEPT = 'IDBYU'
AND d.DEPTID = h.DEPTID
)
This seems like a job for the MINUS operation. Something like
select deptid from ps_y_form_hire where eff_date = <whatever>
minus
select deptid from ps_department_vw <where eff_date = ...>
You didn't provide information to determine what exactly you want done with the effective dates; adapt as needed.
SELECT h.DEPTID
FROM PS_Y_FORM_HIRE h
WHERE h.DEPTID NOT IN (SELECT p.DEPTID
FROM PS_DEPARTMENT_VW p
WHERE p.SETID_GL_DEPT = 'IDBYU')
Your question is a bit unclear around why you want effective dated rows as you are not checking effective status or any other field that may have changed between effective rows. If your question is, You want to know all DEPTIDs in PS_Y_FORM_HIRE that either don't exist or are inactive as of a current effective date, then the SQL below should help
SELECT DEPTID
FROM PS_Y_FORM_HIRE h
WHERE
H.DEPTID NOT IN ( SELECT d.DEPTID
FROM PS_DEPARTMENT_VW d
WHERE d.EFF_STATUS = 'A'
AND d.EFFDT = (SELECT MAX(EFFDT)
FROM PS_DEPARTMENT_VW d2
WHERE d2.SETID_GL_DEPT = d.SETID_GL_DEPT
AND d2.DEPTID = d.DEPTID
AND d2.EFFDT <= CURRENT_DATE)
)

SQL Server CTE use IDs from single column with EXCEPT?

Having received kindness the other day from someone whose eyes were less bleary than mine I thought I'd give it another shot. Thanks in advance for your assistance.
I have a single SQL Server (2012) table named Contacts. That table has four columns I am currently concerned with. The table has a total of 71,454 rows. There are two types of records in the table; Companies and Employees. Both use the same column, named (Client ID), for their primary key. The existence of a Company Name is what differentiates between Company and Employee data. Employees have no associated Company Name. There are 29,021 Companies leaving 42,433 Employees.
There may be 0-n number of Employees associated with any one Company. I am attempting to create output that will reflect the relationship between Companies and Clients, if there are any. I would like to use the Company ID (Client ID column) as my anchor data set.
Not sure my definition is correct but the thought was to create a CTE of the known Companies by virtue of a given Company Name. Then, use the remaining Client IDs but use the EXCEPT clause to filter the already-retrieved Client IDs out of the result set.
Here the code I currently have;
;
WITH cte ( BaseID, Client_id, Company_name,
First_name, Last_name, [level] )
AS ( SELECT Client_id AS BaseID ,
Client_id ,
Company_name ,
First_name ,
Last_name ,
1
FROM dbo.Conv_client_clean
WHERE ( COMPANY_NAME IS NOT NULL
OR COMPANY_NAME != ''
)
UNION ALL
SELECT c.BaseID ,
children.Client_id ,
children.Company_name ,
children.First_name ,
children.Last_name ,
cte.[level] + 1
FROM dbo.Conv_client_clean children
INNER JOIN cte c ON c.Client_id = children.CLIENT_ID
EXCEPT
SELECT children.Client_id
FROM cte
)
SELECT BaseID ,
Client_id ,
Company_name ,
first_name ,
Last_name ,
[Level]
FROM cte
OPTION ( MAXRECURSION 0 );
In this instance I receive the following error;
Msg 252, Level 16, State 1, Line 3
Recursive common table expression 'cte' does not contain a top-level UNION ALL operator.
Any suggestions?
Thanks!
In the recursion cte query, you cannot have more set operations(union, except, union all,intersect) after the the one Union ALL which is refers the cte itself. I think what you can try is change the query as below and check
...
UNION ALL
SELECT c.BaseID ,
children.Client_id ,
children.Company_name ,
children.First_name ,
children.Last_name ,
cte.[level] + 1
FROM dbo.Conv_client_clean children
WHERE children.Client_id NOT IN (SELECT Client_id FROM cte)
As mentioned to Kiran I was able to concoct an 'old fashioned' approach what is good enough for now.
Thank you everyone for your kind attention.
I'm not sure what you are trying to do with level. It seems that it will be 1 for companies and 2 for employees. If that's the case, you don't even need recursion. The first part of your cte creates a list of companies. That's fine. Now use that to join back to the original table to show all the employees too.
WITH
cte( BaseID, ClientID, Company_name, First_name, Last_name )AS(
SELECT Base_ID,
Base_ID AS Client_id ,
Company_name,
First_name,
Last_name
FROM dbo.Conv_client_clean
WHERE COMPANY_NAME IS NOT NULL
OR COMPANY_NAME <> ''
)
select c2.Base_id, c2.Client_id,
c1.Company_Name, c2.First_Name, c2.Last_Name,
case when c2.client_id is null then 1 else 2 end Level
from cte c1
join Conv_client_clean c2
on c1.BaseID = isnull( c2.Client_ID, c2.Base_id )
order by c1.BaseID, c2.Base_id;
Here's where I fiddled with it.
Unfortunately anything besides UNION ALL, after you've made your recursive reference, will not work. And if you think about it, it makes sense.
Recursion is conceptually identical to the following where recursion continues until max depth is reached or a query returns no results upon which another execution could act.
WITH Anchor AS (select...)
,recurse1 as (<Some body referring to Anchor>)
,recurse2 as (<Identical body except referring to recurse1>)
,recurse3 as (<Identical body except referring to recurse2>)
...
select * from Anchor
union all
select * from recurse1
union all
select * from recurse2
...
The problem is that conjunctive operators apply to EVERYTHING that precedes it. In your case, EXCEPT operates on everything to it's left side which includes the Anchor query. Afterwards, when looking for the anchor to which the recursive part must be applied, the query compiler doesn't find a 'top level union all operator' any more because it's been consumed as part of the left side of your recursive query.
It wouldn't help to contrive some syntax akin to parenthesis that could delimit the scope of the left side of your table conjunction because you would then build a case of 'multiple recursive references' which is also illegal.
BOTTOM LINE IS: The only conjunction that works in the recursive part of your query is UNION ALL because it simply concatenates the right side. It doesn't require knowledge of the left side to determine which rows to include.

How to get records from both tables using ms access query

I have 2 Tables in Ms Access
tbl_Master_Employess
tbl_Emp_Salary
I want to show all the employees in the employee table linked with employee salary table
to link both table the id is coluqEmpID in both table
In the second table, I have a date column. I need a query which should fetch records from both tables using a particular date
I tried the following query:
select coluqEID as EmployeeID , colEName as EmployeeName,"" as Type, "" as Amt
from tbl_Master_Employee
union Select b.coluqEID as EmployeeID, b.colEName as EmployeeName, colType as Type, colAmount as Amt
from tbl_Emp_Salary a, tbl_Master_Employee b
where a.coluqEID = b.coluqEID and a.colDate = #12/09/2013#
However, it shows duplicates.
Query4
EmployeeID EmployeeName Type Amt
1 LAKSHMANAN
1 LAKSHMANAN Advance 100
2 PONRAJ
2 PONRAJ Advance 200
3 VIJAYAN
4 THIRUPATHI
5 VIJAYAKUMAR
6 GOVINDAN
7 TAMILMANI
8 SELVAM
9 ANAMALAI
10 KUMARAN
How would I rewrite my query to avoid duplicates, or what would be a different way to not show duplicates?
The problem with your query is that you are using union when what you want is a join. The union is first going to list all employees with the first part:
select coluqEID as EmployeeID , colEName as EmployeeName,"" as Type, "" as Amt
from tbl_Master_Employee
and then adds to that list all employee records where they have a salary with a certain date.
Select b.coluqEID as EmployeeID, b.colEName as EmployeeName, colType as Type,
colAmount as Amt
from tbl_Emp_Salary a, tbl_Master_Employee b
where a.coluqEID = b.coluqEID and a.colDate = #12/09/2013#
Is your goal to get a list of all employees and only display salary information for those who have a certain date? Some sample data would be useful. Assuming the data here: SQL Fiddle this query should create what you want.
Select a.coluqEID as EmployeeID, colEName as EmployeeName,
b.colType as Type, b.colAmount as Amt
FROM tbl_Master_Employees as a
LEFT JOIN (select coluqEID, colType, colAmount FROM tbl_EMP_Salary
where colDate = '20130912') as b ON a.coluqEID = b.coluqEID;
The first step is to create a select that will get you just the salaries that you want by date. You can then perform a join on this as if you were performing a separate query. You use a LEFT JOIN because you want all of the records from one side, the employees, and only the records that match your criteria from the second side, your salaries.
I believe you will need a join, however as to your question on Unique names.
select **DISTINCT** coluqEID as EmployeeID
Adding the distinct operator would give only uniquely returned results.

Randomly assign work location and each location should not exceed the number of designated employees

I am trying to select unique random posting/recruitment places of employees within a list of places, all the employees are already posted at these places, i am trying to generate a new random posting place for them with "where" condition that "employee new random location will not be equal to their home place and randomnly selected Employees with their designation must be less than or equal to Place wise designation numbers from Places table "
the Employee table is :
EmpNo EmpName CurrentPosting Home Designation RandomPosting
1 Mac Alabama Missouri Manager
2 Peter California Montana Manager
3 Prasad Delaware Nebraska PO
4 Kumar Indiana Nevada PO
5 Roy Iowa New Jersey Clerk
And so on...
And the Places table (PlaceNames with number of employees - designation wise) is :-
PlaceID PlaceName Manager PO Clerk
1 Alabama 2 0 1
2 Alaska 1 1 1
3 Arizona 1 0 2
4 Arkansas 2 1 1
5 California 1 1 1
6 Colorado 1 1 2
7 Connecticut 0 2 0
and so on...
tried with with newid() like as below and to be able to select Employees with RandomPosting place names,
WITH cteCrossJoin AS (
SELECT e.*, p.PlaceName AS RandomPosting,
ROW_NUMBER() OVER(PARTITION BY e.EmpNo ORDER BY NEWID()) AS RowNum
FROM Employee e
CROSS JOIN Place p
WHERE e.Home <> p.PlaceName
)
SELECT *
FROM cteCrossJoin
WHERE RowNum = 1;
additionally I need to limit the random selection based upon designation numbers(in Places table)... that is to assign each Employee a PlaceName(from Places) randomly which is not equal to CurrentPosting and Home(in Employee) and Place wise designation will not exceed as given numbers.
Thanks in advance.
Maybe something like this:
select C.* from
(
select *, ROW_NUMBER() OVER(PARTITION BY P.PlaceID, E.Designation ORDER BY NEWID()) AS RandPosition
from Place as P cross join Employee E
where P.PlaceName != E.Home AND P.PlaceName != E.CurrentPosting
) as C
where
(C.Designation = 'Manager' AND C.RandPosition <= C.Manager) OR
(C.Designation = 'PO' AND C.RandPosition <= C.PO) OR
(C.Designation = 'Clerk' AND C.RandPosition <= C.Clerk)
That should attempt to match employees randomly based on their designation discarding same currentPosting and home, and not assign more than what is specified in each column for the designation. However, this could return the same employee for several places, since they could match more than one based on that criteria.
EDIT:
After seeing your comment about not having a need for a high performing single query to solve this problem (which I'm not sure is even possible), and since it seems to be more of a "one-off" process that you will be calling, I wrote up the following code using a cursor and one temporary table to solve your problem of assignments:
select *, null NewPlaceID into #Employee from Employee
declare #empNo int
DECLARE emp_cursor CURSOR FOR
SELECT EmpNo from Employee order by newid()
OPEN emp_cursor
FETCH NEXT FROM emp_cursor INTO #empNo
WHILE ##FETCH_STATUS = 0
BEGIN
update #Employee
set NewPlaceID =
(
select top 1 p.PlaceID from Place p
where
p.PlaceName != #Employee.Home AND
p.PlaceName != #Employee.CurrentPosting AND
(
CASE #Employee.Designation
WHEN 'Manager' THEN p.Manager
WHEN 'PO' THEN p.PO
WHEN 'Clerk' THEN p.Clerk
END
) > (select count(*) from #Employee e2 where e2.NewPlaceID = p.PlaceID AND e2.Designation = #Employee.Designation)
order by newid()
)
where #Employee.EmpNo = #empNo
FETCH NEXT FROM emp_cursor INTO #empNo
END
CLOSE emp_cursor
DEALLOCATE emp_cursor
select e.*, p.PlaceName as RandomPosting from Employee e
inner join #Employee e2 on (e.EmpNo = e2.EmpNo)
inner join Place p on (e2.NewPlaceID = p.PlaceID)
drop table #Employee
The basic idea is, that it iterates over the employees, in random order, and assigns to each one a random Place that meets the criteria of different home and current posting, as well as controlling the amount that get assigned to each place for each Designation to ensure that the locations are not "over-assigned" for each role.
This snippet doesn't actually alter your data though. The final SELECT statement just returns the proposed assignments. However you could very easily alter it to make actual changes to your Employee table accordingly.
I am assuming the constraints are:
An employee cannot go to the same location s/he is currently at.
All sites must have at least one employee in each category, where an employee is expected.
The most important idea is to realize that you are not looking for a "random" assignment. You are looking for a permutation of positions, subject to the condition that everyone moves somewhere else.
I am going to describe an answer for managers. You will probably want three queries for each type of employee.
The key idea is a ManagerPositions table. This has a place, a sequential number, and a sequential number within the place. The following is an example:
Araria 1 1
Araria 2 2
Arwal 1 3
Arungabad 1 4
The query creates this table by joining to INFORMATION_SCHEMA.columns with a row_number() function to assign a sequence. This is a quick and dirty way to get a sequence in SQL Server -- but perfectly valid as long as the maximum number you need (that is, the maximum number of managers in any one location) is less than the number of columns in the database. There are other methods to handle the more general case.
The next key idea is to rotate the places, rather than randomly choosing them. This uses ideas from modulo arithmetic -- add an offset and take the remainder over the total number of positions. The final query looks like this:
with ManagerPositions as (
select p.*,
row_number() over (order by placerand, posseqnum) as seqnum,
nums.posseqnum
from (select p.*, newid() as placerand
from places p
) p join
(select row_number() over (order by (select NULL)) as posseqnum
from INFORMATION_SCHEMA.COLUMNS c
) nums
on p.Manager <= nums.posseqnum
),
managers as (
select e.*, mp.seqnum
from (select e.*,
row_number() over (partition by currentposting order by newid()
) as posseqnum
from Employees e
where e.Designation = 'Manager'
) e join
ManagerPositions mp
on e.CurrentPosting = mp.PlaceName and
e.posseqnum = mp.posseqnum
)
select m.*, mp.PlaceId, mp.PlaceName
from managers m cross join
(select max(seqnum) as maxseqnum, max(posseqnum) as maxposseqnum
from managerPositions mp
) const join
managerPositions mp
on (m.seqnum+maxposseqnum+1) % maxseqnum + 1 = mp.seqnum
Okay, I realize this is complicated. You have a table for each manager position (not a count as in your statement, having a row for each position is important). There are two ways to identify a position. The first is by place and by the count within the place (posseqnum). The second is by an incremental id on the rows.
Find the current position in the table for each manager. This should be unique, because I'm taking into account the number of managers in each place. Then, add an offset to the position, and assign that place. By having the offset larger than the maxseqnum, the managers is guaranteed to move to another location (except in unusual boundary cases where one location has more than half the managers).
If all current manager positions are filled, then this guarantees that all will move to the next location. Because ManagerPositions uses a random id for assigning seqnum, the "next" place is random, not next by id or alphabetically.
This solution does have many employees traveling together to the same new location. You can fix this somewhat by trying values other than "1" in the expression (m.seqnum+maxposseqnum+1).
I realize that there is a way to modify this, to prevent the correlation between the current place and the next place. This does the following:
Assigns the seqnum to ManagerPosition randomly
Compare different offsets in the table, rating each by the number of times two positions in the table, separated by that offset, are the same.
Choose the offset with the minimum rating (which is preferably 0).
Use that offset in the final matching clause.
I don't have enough time right now to write the SQL for this.

How To Get Column Description

For example, in a [emp] table, the columns are :
emp_id emp_name emp_role
If the values being inserted in emp_role column values can be 0 (for Administrator), 1 (for Management), 2 (for Employees).
Now is there any way to get those details of a column emp_role (like, 0 for Administrator) along with the table concerned (i.e, [emp]) in SQL server database ?
Thanks.
If you have dictionary table with role definitions it will be something similar to:
select e.emp_id, e.emp_name, r.name
from emp e
inner join role r on e.emp_role = r.id
if not, but you know role names it will be something similar to:
select emp_id, emp_name,
case emp_role when 0 then 'Administrator' when 1 then 'Management' when 2 then 'Employees' end as RoleName
from emp