Sql Server 2008 - PIVOT without Aggregation Function - sql

I know you've got multiple topics touching on this. But, I havent found one that addressed my needs. I need to (on demand) pivot select deep table data to a wide output table. The gotcha in this is that I cannot use an aggregate with Pivot because it eats responses that are needed in the output. I have worked up to a solution, but I don't think it's the best because it will require umpteen left joins to work. I've included all attempts and notes as follows:
-- Sql Server 2008 db.
-- Deep table structure (not subject to modification) contains name/value pairs with a userId as
-- foreign key. In many cases there can be MORE THAN ONE itemValue given by the user for the
-- itemName such as if asked their race, can answer White + Hispanic, etc. Each response is stored
-- as a seperate record - this cannot currently be changed.
-- Goal: pivot deep data to wide while also compressing result
-- set down. Account for all items per userId, and duplicating
-- column values (rather than show nulls) as applicable
-- Sample table to store some data of both single and multiple responses
DECLARE #testTable AS TABLE(userId int, itemName varchar(50), itemValue varchar(255))
INSERT INTO #testTable
SELECT 1, 'q01', '1-q01 Answer'
UNION SELECT 1, 'q02', '1-q02 Answer'
UNION SELECT 1, 'q03', '1-q03 Answer 1'
UNION SELECT 1, 'q03', '1-q03 Answer 2'
UNION SELECT 1, 'q03', '1-q03 Answer 3'
UNION SELECT 1, 'q04', '1-q04 Answer'
UNION SELECT 1, 'q05', '1-q05 Answer'
UNION SELECT 2, 'q01', '2-q01 Answer'
UNION SELECT 2, 'q02', '2-q02 Answer'
UNION SELECT 2, 'q03', '2-q03 Answer 1'
UNION SELECT 2, 'q03', '2-q03 Answer 2'
UNION SELECT 2, 'q04', '2-q04 Answer'
UNION SELECT 2, 'q05', '2-q05 Answer'
SELECT 'Raw Data'
SELECT * FROM #TestTable
SELECT 'Using Pivot - shows aggregate result of itemValue per itemName - eats others'
; WITH Data AS (
SELECT
[userId]
, [itemName]
, [itemValue]
FROM
#testTable
)
SELECT
[userId]
, [q02]
, [q03]
, [q05]
FROM
Data
PIVOT
(
MIN(itemValue) -- Aggregate function eats needed values.
FOR itemName in ([q02], [q03], [q05])
) AS PivotTable
SELECT 'Aggregate with Grouping - Causes Null Values'
SELECT
DISTINCT userId
,[q02] = Max(CASE WHEN itemName = 'q02' THEN itemValue END)
,[q03] = Max(CASE WHEN itemName = 'q03' THEN itemValue END)
,[q05] = Max(CASE WHEN itemName = 'q05' THEN itemValue END)
FROM
#testTable
WHERE
itemName in ('q02', 'q03', 'q05') -- Makes it a hair quicker
GROUP BY
userId -- If by userId only, it only gives 1 row PERIOD = BAD!!
, [itemName]
, [itemValue]
SELECT 'Multiple Left Joins - works properly but bad if pivoting 175 columns or so'
; WITH Data AS (
SELECT
userId
,[itemName]
,[itemValue]
FROM
#testTable
WHERE
itemName in ('q02', 'q03', 'q05') -- Makes it a hair quicker
)
SELECT
DISTINCT s1.userId
,[q02] = s2.[itemValue]
,[q03] = s3.[itemValue]
,[q05] = s5.[itemValue]
FROM
Data s1
LEFT JOIN Data s2
ON s2.userId = s1.userId
AND s2.[itemName] = 'q02'
LEFT JOIN Data s3
ON s3.userId = s1.userId
AND s3.[itemName] = 'q03'
LEFT JOIN Data s5
ON s5.userId = s1.userId
AND s5.[itemName] = 'q05'
So the bottom query is the only one (so far) that does what I need it to do, but the LEFT JOIN's WILL get out of hand and cause performance issues when I use actual item names to pivot. Any recommendations are appreciated.

I think you'll have to stick with joins, because joins are exactly the way of producing results like the one you are after. The purpose of a join is to combine row sets together (on a condition or without any), and your target output is nothing else than a combination of subsets of rows.
However, if the majority of questions always have single responses, you could substantially reduce the number of necessary joins. The idea is to join only multiple-response groups as separate row sets. As for the single-response items, they are joined only as part of the entire dataset of target items.
An example should better illustrate what I might poorly describe verbally. Assuming there are two potentially multiple-response groups in the source data, 'q03' and 'q06' (actually, here's the source table:
DECLARE #testTable AS TABLE(
userId int,
itemName varchar(50),
itemValue varchar(255)
);
INSERT INTO #testTable
SELECT 1, 'q01', '1-q01 Answer'
UNION SELECT 1, 'q02', '1-q02 Answer'
UNION SELECT 1, 'q03', '1-q03 Answer 1'
UNION SELECT 1, 'q03', '1-q03 Answer 2'
UNION SELECT 1, 'q03', '1-q03 Answer 3'
UNION SELECT 1, 'q04', '1-q04 Answer'
UNION SELECT 1, 'q05', '1-q05 Answer'
UNION SELECT 1, 'q06', '1-q06 Answer 1'
UNION SELECT 1, 'q06', '1-q06 Answer 2'
UNION SELECT 1, 'q06', '1-q06 Answer 3'
UNION SELECT 2, 'q01', '2-q01 Answer'
UNION SELECT 2, 'q02', '2-q02 Answer'
UNION SELECT 2, 'q03', '2-q03 Answer 1'
UNION SELECT 2, 'q03', '2-q03 Answer 2'
UNION SELECT 2, 'q04', '2-q04 Answer'
UNION SELECT 2, 'q05', '2-q05 Answer'
UNION SELECT 2, 'q06', '2-q06 Answer 1'
UNION SELECT 2, 'q06', '2-q06 Answer 2'
;
which is same as the table in the original post, but with added 'q06' items), the resulting script could be like this:
WITH ranked AS (
SELECT
*,
rn = ROW_NUMBER() OVER (PARTITION BY userId, itemName ORDER BY itemValue)
FROM #testTable
),
multiplied AS (
SELECT
r.userId,
r.itemName,
r.itemValue,
rn03 = r03.rn,
rn06 = r06.rn
FROM ranked r03
INNER JOIN ranked r06 ON r03.userId = r06.userId AND r06.itemName = 'q06'
INNER JOIN ranked r ON r03.userId = r.userId AND (
r.itemName = 'q03' AND r.rn = r03.rn OR
r.itemName = 'q06' AND r.rn = r06.rn OR
r.itemName NOT IN ('q03', 'q06')
)
WHERE r03.itemName = 'q03'
AND r.itemName IN ('q02', 'q03', 'q05', 'q06')
)
SELECT userId, rn03, rn06, q02, q03, q05, q06
FROM multiplied
PIVOT (
MIN(itemValue)
FOR itemName in (q02, q03, q05, q06)
) AS PivotTable

; WITH SRData AS (
SELECT -- Only query single response items in this block
[userId]
, [q01]
, [q02]
, [q04]
, [q05]
FROM
#testTable
PIVOT
(
MIN(itemValue)
FOR itemName in ([q01], [q02], [q04], [q05])
) AS PivotTable
)
SELECT
sr.[userId]
, sr.[q01]
, sr.[q02]
, [q03] = mr03.[itemValue]
, sr.[q04]
, sr.[q05]
, [q06] = mr06.[itemValue]
FROM
SRData sr
LEFT JOIN #testTable mr03 ON mr03.userId = sr.userId AND mr03.itemName = 'q03' -- Muli Response for q03
LEFT JOIN #testTable mr06 ON mr06.userId = sr.userId AND mr06.itemName = 'q06' -- Muli Response for q06

Not clear what the desired results should look like exactly but one possibility
; WITH Data AS (
SELECT
ROW_NUMBER() OVER (PARTITION BY [userId], [itemName]
ORDER BY [itemValue]) AS RN
, [userId]
, [itemName]
, [itemValue]
FROM
#testTable
)
SELECT
[userId]
, [q02]
, [q03]
, [q05]
FROM
Data
PIVOT
(
MIN(itemValue)
FOR itemName in ([q02], [q03], [q05])
) AS PivotTable
Returns
userId q02 q03 q05
----------- ------------------------------ ------------------------------ ------------------------------
1 1-q02 Answer 1-q03 Answer 1 1-q05 Answer
1 NULL 1-q03 Answer 2 NULL
1 NULL 1-q03 Answer 3 NULL
2 2-q02 Answer 2-q03 Answer 1 2-q05 Answer
2 NULL 2-q03 Answer 2 NULL

Related

Aggregate on a non group by column check if any value matches a criteria

Let's say I have a table Category with columns
id, childCategory, hasParts
Let's say I want to group by id and check if any value in hasParts has value true.
How to do this efficiently?
this has got to be the most vague post that i've seen on here but i'll take a stab at it. based on my own imagination and the 3 sentences provided, here we go:
create table category (id int, childcategory nvarchar(25), hasparts bit)
insert category
select 1, 'stroller', 1
union all
select 1, 'rocker', 1
union all
select 2, 'car', 0
union all
select 2, 'doll', 0
union all
select 3, 'nasal sprayer', 0
union all
select 3, 'thermometer', 1
select *,
case when exists (select 1 from category b where a.id = b.id and b.hasparts = 1) then 'has true value' end as truecheck
from
(
select id, count(*) as inventory
from category
group by id
) a
drop table category
this should theoretically get you want you want. adjust as needed.

Group accounts by 1 condition vs 2 conditions

I am trying to get a count of pat_id that were either only "Sent" or only "Sent and Received"
Here is my query so far
select pat_id,
(case when addr_msg_actn_c in(1) then 'Sent' else convert(varchar(10),'Received') end) [Status]
from pat_addr_verif
where pat_id in ('Z1000354','Z1000363','Z1000392','Z1000394')
group by pat_id, addr_msg_actn_c
order by pat_id
Which generates this output
However, I'd like to simplify the output to display like this
Here's a small proof of concept. You can generate a case statement on the sum of a value to choose which text string you want.
CREATE TABLE #SentReceive (
Pat_ID NVARCHAR(8)
,[status] TINYINT
)
INSERT INTO #SentReceive
VALUES ( 'Z1000354' ,1 )
,( 'Z1000354' ,1 )
,( 'Z1000363' ,1 )
,( 'Z1000363' ,2 )
,( 'Z1000392' ,2 )
,( 'Z1000394' ,1 )
,( 'Z1000394' ,2 )
SELECT pat_id
,sentstatus = CASE sum(distinct [status])
WHEN 1
THEN 'Sent'
WHEN 2
THEN 'Received'
WHEN 3
THEN 'Sent and Received'
END
FROM #SentReceive
GROUP BY pat_id
ORDER BY pat_id
create table SentReceive
(
Pat_ID nvarchar(8),
SentOnly nvarchar(8)
)
insert SentReceive
values
('Z1000354','Sent'),
('Z1000363','Sent'),
('Z1000363','Received'),
('Z1000392','Sent'),
('Z1000394','Sent'),
('Z1000394','Received')
select distinct a.Pat_ID,
case
when b.SentOnly is not null then
case a.SentOnly
when 'Received' then b.SentOnly + ' and ' + a.SentOnly
else a.SentOnly + ' and ' + b.SentOnly
end
else a.SentOnly
end as Status
from SentReceive a
left join SentReceive b on a.Pat_ID = b.Pat_ID and a.SentOnly <> b.SentOnly
I did a quickie with only the available data that you provided. If there are other variations in your dataset then you will need to account for that and it may or may not change what I came up with. Good luck!
Here is what I came up with:
;with cte as
(
select 1 as _order, 'Z1000354' as pat_id, 'Sent' as SentOnly
union select 2, 'Z1000363', 'Sent'
union select 3, 'Z1000363', 'Received'
union select 4, 'Z1000392', 'Sent'
union select 5, 'Z1000393', 'Received'
union select 5, 'Z1000394', 'Sent'
union select 6, 'Z1000394', 'Received'
)
select pat_id, replace(_Status,',',' and') as _Status
from(
select distinct pat_id,
STUFF((Select ', '+SentOnly
from cte T2
where T1.pat_id=T2.pat_id
FOR XML PATH('')),1,1,'') as _Status from cte T1
) a
where _Status in (' Sent', ' Sent, Received')
Final query with some help from above posters:
select distinct a.pat_id,
case when b.ADDR_MSG_ACTN_C is not null
then 'Sent and Received' else 'Sent Only' end as Status
from pat_addr_verif a
left join pat_addr_verif b on a.pat_id = b.pat_id and
a.ADDR_MSG_ACTN_C <> b.ADDR_MSG_ACTN_C
where a.pat_id in ('Z1000354','Z1000363','Z1000392','Z1000394')
Correct Final Output

sql find max value of a column and select as new field

I am currently working with some ambulance data and in some scenario's multiple vehicles are dispatched to the same call. Of those dispatched sometimes they are stood down(diverted to a different call.)
The relevent fields are incidentID, VehicleAllocationSequenceNumber snd VehicleArrivalAtSceneDateTime.
An incidentID can map to multiple rows with different VehicleAllocationSequenceNumber and VehicleArrivalAtSceneDateTime.
VehicleAllocationSequenceNumber defines the order in which the vehicles arrived on the scene. VehicleArrivalAtSceneDateTime is either a datetime or NULL if the vehicle never arrived.
I want to select distinct IncidentID's with and condense the other columns into two new features; Number_of_Vehicles_assigned and number_of_vehicles_on_scene
I am fairly new to queries of this complexity and I am looking for a way to do this without using a cursor.
Thanks in advance
Edit:
Nevermind I nailed the query, think I'm just a bit scatter brained this morning. Here is my query for posterity's sake.
Feedback is welcome:
select
IncidentID,
max(VehicleAllocationSequenceNumber) as number_vehicles_assigned,
count(VehicleArrivalAtSceneDateTime) as number_of_vehicles_on_scene
FROM
[dwuserobj].[acc].[AmbulanceLinkedDatasetForModelling]
where
IncidentID in(
select distinct
IncidentID
from
[dwuserobj].[acc].[AmbulanceLinkedDatasetForModelling]
)
and vehicleArrivalAtSceneDateTime is not null
group by
IncidentID;
Use a case with a not exists
select distinct incidentID,
case
when not exists
(
select 1
from AmbulanceTable t2
where t2.incidentID = t1.incidentID
and t2.VehicleAllocationSequenceNumber is null
) then 1
else 0 as alloc,
case
when not exists
(
select 1
from AmbulanceTable t3
where t3.incidentID = t1.incidentID
and t3.VehicleArrivalAtSceneDateTime is null
) then 1
else 0 as onscene
from AmbulanceTable t1
Edit:
So no_vehicles is number of vehicles... my bad:
select incidentid
count(VehicleAllocationSequenceNumber) as num_assigned,
count(VehicleArrivalAtSceneDateTime ) as num_oncscene
from AmbulanceLinkedDatasetForModelling
group by incidentid
this will give you zeros if none assigned/on scene
this should provide you with the logic you need. I made a temp table so you can see how the incidentID interacts with the AllocationSequenceNumber and the SceneDateTime.
DECLARE #AmbulanceLinkedDatasetForModelling TABLE (IncidentID int, VehicleAllocationSequenceNumber int, VehicleArrivalAtSceneDateTime datetime)
INSERT INTO #AmbulanceLinkedDatasetForModelling
SELECT 1, 500, NULL UNION ALL -- the vehicle never arrived
SELECT 2, 501, '2016-05-20 06:07:00.370' UNION ALL
SELECT 3, 502, '2018-01-05 08:15:08.970' UNION ALL
SELECT 4, 503, NULL UNION ALL -- the vehicle never arrived
SELECT 4, 504, '2017-04-11 11:22:01.360'UNION ALL
SELECT 4, 505, '2017-04-11 11:32:01.360'
SELECT
IncidentID,
number_vehicles_assigned=COUNT(DISTINCT VehicleAllocationSequenceNumber),
no_vehicles_on_scene=SUM(CASE WHEN VehicleArrivalAtSceneDateTime IS NOT NULL THEN 1 ELSE 0 END)
FROM
#AmbulanceLinkedDatasetForModelling
GROUP BY
IncidentID

How to merge two columns from CASE STATEMENT of DIFFERENT CONDITION

My expected result should be like
----invoiceNo----
T17080003,INV14080011
But right now, I've come up with following query.
SELECT AccountDoc.jobCode,AccountDoc.shipmentSyskey,AccountDoc.docType,
CASE AccountDoc.docType
WHEN 'M' THEN
JobInvoice.invoiceNo
WHEN 'I' THEN
(STUFF((SELECT ', ' + RTRIM(CAST(AccountDoc.docNo AS VARCHAR(20)))
FROM AccountDoc LEFT OUTER JOIN JobInvoice
ON AccountDoc.principalCode = JobInvoice.principalCode AND
AccountDoc.jobCode = JobInvoice.jobCode
WHERE (AccountDoc.isCancelledByCN = 0)
AND (AccountDoc.docType = 'I')
AND (AccountDoc.jobCode = #jobCode)
AND (AccountDoc.shipmentSyskey = #shipmentSyskey)
AND (AccountDoc.principalCode = #principalCode) FOR XML
PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' '))
END AS invoiceNo
FROM AccountDoc LEFT OUTER JOIN JobInvoice
ON JobInvoice.principalCode = AccountDoc.principalCode AND
JobInvoice.jobCode = AccountDoc.jobCode
WHERE (AccountDoc.jobCode = #jobCode)
AND (AccountDoc.isCancelledByCN = 0)
AND (AccountDoc.shipmentSyskey = #shipmentSyskey)
AND (AccountDoc.principalCode = #principalCode)
OUTPUT:
----invoiceNo----
T17080003
INV14080011
Explanation:
I want to select docNo from table AccountDoc if AccountDoc.docType = I.
Or select invoiceNo from table JobInvoice if AccountDoc.docType = M.
The problem is what if under same jobCode there have 2 docType which are M and I, how I gonna display these 2 invoices?
You can achieve this by using CTE and FOR XML. below is the sample code i created using similar tables you have -
Create table #AccountDoc (
id int ,
docType char(1),
docNo varchar(10)
)
Create table #JobInvoice (
id int ,
invoiceNo varchar(10)
)
insert into #AccountDoc
select 1 , 'M' ,'M1234'
union all select 2 , 'M' ,'M2345'
union all select 3 , 'M' ,'M3456'
union all select 4 , 'I' ,'I1234'
union all select 5 , 'I' ,'I2345'
union all select 6 , 'I' ,'I3456'
insert into #JobInvoice
select 1 , 'INV1234'
union all select 2 , 'INV2345'
union all select 3 , 'INV3456'
select *
from #AccountDoc t1 left join #JobInvoice t2
on t1.id = t2.id
with cte as
(
select isnull( case t1.docType WHEN 'M' THEN t2.invoiceNo WHEN 'I' then
t1.docNo end ,'') invoiceNo
from #AccountDoc t1 left join #JobInvoice t2
on t1.id = t2.id )
select invoiceNo + ',' from cte For XML PATH ('')
You need to pivot your data if you have situations where there are two rows, and you want two columns. Your sql is a bit messy, particularly the bit where you put an entire select statement inside a case when in the select part of another query. These two queries are virtually the same, you should look for a more optimal way of writing them. However, you can wrap your entire sql in the following:
select
Jobcode, shipmentsyskey, [M],[I]
from(
--YOUR ENTIRE SQL GOES HERE BETWEEN THESE BRACKETS. Do not alter anything else, just paste your entire sql here
) yoursql
pivot(
max(invoiceno)
for docType in([M],[I])
)pvt

SQL hierarchy count totals report

I'm creating a report with SQL server 2012 and Report Builder which must show the total number of Risks at a high, medium and low level for each Parent Element.
Each Element contains a number of Risks which are rated at a certain level. I need the total for the Parent Elements. The total will include the number of all the Child Elements and also the number the Element itself may have.
I am using CTEs in my query- the code I have attached isn't working (there are no errors - it's just displaying the incorrect results) and I'm not sure that my logic is correct??
Hopefully someone can help. Thanks in advance.
My table structure is:
ElementTable
ElementTableId(PK) ElementName ElementParentId
RiskTable
RiskId(PK) RiskName RiskRating ElementId(FK)
My query:
WITH cte_Hierarchy(ElementId, ElementName, Generation, ParentElementId)
AS (SELECT ElementId,
NAME,
0,
ParentElementId
FROM Extract.Element AS FirtGeneration
WHERE ParentElementId IS NULL
UNION ALL
SELECT NextGeneration.ElementId,
NextGeneration.NAME,
Parent.Generation + 1,
Parent.ElementId
FROM Extract.Element AS NextGeneration
INNER JOIN cte_Hierarchy AS Parent
ON NextGeneration.ParentElementId = Parent.ElementId),
CTE_HighRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS HighRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'High'
GROUP BY r.ElementId),
CTE_LowRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS LowRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'Low'
GROUP BY r.ElementId),
CTE_MedRisk
AS (SELECT r.ElementId,
Count(r.RiskId) AS MedRisk
FROM Extract.Risk r
WHERE r.RiskRating = 'Medium'
GROUP BY r.ElementId)
SELECT rd.ElementId,
rd.ElementName,
rd.ParentElementId,
Generation,
HighRisk,
MedRisk,
LowRisk
FROM cte_Hierarchy rd
LEFT OUTER JOIN CTE_HighRisk h
ON rd.ElementId = h.ElementId
LEFT OUTER JOIN CTE_MedRisk m
ON rd.ElementId = m.ElementId
LEFT OUTER JOIN CTE_LowRisk l
ON rd.ElementId = l.ElementId
WHERE Generation = 1
Edit:
Sample Data
ElementTableId(PK) -- ElementName -- ElementParentId
1 ------------------- Main --------------0
2 --------------------Element1-----------1
3 --------------------Element2 ----------1
4 --------------------SubElement1 -------2
RiskId(PK) RiskName RiskRating ElementId(FK)
a -------- Financial -- High ----- 2
b -------- HR --------- High ----- 3
c -------- Marketing -- Low ------- 2
d -------- Safety -----Medium ----- 4
Sample Output:
Element Name High Medium Low
Main ---------- 2 ---- 1 -------1
Here is your sample tables
SELECT * INTO #TABLE1
FROM
(
SELECT 1 ElementTableId, 'Main' ElementName ,0 ElementParentId
UNION ALL
SELECT 2,'Element1',1
UNION ALL
SELECT 3, 'Element2',1
UNION ALL
SELECT 4, 'SubElement1',2
)TAB
SELECT * INTO #TABLE2
FROM
(
SELECT 'a' RiskId, 'Fincancial' RiskName,'High' RiskRating ,2 ElementId
UNION ALL
SELECT 'b','HR','High',3
UNION ALL
SELECT 'c', 'Marketing','Low',2
UNION ALL
SELECT 'd', 'Safety','Medium',4
)TAB
We are finding the children of a parent, its count of High,Medium and Low and use cross join to show parent with all the combinations of its children's High,Medium and Low
UPDATE
The below variable can be used to access the records dynamically.
DECLARE #ElementTableId INT;
--SET #ElementTableId = 1
And use the above variable inside the query
;WITH CTE1 AS
(
SELECT *,0 [LEVEL] FROM #TABLE1 WHERE ElementTableId = #ElementTableId
UNION ALL
SELECT E.*,e2.[LEVEL]+1 FROM #TABLE1 e
INNER JOIN CTE1 e2 on e.ElementParentId = e2.ElementTableId
AND E.ElementTableId<>#ElementTableId
)
,CTE2 AS
(
SELECT E1.*,E2.*,COUNT(RiskRating) OVER(PARTITION BY RiskRating) CNT
from CTE1 E1
LEFT JOIN #TABLE2 E2 ON E1.ElementTableId=E2.ElementId
)
,CTE3 AS
(
SELECT DISTINCT T1.ElementName,C2.RiskRating,C2.CNT
FROM #TABLE1 T1
CROSS JOIN CTE2 C2
WHERE T1.ElementTableId = #ElementTableId
)
SELECT *
FROM CTE3
PIVOT(MIN(CNT)
FOR RiskRating IN ([High], [Medium],[Low])) AS PVTTable
SQL FIDDLE
RESULT
UPDATE 2
I am updating as per your new requirement
Here is sample table in which I have added extra data to test
SELECT * INTO #ElementTable
FROM
(
SELECT 1 ElementTableId, 'Main' ElementName ,0 ElementParentId
UNION ALL
SELECT 2,'Element1',1
UNION ALL
SELECT 3, 'Element2',1
UNION ALL
SELECT 4, 'SubElement1',2
UNION ALL
SELECT 5, 'Main 2',0
UNION ALL
SELECT 6, 'Element21',5
UNION ALL
SELECT 7, 'SubElement21',6
UNION ALL
SELECT 8, 'SubElement22',7
UNION ALL
SELECT 9, 'SubElement23',7
)TAB
SELECT * INTO #RiskTable
FROM
(
SELECT 'a' RiskId, 'Fincancial' RiskName,'High' RiskRating ,2 ElementId
UNION ALL
SELECT 'b','HR','High',3
UNION ALL
SELECT 'c', 'Marketing','Low',2
UNION ALL
SELECT 'd', 'Safety','Medium',4
UNION ALL
SELECT 'e' , 'Fincancial' ,'High' ,5
UNION ALL
SELECT 'f','HR','High',6
UNION ALL
SELECT 'g','HR','High',6
UNION ALL
SELECT 'h', 'Marketing','Low',7
UNION ALL
SELECT 'i', 'Safety','Medium',8
UNION ALL
SELECT 'j', 'Safety','High',8
)TAB
I have written the logic in query
;WITH CTE1 AS
(
-- Here you will find the level of every elements in the table
SELECT *,0 [LEVEL]
FROM #ElementTable WHERE ElementParentId = 0
UNION ALL
SELECT ET.*,CTE1.[LEVEL]+1
FROM #ElementTable ET
INNER JOIN CTE1 on ET.ElementParentId = CTE1.ElementTableId
)
,CTE2 AS
(
-- Filters the level and find the major parant of each child
-- ie, 100->150->200, here the main parent of 200 is 100
SELECT *,CTE1.ElementTableId MajorParentID,CTE1.ElementName MajorParentName
FROM CTE1 WHERE [LEVEL]=1
UNION ALL
SELECT CTE1.*,CTE2.MajorParentID,CTE2.MajorParentName
FROM CTE1
INNER JOIN CTE2 on CTE1.ElementParentId = CTE2.ElementTableId
)
,CTE3 AS
(
-- Since each child have columns for main parent id and name,
-- you will get the count of each element corresponding to the level you have selected directly
SELECT DISTINCT CTE2.MajorParentName,RT.RiskRating ,
COUNT(RiskRating) OVER(PARTITION BY MajorParentID,RiskRating) CNT
FROM CTE2
JOIN #RiskTable RT ON CTE2.ElementTableId=RT.ElementId
)
SELECT MajorParentName, ISNULL([High],0)[High], ISNULL([Medium],0)[Medium],ISNULL([Low],0)[Low]
FROM CTE3
PIVOT(MIN(CNT)
FOR RiskRating IN ([High], [Medium],[Low])) AS PVTTable
SQL FIDDLE