How to avoid Recursive CTE repeating the anchor values - sql

I am using following CTE to get hierarchical structure of manager and employees, i have multiple managers for one employee, in this case i do not want CTE to repeat for that employee again and again as it is doing in my code -
getemp() is a simple function returning employeeid, name and managerID
;With hierarchy as
(
select [Level]=1 , * from dbo.getemp() where managerid = 1
union all
select [Level]+1 , e.* from getemp() e
join hierarchy h on h.employeeid = e.managerid
)
Select * from hierarchy
After edit -
Following approach working for me. Is it possible with CTE ?
SET NOCOUNT ON;
DECLARE #Rows int
SELECT [Level] = ISNULL(1,0),
employeeid = ISNULL(employeeid, 0 ),
empname = CAST(empname as varchar(10)),
managerid = ISNULL(managerid,0)
into #Temp1
from dbo.getemp() as a1
where a1.managerid = #Top1
--select * from #Temp1
SELECT #Rows=##ROWCOUNT
DECLARE #I INT = 2;
while #Rows > 0
BEGIN
Insert into #Temp1
select #I as Level, b.employeeid, b.empname, b.managerid from #Temp1 as e
inner join (select [employeeid], [empname], [managerid] from dbo.GetEmp()) as b on b.managerid = e.employeeid
where e.Level = #I - 1
and not exists (
SELECT 1 FROM #Temp1 t
WHERE b.employeeid = t.employeeid
AND b.managerid = t.managerid);
SELECT #Rows=##ROWCOUNT
--SELECT #Rows AS Rows
IF #Rows > 0
BEGIN
SELECT #I = #I + 1;
END
END
select distinct * from #Temp1
END

Since you have several managers, which means that people can also be in several different levels due to having different levels in the manager, you could just take the minimum levels for each branch with something like this:
;With hierarchy as
(
select [Level]=1 , * from dbo.getemp() where managerid = 1
union all
select [Level]+1 , e.* from getemp() e
join hierarchy h on h.employeeid = e.managerid
)
Select min(Level) as Level, employeeid, name, managerid from hierarchy
group by employeeid, name, managerid
Using a function to return all the employees in every recursion might not be the best solution regarding to performance, especially if it's not an inline function. You might want to consider using for example a temp. table if you can't read the tables themselves directly.

Couldnt find the solution using CTE, so I have used the while loop to avoid the repeating anchors, here the code ..
DECLARE #Rows int
SELECT [Level] = ISNULL(1,0),
employeeid = ISNULL(employeeid, 0 ),
empname = CAST(empname as varchar(10)),
managerid = ISNULL(managerid,0)
into #Temp1
from dbo.getemp() as a1
where a1.managerid = #Top1
--select * from #Temp1
SELECT #Rows=##ROWCOUNT
DECLARE #I INT = 2;
while #Rows > 0
BEGIN
Insert into #Temp1
select #I as Level, b.employeeid, b.empname, b.managerid from #Temp1 as e
inner join (select [employeeid], [empname], [managerid] from dbo.GetEmp()) as b on b.managerid = e.employeeid
where e.Level = #I - 1
and not exists (
SELECT 1 FROM #Temp1 t
WHERE b.employeeid = t.employeeid
AND b.managerid = t.managerid);
SELECT #Rows=##ROWCOUNT
--SELECT #Rows AS Rows
IF #Rows > 0
BEGIN
SELECT #I = #I + 1;
END
END
select distinct * from #Temp1
END

Related

How to find Missing number in Sequence field using SQL?

I have a table called Student and field name called StudentNumber
Student Table
StudentNumber
-------------
1
2
3
4
5
8
10
Expecting output
6
7
9
I tried like below
Declare #trans int;
set #trans = 1;
while(#trans <=50000)
BEGIN
if((select StudentNumber from [Student] where StudentNumber = #trans) != #trans)
BEGIN
print #trans;
END
END
set #trans = #trans + 1;
You could use (SQL Server 2016 and above):
SELECT Number
FROM (select cast([key] as int) +
(SELECT MIN(StudentNumber) FROM Students) as number
from OPENJSON( '[1'
+ replicate(',1',(SELECT MAX(StudentNumber) FROM Students)-
(SELECT MIN(StudentNumber) FROM Students))+']')) n
LEFT JOIN Students s
ON n.number = s.StudentNumber
WHERE s.StudentNumber IS NULL;
DBFiddle Demo
Note: You could exchange first subquery with any other tally number generator.
More info: SQL, Auxiliary table of numbers
it should be like below
Declare #trans int;
set #trans = 1;
while(#trans <=50000)
BEGIN
if NOT EXISTS (select StudentNumber from [Student] where StudentNumber = #trans)
BEGIN
print #trans;
END
END
set #trans = #trans + 1;
You can do the following
;with report as(
select 1 as missing
union all
select missing + 1
from report
where missing < #max
)
select *
from report m
where not exists ( select 1 from student s where s.id = m.missing)
option (maxrecursion 0);
Here a working demo
Result
missing
6
7
9
Hope that this will help you
I know you try to solve it iterative, just for sports
an idea how to achieve the same using recursive CTE
with missingstudents (StudentNumber)
as (
select StudentNumber-1 from Students s
where not exists (
select StudentNumber from Students s2
where s.StudentNumber-1 = s2.StudentNumber)
UNION ALL
select StudentNumber-1 from missingstudents s
where not exists (
select StudentNumber from Students s2
where s.StudentNumber-1 = s2.StudentNumber)
)
select * from missingstudents
You can try this:
select m.number from
(select min(StudentNumber) a,max(StudentNumber) b from Students) c ,master..spt_values M
where c.a <= m.number
and c.b > = m.number
and type ='P'
and m.number not in (select StudentNumber from Students)
Try like this;
declare #id int
declare #maxStudentNumber int
set #id = 1
select #maxStudentNumber = max(StudentNumber) from Student
create table #MissingIds
(
id int
)
while #id < #maxStudentNumber
begin
insert into #MissingIds values(#id)
set #id = #id + 1
end
select m.id
from #MissingIds m
left join Student s
on m.id = s.StudentNumber
where s.StudentNumber is null
drop table #MissingIds

SQL: Optimizing Recursive CTE

Example table structure:
EmployeeId TeamleaderId TopTeamleaderId LEVEL ParentTree CompanyId
1 0 0 0 NULL 1
2 1 1 1 2>1 1
3 2 1 2 3>2>1 1
TeamleaderId is foreignKey reference to EmployeeId in the same table
Goal:
Whenever a row is inserted in the table with EmployeeId, TeamleaderId, CompanyId automatically populate TopTeamleaderId, LEVEL and ParentTree with AFTER INSERT trigger
Code:
WITH CTE AS (
SELECT EmployeeId, TeamleaderId,0 AS [Level], CAST(EmployeeId AS varchar(100)) AS Heirarchy, TopTeamleaderId
FROM dbo.Employee
WHERE EmployeeId IN (SELECT EmployeeId FROM Employee WHERE TeamleaderId IS NULL
AND CompanyId IN(SELECT DISTINCT CompanyId FROM INSERTED))
UNION ALL
SELECT mgr.EmployeeId, mgr.TeamleaderId, CTE.[Level] +1 AS [Level],
CAST(( CAST(mgr.EmployeeId AS VARCHAR(100)) + '>' + CTE.Heirarchy) AS varchar(100)) AS Heirarchy, CTE.TopTeamleaderId
FROM CTE
INNER JOIN dbo.Employee AS mgr
ON TaskCTE.EmployeeId = mgr.ParentTeamleaderId
)
UPDATE Employee SET [LEVEL] = TC.[LEVEL], ParentTree = TC.Heirarchy, TopTeamleaderId = TC.TopTeamleaderId
FROM dbo.Employee AS Employee
JOIN (SELECT * FROM CTE WHERE EmployeeId IN(SELECT DISTINCT EmployeeId FROM INSERTED) AND ParentTeamleaderId IS NOT NULL) TC
ON
Employee.EmployeeId = TC.EmployeeId
Problem:
Imagine there are like 1000000 employees in a company, this query would take a long time to execute. How to optimize it so that only the parents of the inserted row are taken in to account?
Recursive CTE's are great, but as you can see the perfomance can suffer with larger hierarchies. It is my firm belief that there is no shame in temp tables.
The following will generate a 200K point hierarchy in 0.784 seconds.
Example
Select EmployeeId
,TeamleaderId
,Lvl=1
,TopTeamleaderId = 0
,ParentTree=cast(EmployeeId as varchar(500))
,CompanyID
Into #TempBld
From Employee
Where TeamleaderId is null
Declare #Cnt int=1
While #Cnt<=30 --<< Set Your Max Level
Begin
Insert Into #TempBld
Select A.EmployeeId
,A.TeamleaderId
,B.Lvl+1
,IIF(B.Lvl=1,B.EmployeeId,B.TopTeamleaderId)
,concat(A.EmployeeId,'>',B.ParentTree)
,A.CompanyID
From Employee A
Join #TempBld B on (B.Lvl=#Cnt and A.TeamleaderId=B.EmployeeId)
Set #Cnt=#Cnt+1
End
--Select * from #TempBld Order by ParentTree
Returns

Returning column with count of 0

I have a query that looks up a list of documents depending on their department and their status.
DECLARE #StatusIds NVARCHAR(MAX) = '1,2,3,4,5';
DECLARE #DepartmentId NVARCHAR(2) = 'IT';
SELECT ILDPST.name,
COUNT(*) AS TodayCount
FROM dbo.TableA ILDP
LEFT JOIN dbo.TableB ILDPS ON ILDPS.IntranetLoanDealPreStateId = ILDP.IntranetLoanDealPreStateId
LEFT JOIN dbo.TableC ILDPST ON ILDPST.IntranetLoanDealPreStateTypeId = ILDPS.CurrentStateTypeId
WHERE (ILDP.CreatedByDepartmentId = #DepartmentId OR #DepartmentId IS NULL)
AND ILDPS.CurrentStateTypeId IN (
SELECT value
FROM dbo.StringAsIntTable(#StatusIds)
)
GROUP BY ILDPST.name;
This returns the results:
However, I'd also like to be able to return statuses where the TodayCount is equal to 0 (i.e. any status with an id included in #StatusIds should be returned, regardless of TodayCount).
I've tried messing with some unions / joins / ctes but I couldn't quite get it to work. I'm not much of an SQL person so not sure what else to provide that could be useful.
Thanks!
If you want to have all the records from TableC you need to left join all other tables to it, not left join it to the other tables. Also it's best to INNER JOIN the filtering table you create from #StatusIds rather then apply it through INclause. Try this:
DECLARE #StatusIds NVARCHAR(MAX) = '1,2,3,4,5';
DECLARE #DepartmentId NVARCHAR(2) = 'IT';
SELECT ILDPST.Name, COUNT(ILDP.IntranetLoanDealPreStateId) AS TodayCount
FROM (SELECT DISTINCT value FROM dbo.StringAsIntTable(#StatusIds)) StatusIds
INNER JOIN dbo.TableC ILDPST
ON ILDPST.IntranetLoanDealPreStateTypeId = StatusIds.value
LEFT JOIN dbo.TableB ILDPS
ON ILDPS.CurrentStateTypeId = ILDPST.IntranetLoanDealPreStateTypeId
LEFT JOIN dbo.TableA ILDP
ON ILDP.IntranetLoanDealPreStateId = ILDPS.IntranetLoanDealPreStateId
AND (ILDP.CreatedByDepartmentId = #DepartmentId OR #DepartmentId IS NULL)
GROUP BY ILDPST.Name;
Try this instead:
DECLARE #StatusIds NVARCHAR(MAX) = '1,2,3,4,5';
DECLARE #DepartmentId NVARCHAR(2) = 'IT';
SELECT ILDPST.name,
COUNT(ILDP.IntranetLoanDealPreStateId) AS TodayCount
FROM
dbo.TableC ILDPST
LEFT JOIN
dbo.TableB ILDPS ON ILDPST.IntranetLoanDealPreStateTypeId = ILDPS.CurrentStateTypeId
LEFT JOIN
dbo.TableA ILDP ON ILDPS.IntranetLoanDealPreStateId = ILDP.IntranetLoanDealPreStateId
AND (ILDP.CreatedByDepartmentId = #DepartmentId OR #DepartmentId IS NULL)
WHERE
ILDPST.IntranetLoanDealPreStateTypeId
IN (
SELECT value
FROM dbo.StringAsIntTable(#StatusIds)
)
GROUP BY ILDPST.name;
You could use the following function to create a table value for your status id's.
CREATE FUNCTION [dbo].[SplitString]
(
#myString varchar(max),
#deliminator varchar(2)
)
RETURNS
#ReturnTable TABLE
(
[Part] [varchar](max) NULL
)
AS
BEGIN
Declare #iSpaces int
Declare #part varchar(max)
--initialize spaces
Select #iSpaces = charindex(#deliminator,#myString,0)
While #iSpaces > 0
Begin
Select #part = substring(#myString,0,charindex(#deliminator,#myString,0))
Insert Into #ReturnTable(Part)
Select #part
Select #myString = substring(#mystring,charindex(#deliminator,#myString,0)+ len(#deliminator),len(#myString) - charindex(' ',#myString,0))
Select #iSpaces = charindex(#deliminator,#myString,0)
end
If len(#myString) > 0
Insert Into #ReturnTable
Select #myString
RETURN
END
This can now be used as a table that you can LEFT JOIN to.
DECLARE #StatusIds NVARCHAR(MAX) = '1,2,3,4,5';
SELECT * FROM dbo.SplitString(#StatusIds, ',')
It is not tested but give it a try:
;With Cte ( Value ) As
( Select Distinct Value From dbo.StringAsIntTable( #StatusIds ) )
Select
ILDPST.name,
COUNT(*) AS TodayCount
From
dbo.TableC As ILDPST
Inner Join Cte On ( ILDPST.IntranetLoanDealPreStateTypeId = Cte.Value )
Left Join dbo.TableB As ILDPS On ( ILDPST.IntranetLoanDealPreStateTypeId = ILDPS.CurrentStateTypeId )
Left Join dbo.TableA As ILDP On ( ILDPS.IntranetLoanDealPreStateId = ILDP.IntranetLoanDealPreStateId )
And ( ( ILDP.CreatedByDepartmentId = #DepartmentId ) Or ( #DepartmentId Is Null ) )
Group By
ILDPST.name

How to tune the following query?

This query gives me the desired result but i can't run this query every time.The 2 loops is costing me.So i need to implement something like view.But the logic has temp tables involved which isn't allowed in views as well.so, is there any other way to store this result or change the query so that it will cost me less.
DECLARE #Temp TABLE (
[SiteID] VARCHAR(100)
,[StructureID] INT
,[row] DECIMAL(4, 2)
,[col] DECIMAL(4, 2)
)
DECLARE #siteID VARCHAR(100)
,#structureID INT
,#struct_row INT
,#struct_col INT
,#rows_count INT
,#cols_count INT
,#row INT
,#col INT
DECLARE structure_cursor CURSOR
FOR
SELECT StructureID
,SiteID
,Cols / 8.5 AS Cols
,Rows / 11 AS Rows
FROM Structure
WHERE SellerID = 658 --AND StructureID = 55
OPEN structure_cursor
FETCH NEXT
FROM structure_cursor
INTO #structureID
,#siteID
,#struct_col
,#struct_row
SELECT #rows_count = 1
,#cols_count = 1
,#row = 1
,#col = 1
WHILE ##FETCH_STATUS = 0
BEGIN
WHILE #row <= #struct_row
BEGIN
WHILE #col <= #struct_col
BEGIN
--PRINT 'MEssage';
INSERT INTO #Temp (
SiteID
,StructureID
,row
,col
)
VALUES (
#siteID
,#structureID
,#rows_count
,#cols_count
)
SET #cols_count = #cols_count + 1;
SET #col = #col + 1;
END
SET #cols_count = 1;
SET #col = 1;
SET #rows_count = #rows_count + 1;
SET #row = #row + 1;
END
SET #row = 1;
SET #col = 1;
SET #rows_count = 1;
FETCH NEXT
FROM structure_cursor
INTO #structureID
,#siteID
,#struct_col
,#struct_row
END
CLOSE structure_cursor;
DEALLOCATE structure_cursor;
SELECT * FROM #Temp
Do this with a set-based operation. I think you just want insert . . . select:
INSERT INTO #Temp (SiteID, StructureID, row, col)
SELECT StructureID, SiteID, Cols / 8.5 AS Cols, Rows / 11 AS Rows
FROM Structure
WHERE SellerID = 658;
You should avoid cursors, unless you really need them for some reason (such as calling a stored procedure or using dynamic SQL on each row).
EDIT:
Reading the logic, it looks like you want to insert rows for based on the limits in each row. You still don't want to use a cursor. For that, you need a number generator and master..spt_values is a convenient one, if it has enough rows. So:
with n as (
select row_number() over (order by (select null)) as n
from master..spt_values
)
INSERT INTO #Temp (SiteID, StructureID, row, col)
SELECT StructureID, SiteID, ncol.n / 8.5 AS Cols, nrow.n / 11 AS Rows
FROM Structure s JOIN
n ncol
ON ncol.n <= s.struct_col CROSS JOIN
n nrow
ON nrow <= s.struct_row
WHERE SellerID = 658;
You can generate the number of rows and columns and then CROSS APPLY with those, like below. I've left out your SellerID condition.
;WITH Cols
AS
(
SELECT StructureID, SiteID, CAST(Cols / 8.5 AS INT) AS Col
FROM Structure
UNION ALL
SELECT s.StructureID, s.SiteID, Col - 1
FROM Structure s
INNER JOIN Cols c ON s.StructureID = c.StructureID AND s.SiteID = c.SiteID
WHERE Col > 1
)
, Rows
AS
(
SELECT StructureID, SiteID, CAST(Rows / 11 AS INT) AS Row
FROM Structure
UNION ALL
SELECT s.StructureID, s.SiteID, Row - 1
FROM Structure s
INNER JOIN Rows r ON s.StructureID = r.StructureID AND s.SiteID = r.SiteID
WHERE Row > 1
)
--INSERT INTO #Temp (SiteID, StructureID, row, col)
SELECT s.SiteID, s.StructureID, r.Row, c.Col
FROM Structure s
CROSS APPLY Cols c
CROSS APPLY Rows r
WHERE s.StructureID = c.StructureID AND s.SiteID = c.SiteID
AND s.StructureID = r.StructureID AND s.SiteID = r.SiteID
We can do this by using CROSS APPLY and CTE.
CREATE TABLE Structure(SiteID varchar(20), StructureID int,
Cols decimal(18,2), [Rows] decimal(18,2))
INSERT INTO Structure (SiteID, StructureID, Cols, [Rows])
VALUES
('MN353970', 51,17,22),
('MN272252', 52,17,11)
;WITH RowCTE([Rows]) AS
(
SELECT 1
UNION ALL
SELECT 2
),
ColCTE(Cols) AS
(
SELECT 1
UNION ALL
SELECT 2
)
SELECT SiteID, StructureID, R.Rows, C.Cols
FROM Structure s
CROSS APPLY
(
SELECT Cols FROM ColCTE
) C
CROSS APPLY
(
SELECT [Rows] FROM RowCTE
) R
Sql Fiddle Demo

TSQL Multiple count using same table with different JOIN

I have a weird situation and not too sure how to approach it.
I have 2 separate tables:
Table A is submissions
id
submitterQID
nomineeQID
story
Table B is employees
QID
Name
Department
I am trying to get the total number of submissions grouped by department as well as the total number of nominations.
This is what my Stored procedure looks like:
BEGIN
SELECT TOP 50 count(A.[nomineeQID]) AS totalNominations,
count(A.[subQID]) AS totalSubmissions,
B.[DepartmentDesc] AS department
FROM empowermentSubmissions AS A
JOIN empTable AS B
ON B.[qid] = A.[nomineeQID]
WHERE A.[statusID] = 3
AND A.[locationID] = #locale
GROUP BY B.[Department]
ORDER BY totalNominations DESC
FOR XML PATH ('data'), TYPE, ELEMENTS, ROOT ('root');
END
This issue with this is that the JOIN is joining by the nomineeQID only and not the subQID as well.
My end result I am looking for is:
Department Customer Service has 25 submissions and 90 nominations
ORDERED BY the SUM of both counts...
I tried to just JOIN again on the subQID but was told I cant join on the same table twice.
Is there an easier way to accomplish this?
This is a situaton where you'll need to gather your counts independently of each other. Using two left joins will cause some rows to be counted twice in the first left join when the join condition is met for both. Your scenario can be solved using either correlated subqueries or an outer apply gathering the counts on different criteria. I did not present a COUNT(CASE ... ) option here, because you don't have an either-or scenario in the data, you have two foreign keys to the employees table. So, setting up sample data:
declare #empowermentSubmissions table (submissionID int primary key identity(1,1), submissionDate datetime, nomineeQID INT, submitterQID INT, statusID INT, locationID INT)
declare #empTable table (QID int primary key identity(1,1), AreaDesc varchar(10), DepartmentDesc varchar(20))
declare #locale INT = 0
declare #n int = 1
while #n < 50
begin
insert into #empTable (AreaDesc, DepartmentDesc) values ('Area ' + cast((#n % 2)+1 as varchar(1)), 'Department ' + cast((#n % 4)+1 as varchar(1)))
set #n = #n + 1
end
set #n = 1
while #n < 500
begin
insert into #empowermentSubmissions (submissionDate, nomineeQID, submitterQID, StatusID, locationID) values (dateadd(dd,-(cast(rand()*600 as int)),getdate()), (select top 1 QID from #empTable order by newid()), (select top 1 QID from #empTable order by newid()), 3 + (#n % 2) - (#n % 3), (#n % 2) )
set #n = #n + 1
end
And now the OUTER APPLY option:
SELECT TOP 50 E.DepartmentDesc, SUM(N.Nominations) Nominations, SUM(S.TotalSubmissions) TotalSubmissions
FROM #empTable E
OUTER APPLY (
SELECT COUNT(submissionID) Nominations
FROM #empowermentSubmissions A
WHERE A.statusID = 3
AND A.nomineeQID = E.QID
AND A.locationID = #locale
) N
OUTER APPLY (
SELECT COUNT(submissionID) TotalSubmissions
FROM #empowermentSubmissions A
WHERE A.statusID = 3
AND A.submitterQID = E.QID
AND A.locationID = #locale
) S
GROUP BY E.DepartmentDesc
ORDER BY SUM(Nominations) + SUM(TotalSubmissions) DESC