I need some help with my recursive query to get a direct count (all members(children) directly) and total count (all team members) for my SSRS report.
Here is my current query and the result set.
WITH AgentHierarchy([Name], AId, UId, HLevel, ContractDate)
AS
(SELECT
FirstName + ' ' + LastName AS Name, AId, UId,
0 AS HLevel, ContractDate
FROM tbl_Asso
WHERE (AId ='A049')
UNION ALL
SELECT
e.FirstName + ' ' + e.LastName AS Name,
e.AId, e.UId,
eh.HLevel + 1 AS HLevel, e.ContractDate
FROM
tbl_Asso AS e
INNER JOIN
AgentHierarchy AS eh ON eh.AId = e.UId)
SELECT
AId, Name,
(select u.FirstName + ' ' + u.LastName
from tbl_Asso u
where u.AId = d.UId) as Upline,
UId,
HLevel,
ContractDate,
(Select count(*)
from tbl_Asso as dc
where dc.UId = d.AId) As DirectCount
FROM
AgentHierarchy AS d
ORDER BY
HierarchyLevel
the current result set
AId Name Upline UId HLevel ContractDate DirectCount
-----------------------------------------------------------------------
A049 King Bori Cindy Hoss A001 0 8/29/2012 5
A052 Kac Marque King Bori A049 1 11/6/2012 0
A050 Joseph Moto King Bori A049 1 10/9/2012 1
A059 Nancy Ante King Bori A049 1 3/27/2013 1
A053 Kathy May King Bori A049 1 11/15/2012 2
A057 Robert Murphy King Bori A049 1 2/12/2013 1
A051 Andy Jane Joseph Moto A050 2 2/14/2013 0
A060 Arian Colle Nancy Ante A059 2 3/26/2013 0
A058 Phil Hunk Robert Murphy A057 2 3/21/2013 0
A055 Rea Wane Kathy May A053 2 2/20/2013 1
A054 Gabby Orez Kathy May A053 2 12/7/2012 0
A056 Steve Wells Rea Wane A055 3 3/25/2013 0
I Need to change the above query to get the Direct count (all Members(children) directly) and TotalTeam count based on the contract date
E.g for e.g contract date between 03/01/2013 and 03/31/2013. I need to get the following result set.
I need to incorporate the parameter for contractDate (so that they can get the range or if it is null then they get all the records and the counts.
e.g (ContractDate between #Begindate and #Enddate) or ((#Begindate is null) and (#enddate is null))
AId Name Upline UId HLevel ContractDate DirectCount TotalTeam
---------------------------------------------------------------------------------
A049 King Bori Cindy Hoss A001 0 8/29/2012 1 4
A052 Kac Marque King Bori A049 1 11/6/2012 0 0
A050 Joseph Moto King Bori A049 1 10/9/2012 0 0
A059 Nancy Ante King Bori A049 1 3/27/2013 1 1
A053 Kathy May King Bori A049 1 11/15/2012 0 0
A057 Robert Murphy King Bori A049 1 2/12/2013 1 1
A051 Andy Jane Joseph Moto A050 2 2/14/2013 0 0
A060 Arian Colle Nancy Ante A059 2 3/26/2013 0 0
A058 Phil Hunk Robert Murphy A057 2 3/21/2013 0 0
A055 Rea Wane Kathy May A053 2 2/20/2013 1 1
A054 Gabby Orez Kathy May A053 2 12/7/2012 0 0
A056 Steve Wells Rea Wane A055 3 3/25/2013 0 0
Thanks In advance.
I am not certain but you are doing an explicit listing of a single person in your recursive CTE, this will limit scope to just that person and their parents ONLY. Unless you are doing recursion on a starting set of millions of records it should be able to handle a predicate at the bottom of the regular expression and not in the recursive CTE by itself. Provided you are handling proper max recursion level. For your contract date just leave that table out till the bottom where you link the recursion. Unless you need to get their levels first. In that case I would get that data in a first cte, then list a second one doing recurion on that.
Here is a simple example I do that includes combined sales, basically I form the recursion and am not id specific, I find max recursion(you can leave that part out if you want) to find the lowest leaf level, then I perform end predicates needed. I hope this helps. A lot of times I see people listing predicates in their recursive CTE that will limit their scope, keep in mind recursion is in essence limiting a layer over top itself n times. You can get data you need before and after that point but doing predicates in there will limit where that scope leads to.
Declare #table table ( PersonId int identity, PersonName varchar(512), Account int, ParentId int, Orders int);
insert into #Table values ('Brett', 1, NULL, 1000),('John', 1, 1, 100),('James', 1, 1, 200),('Beth', 1, 2, 300),('John2', 2, 4, 400);
select
PersonID
, PersonName
, Account
, ParentID
from #Table
; with recursion as
(
select
t1.PersonID
, t1.PersonName
, t1.Account
--, t1.ParentID
, cast(isnull(t2.PersonName, '')
+ Case when t2.PersonName is not null then '\' + t1.PersonName else t1.PersonName end
as varchar(255)) as fullheirarchy
, 1 as pos
, cast(t1.orders +
isnull(t2.orders,0) -- if the parent has no orders than zero
as int) as Orders
from #Table t1
left join #Table t2 on t1.ParentId = t2.PersonId
union all
select
t.PersonID
, t.PersonName
, t.Account
--, t.ParentID
, cast(r.fullheirarchy + '\' + t.PersonName as varchar(255))
, pos + 1 -- increases
, r.orders + t.orders
from #Table t
join recursion r on t.ParentId = r.PersonId
)
, b as
(
select *, max(pos) over(partition by PersonID) as maxrec -- I find the maximum occurrence of position by person
from recursion
)
select *
from b
where pos = maxrec -- finds the furthest down tree
-- and Account = 2 -- I could find just someone from a different department
Related
Here is what I am trying to produce:
Row_Num Person Value Row_Number
1 Leo Math 1
1 Leo Science 2
1 Leo History 3
1 Leo Math,Science,History 4
2 Robert Gym 2
2 Robert Math 3
2 Robert History 4
2 Robert Gym,Math,History 1
3 David Art 1
3 David Science 2
3 David English 3
3 David History 4
3 David Computer Science 5
3 David Art,Science,English,History,Computer Science 6
This is the code I am using:
with part_1 as
(
select
1 as [Row_Num],
'Leo' as [Person],
'Math,Science,History' as [Subjects]
---
union
---
select
'2',
'Robert',
'Gym,Math,History'
---
union
---
select
'3',
'David',
'Art,Science,English,History,Computer Science'
---
)
----------------------------------------------------------------------
select
[Row_Num],
[Person],
[Subjects]
into
#part1
from
part_1;
go
--------------------------------------------------------------------------------
with p_2 as(
select
[Row_Num],
[Person],
--[Subjects],
[value]
from
#part1
cross apply
STRING_SPLIT([Subjects],',')
union all
select
[Row_Num],
[Person],
[Subjects]
from
#part1
)
select
[Row_Num]
,[Person]
,[Value]
,row_number()
over(Partition by Row_Num order by (select 1)) as [Row_Number]
from
p_2
order by
[Row_Num]
,[Row_Number]
Here is what I am producing:
Row_Num Person Value Row_Number
1 Leo Math 1
1 Leo Science 2
1 Leo History 3
1 Leo Math,Science,History 4
2 Robert Gym,Math,History 1
2 Robert Gym 2
2 Robert Math 3
2 Robert History 4
3 David Art 1
3 David Science 2
3 David English 3
3 David History 4
3 David Computer Science 5
3 David Art,Science,English,History,Computer Science 6
It looks good, until you look at Robert. All of the subjects are on the first row, instead of the bottom.
Any suggestions?
STRING_SPLIT is documented to not "care" about ordinal position:
The output rows might be in any order. The order is not guaranteed to match the order of the substrings in the input string. You can override the final sort order by using an ORDER BY clause on the SELECT statement (ORDER BY value).
If the ordinal position of the data is important, don't use STRING_SPLIT. Personally, I recommend using delimitedsplit8k_LEAD, which includes a itemnumber column.
But idealy, the real solution is to stop storing delimited data in your database. Create 2 further tables, one with a list of the subjects, and another that creates a relationship between the student and subject.
Note that SQL Server 2022 brings a new parameter to STRING_SPLIT called ordinal which, when 1 is passed to it, will cause STRING_SPLIT to return an additional column (called ordinal) with the ordinal position of the value within the string; so you could add that column to your ORDER BY to ensure the ordering in maintained.
Of course, this doesn't change the fact that you should not be storing delimited data to start with, and should still be aiming to fix your design.
Here's an easy solution.
DECLARE #x VARCHAR(1000) = 'a,b,c,d,e,f,g';
DECLARE #t TABLE
(
[Index] INT PRIMARY KEY IDENTITY(1, 1)
, [Value] VARCHAR(50)
)
INSERT INTO #t (VALUE)
SELECT [Value]
FROM string_split(#x, ',')
SELECT * FROM #t
Wrap it like this:
CREATE FUNCTION SPLIT_STRING2
(
#x VARCHAR(5000)
, #y VARCHAR(5000)
) RETURNS #t TABLE
(
[Index] INT PRIMARY KEY IDENTITY(1, 1)
, [Value] VARCHAR(50)
)
AS
BEGIN
INSERT INTO #t (VALUE)
SELECT [Value]
FROM string_split(#x, #y)
RETURN
END
Here is a recursive CTE method to parse the Subject. There's one anchor and two recursive queries. The first recursive query parses the Subjects. The second recursive query adds the summary. I added the special case of one subject. The summary and parsed subjects records are the same. (This means there was only one subjects in Subjects.) It is filtered out in this example.
This only works because a recursive CTE has only the records from the prior iteration. If looking for set N+1 and referring back to the CTE, the CTE has records from set N, not sets 1 through N. (Reminds me of the math proof - prove n = 1 is true, then prove if n true then n+1 true. Then it's true for all n > 0.)
DECLARE #delimiter char(1) = ',';
WITH part_1 as (
-- Subjects has 0 or more "tokens" seperated by a comma
SELECT *
FROM (values
(1, 'Leo', 'Math,Science,History'),
('2', 'Robert', 'Gym,Math,History'),
('3', 'David', 'Art,Science,English,History,Computer Science'),
('4', 'Lazy', 'Art')
) t ([Row_Num],[Person],[Subjects])
), part_2 as (
-- Anchor on the first token. Every token has delimiter before an after, even if we have to pretend it exists
SELECT Row_Num, Person, Subjects,
LEN(Subjects) + 1 as [index_max], -- the last index possible is a "pretend" index just after the end of the string
1 as N, -- this is the first token
0 as [index_before], -- for the first token, pretend the delimiter exists just before the first character at index 0
CASE WHEN CHARINDEX(#delimiter, Subjects) > 0 THEN CHARINDEX(#delimiter, Subjects) -- delimiter after exists
ELSE LEN(Subjects) + 1 -- pretend the delimiter exists just after the end of the string at index len + 1
END as [index_after],
CAST(1 as bit) as [is_token] -- needed to stop the 2nd recursion
FROM part_1
-- Recursive part that checks records for token N to add records for token N + 1 if it exists
UNION ALL
SELECT Row_Num, Person, Subjects,
index_max,
N + 1,
index_after, -- the delimiter before is just the prior token's delimiter after.
CASE WHEN CHARINDEX(#delimiter, Subjects, index_after + 1) > 0 THEN CHARINDEX(#delimiter, Subjects, index_after + 1) -- delimiter after exists
ELSE index_max -- pretend the delimiter exists just after the end of the string at index len + 1
END,
CAST(1 as bit) as [is_token] -- needed to stop the 2nd recursion
FROM part_2 -- a recursive CTE has only the prior result for token N, not accumulated result of tokens 1 to N
WHERE index_after > 0 AND index_after < index_max
UNION ALL
-- Another recursive part that checks if the prior token is the last. If the last, add the record with full string that was just parsed.
SELECT Row_Num, Person, Subjects,
index_max,
N + 1, -- this is not a token
0, -- the entire originsal string is desired
index_max, -- the entire originsal string is desired
CAST(0 as bit) as [is_token] -- not a token - stops this recursion
FROM part_2 -- this has only the prior result for N, not accumulated result of 1 to N
WHERE index_after = index_max -- the prior token was the last
AND is_token = 1 -- it was a token - stops this recursion
AND N > 1 -- add this to remove the added record it it's identical - 1 token
)
SELECT Row_Num, Person, TRIM(SUBSTRING(Subjects, index_before + 1, index_after - index_before - 1)) as [token], N,
index_max, index_before, index_after, is_token
FROM part_2
ORDER BY Row_Num, N -- Row_Num, is_token DESC, N is not required
Row_Num Person token N index_max index_before index_after is_token
----------- ------ -------------------------------------------- ----------- ----------- ------------ ----------- --------
1 Leo Math 1 21 0 5 1
1 Leo Science 2 21 5 13 1
1 Leo History 3 21 13 21 1
1 Leo Math,Science,History 4 21 0 21 0
2 Robert Gym 1 17 0 4 1
2 Robert Math 2 17 4 9 1
2 Robert History 3 17 9 17 1
2 Robert Gym,Math,History 4 17 0 17 0
3 David Art 1 45 0 4 1
3 David Science 2 45 4 12 1
3 David English 3 45 12 20 1
3 David History 4 45 20 28 1
3 David Computer Science 5 45 28 45 1
3 David Art,Science,English,History,Computer Science 6 45 0 45 0
4 Lazy Art 1 4 0 4 1
Windows Server 2012, MS SQL Server
I know there's a set-based way to do this, but I can't figure out how to concisely phrase my question to get a useful google answer.
tblConfig
companyid var val
---------------------------------------------------------
1 fruit orange
1 game Monopoly
1 book Joyland
1 actor Ernest Thesiger
1 condiment ketchup
2 fruit apple
2 book Revival
3 actor Colin Clive
3 condiment relish
3 fruit kiwi
3 book Tales From a Buick8
I would like to select company 2's values (or 3, or 4, or n...), plus company 1's values where 2 doesn't have one (order doesn't matter), as in:
2 fruit apple
1 game Monopoly
2 book Revival
1 actor Ernest Thesiger
1 condiment ketchup
I've looked at this answer and thought I could make it work, but it eludes me. I just end up with a list of all values in the table.
You are looking for a prioritization query. In SQL Server, you can do this using row_number():
select t.*
from (select t.*,
row_number() over (partition by var
order by (case when companyid = 2 then 1
when companyid = 1 then 2
end)
) as seqnum
from t
) t
where seqnum = 1;
The logic for prioritization is in the order by clause for row_number().
Declare #YourTable table (companyid int,var varchar(50), val varchar(50))
Insert into #YourTable values
(1,'fruit','orange'),
(1,'game','Monopoly'),
(1,'book','Joyland'),
(1,'actor','Ernest Thesiger'),
(1,'condiment','ketchup'),
(2,'fruit','apple'),
(2,'book','Revival'),
(3,'actor','Colin Clive'),
(3,'condiment','relish'),
(3,'fruit','kiwi'),
(3,'book','Tales From a Buick8')
;with cteBase as (
Select *
,RowNr=Row_Number() over (Partition By var order by companyid Desc)
From #YourTable
Where companyid<=2
)
Select * from cteBase where RowNr=1
Returns
companyid var val RowNr
1 actor Ernest Thesiger 1
2 book Revival 1
1 condiment ketchup 1
2 fruit apple 1
1 game Monopoly 1
I'm really confused on how to segment these groups into subgroups. This is an example of 2 orders (out of ~5M)
An order may have 1 or more "grouped items".
The group number = SUM(ItemQuantity).
Groups are ordered by OrderLine
eg. In the below table we see one group of "3" & two groups of "2"
OrderNo OrderLine GroupNo ItemQty
10496 1 3 1 =3
10496 2 3 1 =3
10496 3 3 1 =3
10496 4 2 1 =2(1)
10496 5 2 1 =2(1)
10496 6 2 1 =2(2)
10496 7 2 1 =2(2)
Rank() & Dense_Rank dont solve the issue as there are multiples of the same group, OrderLines are different.
I'll be joining this to another table in the end but what I'd like is a way to differentiate the same groups. Perhaps by adding a "subgroup" field.
OrderNo OrderLine GroupNo ItemQty Subgroup
10496 1 3 1 300
10496 2 3 1 300
10496 3 3 1 300
10496 4 2 1 201
10496 5 2 1 201
10496 6 2 1 202
10496 7 2 1 202
Testing below
CREATE TABLE #temptable(
OrderNo varchar(5),
OrderLine int,
GroupNo int,
ItemQty int);
INSERT INTO #temptable (OrderNo,OrderLine,GroupNo,ItemQty)
VALUES
('10496','1','3','1'),
('10496','2','3','1'),
('10496','3','3','1'),
('10495','1','4','1'),
('10495','2','4','2'),
('10495','3','4','1'),
('10495','4','2','1'),
('10495','5','2','1'),
('10495','6','3','1'),
('10495','7','3','2'),
('10495','8','2','1'),
('10495','9','2','1'),
('10495','10','2','1'),
('10495','11','2','1'),
('10495','12','2','1'),
('10495','13','2','1');
A DO WHILE
SUM(ItemQty)Over(Partition by OrderNo,GroupNo Order by OrderLine) >= GroupNo
may work but it'll need to run for every group in every order.
I then started using XML path to query each line but it's really not going to be efficient.
SELECT distinct t1.OrderNo,t1.GroupNo,
STUFF(( SELECT ',' + QUOTENAME(t2.OrderLine)
FROM #temptable t2
WHERE
t2.OrderNo = t1.OrderNo AND t2.GroupNo = t1.GroupNo
Order by t2.OrderLine Asc
FOR XML PATH(''),TYPE
).value('.', 'NVARCHAR(MAX)') ,1,1,'' )
AS [Rows]
FROM #temptable t1
Order by t1.OrderNo,t1.GroupNo
Taking #Nick.McDermaid s advice about the mod % here's a solution, admittedly it could be improved but for now it'll work out.
With a as (
select OrderNo,OrderLine,GroupNo,ItemQty
,CASE
WHEN SUM(ItemQty)Over
(Partition by OrderNo,GroupNo Order by OrderNo,OrderLine) % GroupNo=1
THEN GroupNo*100
ELSE NULL END as SG
from #temptable )
Select a.OrderNo,a.OrderLine,a.ItemQty,a.GroupNo
,MAX(a.SG2)Over(Partition by a.OrderNo,a.GroupNo Order by a.OrderNo,a.OrderLine ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) as Subgroup
from
(Select OrderNo,OrderLine,GroupNo,ItemQty
,CASE WHEN SG IS NULL THEN NULL ELSE SG+RANK()Over(Partition by OrderNo,SG Order by OrderNo,OrderLine) END as SG2
from a )a
Order by a.OrderNo,a.OrderLine;
We have a table with a parent child relationship, that represents a deep tree structure.
We are using a view with a CTE to query the data but the performance is poor (see code and execution plan below).
Is there any way we can improve the performance?
WITH cte (ParentJobTypeId, Id) AS
(
SELECT
Id, Id
FROM
dbo.JobTypes
UNION ALL
SELECT
e.Id, cte.Id
FROM
cte
INNER JOIN
dbo.JobTypes AS e ON e.ParentJobTypeId = cte.ParentJobTypeId
)
SELECT
ISNULL(Id, 0) AS ParentJobTypeId,
ISNULL(ParentJobTypeId, 0) AS Id
FROM
cte
A quick example of using the range keys. As I mentioned before, hierarchies were 127K points and some sections where 15 levels deep
The cte Builds, let's assume the hier results will be will be stored in a table (indexed as well)
Declare #Table table(ID int,ParentID int,[Status] varchar(50))
Insert #Table values
(1,101,'Pending'),
(2,101,'Complete'),
(3,101,'Complete'),
(4,102,'Complete'),
(101,null,null),
(102,null,null)
;With cteOH (ID,ParentID,Lvl,Seq)
as (
Select ID,ParentID,Lvl=1,cast(Format(ID,'000000') + '/' as varchar(500)) from #Table where ParentID is null
Union All
Select h.ID,h.ParentID,cteOH.Lvl+1,Seq=cast(cteOH.Seq + Format(h.ID,'000000') + '/' as varchar(500)) From #Table h INNER JOIN cteOH ON h.ParentID = cteOH.ID
),
cteR1 as (Select ID,Seq,R1=Row_Number() over (Order by Seq) From cteOH),
cteR2 as (Select A.ID,R2 = max(B.R1) From cteOH A Join cteR1 B on (B.Seq Like A.Seq+'%') Group By A.ID)
Select B.R1
,C.R2
,A.Lvl
,A.ID
,A.ParentID
Into #TempHier
From cteOH A
Join cteR1 B on (A.ID=B.ID)
Join cteR2 C on (A.ID=C.ID)
Select * from #TempHier
Select H.R1
,H.R2
,H.Lvl
,H.ID
,H.ParentID
,Total = count(*)
,Complete = sum(case when D.Status = 'Complete' then 1 else 0 end)
,Pending = sum(case when D.Status = 'Pending' then 1 else 0 end)
,PctCmpl = format(sum(case when D.Status = 'Complete' then 1.0 else 0.0 end)/count(*),'##0.00%')
From #TempHier H
Join (Select _R1=B.R1,A.* From #Table A Join #TempHier B on A.ID=B.ID) D on D._R1 between H.R1 and H.R2
Group By H.R1
,H.R2
,H.Lvl
,H.ID
,H.ParentID
Order By 1
Returns the hier in a #Temp table for now. Notice the R1 and R2, I call these the range keys. Data (without recursion) can be selected and aggregated via these keys
R1 R2 Lvl ID ParentID
1 4 1 101 NULL
2 2 2 1 101
3 3 2 2 101
4 4 2 3 101
5 6 1 102 NULL
6 6 2 4 102
VERY SIMPLE EXAMPLE: Illustrates the rolling the data up the hier.
R1 R2 Lvl ID ParentID Total Complete Pending PctCmpl
1 4 1 101 NULL 4 2 1 50.00%
2 2 2 1 101 1 0 1 0.00%
3 3 2 2 101 1 1 0 100.00%
4 4 2 3 101 1 1 0 100.00%
5 6 1 102 NULL 2 1 0 50.00%
6 6 2 4 102 1 1 0 100.00%
The real beauty of the the range keys, is if you know an ID, you know where it exists (all descendants and ancestors).
I have a table with the following structure
uid sid eid Key value
1 1 1 F.Name Simon
2 1 1 L.Name Jones
3 1 1 C.Name AAPL
4 1 1 Address Infinite
5 2 1 F.Name Brad
6 2 1 L.Name Pitt
7 2 1 C.Name Holly
8 2 1 Address LA
I would like to convert the above table to below format
sid F.Name L.Name C.Name Address
1 Simon Jones AAPL Infinite
2 Brad Pitt Holly LA
Basically I need values of "Key" column to be column fields in new table. I have no other table. Even Linq to sql is ok and I can understand it.
In 2005 and later:
SELECT *
FROM (
SELECT sid, key, value
FROM mytable
) q
PIVOT (
MIN(value)
FOR
key IN ([F.Name], [L.Name], [C.Name], [Address])
) p