join 2 tables in oracle sql - sql

Here is the configuration I am starting with:
DROP TABLE ruleset1;
CREATE TABLE ruleset1 (id int not null unique,score_rule1 float default 0.0,score_rule2 float default 0.0,score_rule3 float default 0.0);
DROP TABLE ruleset2;
CREATE TABLE ruleset2 (id int not null unique,score_rule1 float default 0.0,score_rule2 float default 0.0,score_rule3 float default 0.0);
insert into ruleset1 (id, score_rule1, score_rule2, score_rule3) values (0,0.8,0,0);
insert into ruleset1 (id, score_rule1, score_rule2, score_rule3) values (1,0,0.1,0);
insert into ruleset2 (id, score_rule1, score_rule2, score_rule3) values (0,0,0,0.3);
insert into ruleset2 (id, score_rule1, score_rule2, score_rule3) values (2,0,0.2,0);
what I have is this now is 2 tables
ruleset1:
| ID | SCORE_RULE1 | SCORE_RULE2 | SCORE_RULE3
================================================
| 0 | 0.8 | 0 | 0
| 1 | 0 | 0.1 | 0
and ruleset2:
| ID | SCORE_RULE1 | SCORE_RULE2 | SCORE_RULE3
================================================
| 0 | 0 | 0 | 0.3
| 2 | 0 | 0.2 | 0
and I want to outer join them and calculate the mean of non zero columns, like this:
| ID | Average
================
| 0 | 0.55
| 1 | 0.1
| 2 | 0.2
My current query is:
select * from ruleset1 full outer join ruleset2 on ruleset1.id = ruleset2.id;
which gives an ugly result:
| ID | SCORE_RULE1 | SCORE_RULE2 | SCORE_RULE3 | ID | SCORE_RULE1 | SCORE_RULE2 | SCORE_RULE3
============================================================================================
| 0 | .8 | 0 | 0 | 0 | 0 | 0 | .3
| - | - | - | - | 2 | 0 | .2 | 0
| 1 | 0 | .1 | 0 | - | - | - | -
Can anyone help with a better query please?
Thank you very much!

Of course avg doesn't ignore zeroes, only NULLs, thus NULLIF(column, 0) could be used.
But as you got denormalized data you can simply normalize it on-the-fly:
select id, avg(score)
from
(
select id, score_rule1 score
from ruleset1 where score_rule1 <> 0
union all
select id, score_rule2 from ruleset1 where score_rule2 <> 0
union all
select id, score_rule3 from ruleset1 where score_rule3 <> 0
union all
select id, score_rule1 from ruleset2 where score_rule1 <> 0
union all
select id, score_rule2 from ruleset2 where score_rule2 <> 0
union all
select id, score_rule3 from ruleset2 where score_rule3 <> 0
) dt
group by id;
To avoid five Unions you could use only one and do some additional logic:
select id, sum(score) / sum(score_count)
from
(
select id, score_rule1 + score_rule2 + score_rule3 score,
case when score_rule1 = 0 then 0 else 1 end +
case when score_rule2 = 0 then 0 else 1 end +
case when score_rule3 = 0 then 0 else 1 end score_count
from ruleset1
union all
select id, score_rule1 + score_rule2 + score_rule3 score,
case when score_rule1 = 0 then 0 else 1 end +
case when score_rule2 = 0 then 0 else 1 end +
case when score_rule3 = 0 then 0 else 1 end score_count
from ruleset2
) dt
group by id;
This assumes there are no NULLs in the core_rule columns.

Here's an example with PostgreSQL that you could adapt with Oracle (sorry, SQLFiddle's Oracle isn't cooperating). Thanks to Juan Carlos Oropeza's suggestion, the code below runs on Oracle well: http://rextester.com/DVP59353
select
r.id,
sum(coalesce(r1.score_rule1,0) +
coalesce(r1.score_rule2,0) +
coalesce(r1.score_rule3,0) +
coalesce(r2.score_rule1,0) +
coalesce(r2.score_rule2,0) +
coalesce(r2.score_rule3,0)
)
/
sum(case when coalesce(r1.score_rule1,0) <> 0 then 1 else 0 end +
case when coalesce(r1.score_rule2,0) <> 0 then 1 else 0 end +
case when coalesce(r1.score_rule3,0) <> 0 then 1 else 0 end +
case when coalesce(r2.score_rule1,0) <> 0 then 1 else 0 end +
case when coalesce(r2.score_rule2,0) <> 0 then 1 else 0 end +
case when coalesce(r2.score_rule3,0) <> 0 then 1 else 0 end) as Average
from
(select id from ruleset1
union
select id from ruleset2) r
left join ruleset1 r1 on r.id = r1.id
left join ruleset2 r2 on r.id = r2.id
group by r.id
SQLFiddle with PostgreSQL version is here: http://sqlfiddle.com/#!15/24e3f/1.
This example combines id from both tables using a union. Doing so allows the same ID in both ruleset1 and ruleset2 to appear just once in the result. r is an alias given to this generated table.
All the ids are then left joined with both tables. During the summation process, it is possible that the NULL values resulting from left join may impact the result. So the NULLs are coalesced to zero in the math.

dnoeth is the easy and clean answer.
here I was just playing with COALESCE and NVL2
select COALESCE(r.ID, s.ID),
COALESCE(r.score_rule1, 0) +
COALESCE(r.score_rule2, 0) +
COALESCE(r.score_rule3, 0) +
COALESCE(s.score_rule1, 0) +
COALESCE(s.score_rule2, 0) +
COALESCE(s.score_rule3, 0) as sum,
NVL2(r.score_rule1, 0, 1) +
NVL2(r.score_rule2, 0, 1) +
NVL2(r.score_rule3, 0, 1) +
NVL2(s.score_rule1, 0, 1) +
NVL2(s.score_rule2, 0, 1) +
NVL2(s.score_rule3, 0, 1) as tot
from ruleset1 r
full outer join ruleset2 s
on ruleset1.id = ruleset2.id;
Then your avg is sum/tot

union all your two tables, unpivot, change the zeros into null with nullif, and use standard avg() aggregate function:
select id, avg(nullif(value, 0)) as avg_value from (
select * from ruleset1
union all
select * from ruleset2
)
unpivot ( value for column_name in (score_rule1, score_rule2, score_rule3))
group by id
order by id
;
ID AVG_VALUE
---------- ----------
0 .55
1 .1
2 .2

SELECT s.id, AVG(s.score)
FROM(
SELECT id,score_rule1+score_rule2+score_rule3 as score
FROM ruleset2
UNION ALL
SELECT id,(score_rule1+score_rule2+score_rule3) as score
FROM ruleset1) s
group by s.id

Related

SQL Query for below scenario for multiple rows

Need response as per expected result column in attached image.
The row filtration is required in multiple rows
The rule is (x.attr2 = '1' AND x.attr3 = '1') AND (x.attr2='' AND x.attr3='2') then its expected column value is true but all other conditions its false
Its MS SQL
Key Atr2 Atr3 expected result
111 1 1 TRUE
111 2 2
112 1 4 FALSE
113 1 4 FALSE
113 2 2
114 1 1 FALSE
Check the below script-
IF OBJECT_ID('[Sample]') IS NOT NULL
DROP TABLE [Sample]
CREATE TABLE [Sample]
(
[Key] INT NOT NULL,
Attr1 INT NOT NULL,
Attr2 INT NOT NULL,
Attr3 INT NOT NULL
)
GO
INSERT INTO [Sample] ([Key],Attr1,Attr2,Attr3)
VALUES (111,62,1,1),
(111,62,2,2),
(112,62,1,4),
(113,62,1,4),
(113,62,2,2),
(114,62,1,1)
--EXPECTED_RESULT:
SELECT S.*,CASE WHEN T.[KEY] IS NOT NULL THEN 'TRUE' ELSE 'FALSE' END AS Expected_Result
FROM [Sample] S LEFT JOIN
(
SELECT T.[KEY] FROM
(
SELECT x.*,
ROW_NUMBER() OVER( PARTITION BY x.[KEY],x.attr1 ORDER BY x.attr2,x.attr3) AS r_no
--,CASE WHEN (x.attr2 = 1 AND x.attr3 = 1) OR (x.attr2 = 2 AND x.attr3 = 2)
--then 'TRUE' else 'FALSE' end as expected_result
FROM [Sample] x WHERE x.attr2=x.attr3
) T WHERE T.r_no>1
) T ON S.[KEY]=T.[KEY]
This query:
select key from tablename
group by key
having sum(case when atr2 = '1' and atr3 = '1' then 1 else 0 end) > 0
and sum(case when atr2 = '2' and atr3 = '2' then 1 else 0 end) > 0
and count(*) = 2
uses conditional aggregation to find the keys for which the result should be true.
So join it to the table like this:
select t.*,
case when g.[key] is null then 'FALSE' else 'TRUE' end result
from tablename t left join (
select [key] from tablename
group by [key]
having sum(case when atr2 = '1' and atr3 = '1' then 1 else 0 end) > 0
and sum(case when atr2 = '2' and atr3 = '2' then 1 else 0 end) > 0
and count(*) = 2
) g on g.[key] = t.[key]
See the demo.
Results:
> Key | Atr2 | Atr3 | result
> --: | ---: | ---: | :-----
> 111 | 1 | 1 | TRUE
> 111 | 2 | 2 | TRUE
> 112 | 1 | 4 | FALSE
> 113 | 1 | 4 | FALSE
> 113 | 2 | 2 | FALSE
> 114 | 1 | 1 | FALSE

Select only the "most complete" record

I need to solve the following problem.
Let's suppose I have a table with 4 fields called a, b, c, d.
I have the following records:
-------------------------------------
a | b | c | d
-------------------------------------
1 | 2 | | row 1
1 | 2 | 3 | 4 row 2
1 | 2 | | 4 row 3
1 | 2 | 3 | row 4
As it's possible to observe, rows 1,3,4 are "sub-records" of row 2.
What I would like to do is, to extract only 2nd row.
Could you help me please?
Thanks in advance for the answer
EDIT: I need to be more specific.
I could have also the cases:
-------------------------------------
a | b | c | d
-------------------------------------
1 | 2 | | row 1
1 | 2 | | 4 row 2
1 | | | 4 row 3
where I need to extract the 2nd row,
-------------------------------------
a | b | c | d
-------------------------------------
1 | 2 | | row 1
1 | 2 | 3 | row 2
1 | | 3 | row 3
and again I need to extract the 2nd row.
Same for couples,
a | b | c | d
-------------------------------------
1 | | | row 1
1 | | 3 | row 2
| | 3 | row 3
and so on for the other examples.
(Of course, it's now always 2nd row)
Using a NOT EXISTS the records that have a better duplicate can be filtered out.
create table abcd (
a int,
b int,
c int,
d int
);
insert into abcd (a, b, c, d) values
(1, 2, null, null)
,(1, 2, 3, 4)
,(1, 2, null, 4)
,(1, 2, 3, null)
,(2, 3, null,null)
,(2, 3, null, 5)
,(2, null, null, 5)
,(3, null, null, null)
,(3, null, 5, null)
,(null, null, 5, null)
SELECT *
FROM abcd AS t
WHERE NOT EXISTS
(
select 1
from abcd as d
where (t.a is null or d.a = t.a)
and (t.b is null or d.b = t.b)
and (t.c is null or d.c = t.c)
and (t.d is null or d.d = t.d)
and (case when t.a is null then 0 else 1 end +
case when t.b is null then 0 else 1 end +
case when t.c is null then 0 else 1 end +
case when t.d is null then 0 else 1 end) <
(case when d.a is null then 0 else 1 end +
case when d.b is null then 0 else 1 end +
case when d.c is null then 0 else 1 end +
case when d.d is null then 0 else 1 end)
);
a | b | c | d
-: | ---: | ---: | ---:
1 | 2 | 3 | 4
2 | 3 | null | 5
3 | null | 5 | null
db<>fiddle here
You will need to compute a "completion index" for each row. In the example you provided, you might use something along the lines of:
(CASE WHEN a IS NULL THEN 0 ELSE 1) +
(CASE WHEN b IS NULL THEN 0 ELSE 1) +
(CASE WHEN c IS NULL THEN 0 ELSE 1) +
(CASE WHEN d IS NULL THEN 0 ELSE 1) AS CompletionIndex
Then SELECT the top 1 ordered by CompletionIndex in descending order.
This is obviously not very scalable across a large number of columns. But if you have a large number of sparsely populated columns you might consider a row-based rather than column-based structure for your data. That design would make it much easier to count the number of non-NULL values for each entity.
Most complete rows, by your definition, are the ones with the least null columns:
SELECT * FROM tablename
WHERE (
(CASE WHEN a IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN b IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN c IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN d IS NULL THEN 0 ELSE 1 END)
) =
(SELECT MAX(
(CASE WHEN a IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN b IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN c IS NULL THEN 0 ELSE 1 END) +
(CASE WHEN d IS NULL THEN 0 ELSE 1 END))
FROM tablename)
Hmmm . . . I think you can use not exists:
with t as (
select t.*, row_number() over (order by a) as id
from t
)
select t.*
from t
where not exists (select 1
from t t2
where ((t2.a is not distinct from t.a or t2.a is not null and t.a is null) and
(t2.b is not distinct from t.b or t2.b is not null and t.b is null) and
(t2.c is not distinct from t.c or t2.c is not null and t.c is null) and
(t2.d is not distinct from t.d or t2.d is not null and t.d is null)
) and
t2.id <> t.id
);
The logic is that no more specific row exists, where the values match
Here is a db<>fiddle.
As mentioned by Gordon Linoff, we do have to use something like not exists too,
Edit Using EXCEPT helps
This might work...
SELECT * from table1
EXCEPT
(
SELECT t1.*
FROM table1 t1
JOIN table1 t2
ON COALESCE(t1.a, t2.a, -1) = COALESCE(t2.a, -1)
AND COALESCE(t1.b, t2.b, -1) = COALESCE(t2.b, -1)
AND COALESCE(t1.c, t2.c, -1) = COALESCE(t2.c, -1)
AND COALESCE(t1.d, t2.d, -1) = COALESCE(t2.d, -1)
)
Here, t1 is every subset row.
Note: We are assuming value -1 as sentinel value and it does not occur in any column.

In T-SQL, What is the best way to find % of male customers by area

Support I have a table with area, customer and customer's sex info and I want to find out % of male customers in each area. Whats the best way to come up with that?
create table temp(area_id varchar(10),customer_id varchar(10),customer_sex varchar(10))
insert into temp select 1,1,'male'
insert into temp select 1,1,'male'
insert into temp select 1,1,'female'
insert into temp select 1,1,'female'
insert into temp select 2,1,'male'
insert into temp select 2,1,'female'
insert into temp select 2,1,'female'
insert into temp select 3,1,'male'
insert into temp select 3,1,'female'
insert into temp select 4,1,'male'
insert into temp select 5,1,'female'
select * from temp
The result should be like below:
; WITH x AS
(
select
area_id
, count(*) AS total_customers
, SUM(CASE WHEN customer_sex = 'male' THEN 1 ELSE 0 END) AS total_male_customers
FROM temp
GROUP BY area_id
)
SELECT
area_id
, total_customers
, total_male_customers
, CASE WHEN total_male_customers > 0 THEN CAST( (total_male_customers * 100.0) / total_customers AS DECIMAL(6,2)) ELSE 0 END AS Male_percentage
From x
Group by and case will provide your results:
SELECT area_id, count(customer_id) as Total_Customers, Total_Male_Customers = sum(case when customer_sex = 'male' then 1 else 0 end),
Format(sum(case when customer_sex = 'male' then 1 else 0 end)/(count(customer_id)*1.0),'P') as MaleCustomers
FROM dbo.temp
GROUP BY area_id
HAVING sum(case when customer_sex = 'male' then 1 else 0 end) > 0
Here if it is smaller dataset format is better else it has performance issues you can go with custom multiplication and concatenating % symbol.
Output as below:
+---------+-----------------+----------------------+---------------+
| area_id | Total_Customers | Total_Male_Customers | MaleCustomers |
+---------+-----------------+----------------------+---------------+
| 1 | 4 | 2 | 50.00 % |
| 2 | 3 | 1 | 33.33 % |
| 3 | 2 | 1 | 50.00 % |
| 4 | 1 | 1 | 100.00 % |
+---------+-----------------+----------------------+---------------+
Use IIF (Sqlserver 2012+) otherwise CASE, group by and sum of males /count all customers * 100
+ 0.0 to treat sum of males and all customer as float or decimal to get the correct result.
select area_id,count(customer_id) [Total Customers],
sum(iif(customer_sex='male',1,0)) [Total Males],
cast(cast(((sum(iif(customer_sex='male',1,0)) + 0.0) / (count(customer_sex) + 0.0)) * 100 as decimal(18,1)) as varchar(10)) + '%' [percentage of males]
from temp
group by area_id
This will do:
select x.area_id, x.total, x.m, cast(CONVERT(DECIMAL(10,2), x.m * 100.0 / x.total) as nvarchar(max)) + '%'
from
(
select t.area_id, count(1) total, sum(iif(t.customer_sex = 'male', 1, 0)) m
from #temp t
group by t.area_id
)x

How do I compute the difference between consecutive rows for a histogram?

I'm trying to create a histogram from some data. SQL Server Developer 2014
Data structure:
+-------------Simulations------------+
+ ID | Cycle | Xa | nextCycleShort +
+ 0 | 0 | 5.63 | True +
+ 0 | 1 | 11.45 | False +
+ 0 | 2 | 12.3 | True +
+-Parameters-+
+ ID | CR +
+ 0 | 1 +
+ 1 | 2 +
In array notation, I want a table with something like:
(Xa[i + 1] - Xa[i])*(CASE nextCycleShort[i] WHEN 0 THEN 1.0 ELSE 2.0) AS DIFF
From this table, I want to select the COUNT(CAST(DIFF as int)). And I want to group that by CAST(DIFF as INT),Parameters.CR.
So for each CR, I'll be able to make a histogram of the DIFFs. What does this look like? Here's my attempt at it:
SELECT
p.ControlRange as ControlRange,
CAST(DIFF as int) as XaMinusXb,
Count(DIFF) as total_diffs,
Select q.Xnew FROM
(SELECT Top 1 Xa AS Xnew
FROM Simulations t
WHERE t.ExperimentID = s.ExperimentID AND t.CycleCount > s.CycleCount
ORDER BY CycleCount DESC) q,
(q.Xnew - s.Xa)*(CASE WHEN s.nextCycleShort = 0 then 1.0 ELSE 2.0) AS DIFF
FROM Simulations s join Parameters p
GROUP BY CAST(DIFF as int), p.ControlRange
ORDER by p.controlRange ASC, DIFF ASC
on s.ExperimentID = p.ExperimentID
Just a thought to do it like this. every row looks back to the previous Xa. You can see how we can get simple diff and also the case based multiplier DIFF:
select
p.CR, s.Xa,
lag(s.Xa) over (partition by p.CR order by cycle asc) prev_Xa,
s.Xa - lag(s.Xa) over (partition by p.CR order by cycle asc) diff,
case when nextCycleShort = 'False'
then 1.0
else 2.0
end nextCyleShort_int,
(s.Xa - lag(s.Xa) over (partition by p.CR order by cycle asc)) * (case when nextCycleShort = 'False' then 1.0 else 2.0 end) myDIFF
from
(
select 0 ID, 0 Cycle, 5.63 Xa , 'True' nextCycleShort union
select 0 ID, 1 Cycle, 11.45 Xa , 'False' nextCycleShort union
select 0 ID, 2 Cycle, 12.3 Xa , 'True' nextCycleShort
) s
join
(
select 0 ID, 1 CR union
select 1 ID, 2 CR
) p
on s.ID = p.ID

Aggregative sum of objects belonging to objects residing inside hierarchy structure

My problem is similar in a way to this one, yet different enough in my understanding.
I have three tables:
Units ([UnitID] int, [UnitParentID] int)
Students ([StudentID] int, [UnitID] int)
Events ([EventID] int, [EventTypeID] int, [StudentID] int)
Students belong to units, units are stacked in a hierarchy (tree form - one parent per child), and each student can have events of different types.
I need to sum up the number of events of each type per user, then aggregate for all users in a unit, then aggregate through hierarchy until I reach the mother of all units.
The result should be something like this:
My tools are SQL Server 2008 and Report Builder 3.
I put up a SQL fiddle with sample data for fun.
Use this query:
;WITH CTE(Id, ParentId, cLevel, Title, ord) AS (
SELECT
u.UnitID, u.UnitParentID, 1,
CAST('Unit ' + CAST(ROW_NUMBER() OVER (ORDER BY u.UnitID) AS varchar(3)) AS varchar(max)),
CAST(RIGHT('000' + CAST(ROW_NUMBER() OVER (ORDER BY u.UnitID) AS varchar(3)), 3) AS varchar(max))
FROM
dbo.Units u
WHERE
u.UnitParentID IS NULL
UNION ALL
SELECT
u.UnitID, u.UnitParentID, c.cLevel + 1,
c.Title + '.' + CAST(ROW_NUMBER() OVER (PARTITION BY c.cLevel ORDER BY c.Id) AS varchar(3)),
c.ord + RIGHT('000' + CAST(ROW_NUMBER() OVER (ORDER BY u.UnitID) AS varchar(3)), 3)
FROM
dbo.Units u
JOIN
CTE c ON c.Id = u.UnitParentID
WHERE
u.UnitParentID IS NOT NULL
), Units AS (
SELECT
u.Id, u.ParentId, u.cLevel, u.Title, u.ord,
SUM(CASE WHEN e.EventTypeId = 1 THEN 1 ELSE 0 END) AS EventA,
SUM(CASE WHEN e.EventTypeId = 2 THEN 1 ELSE 0 END) AS EventB,
SUM(CASE WHEN e.EventTypeId = 3 THEN 1 ELSE 0 END) AS EventC,
SUM(CASE WHEN e.EventTypeId = 4 THEN 1 ELSE 0 END) AS EventD
FROM
CTE u
LEFT JOIN
dbo.Students s ON u.Id = s.UnitId
LEFT JOIN
dbo.[Events] e ON s.StudentId = e.StudentId
GROUP BY
u.Id, u.ParentId, u.cLevel, u.Title, u.ord
), addStudents AS (
SELECT *
FROM Units
UNION ALL
SELECT
s.StudentId, u.Id, u.cLevel + 1,
'Student ' + CAST(s.StudentId AS varchar(3)),
u.ord + RIGHT('000' + CAST(s.StudentId AS varchar(3)), 0),
SUM(CASE WHEN e.EventTypeId = 1 THEN 1 ELSE 0 END),
SUM(CASE WHEN e.EventTypeId = 2 THEN 1 ELSE 0 END),
SUM(CASE WHEN e.EventTypeId = 3 THEN 1 ELSE 0 END),
SUM(CASE WHEN e.EventTypeId = 4 THEN 1 ELSE 0 END)
FROM Units u
JOIN
dbo.Students s ON u.Id = s.UnitId
LEFT JOIN
dbo.[Events] e ON s.StudentId = e.StudentId
GROUP BY
s.StudentID, u.ID, u.cLevel, u.ord
)
SELECT --TOP(10)
REPLICATE(' ', cLevel) + Title As Title,
EventA, EventB, EventC, EventD
FROM
addStudents
ORDER BY
ord
For this:
Title | EventA | EventB | EventC | EventD
-----------------+--------+---------+--------+--------
Unit 1 | 0 | 1 | 0 | 0
Student 6 | 0 | 1 | 0 | 0
Unit 1.1 | 0 | 0 | 0 | 1
Student 21 | 0 | 0 | 0 | 1
Student 33 | 0 | 0 | 0 | 0
Unit 1.1.1 | 0 | 0 | 0 | 0
Student 23 | 0 | 0 | 0 | 0
Unit 1.1.1.1 | 3 | 2 | 3 | 0
Student 10 | 0 | 0 | 0 | 0
Student 17 | 1 | 0 | 0 | 0
...
SQL Fiddle Demo
Do you need also the hierarchy to be sorted / visualized? At least this will calculate the sums, but the order of the data is pretty random :)
;with CTE as (
select S.StudentId as UnitID, S.UnitId as UnitParentID,
S.StudentID, 'Student' as Type
from Students S
union all
select U.UnitId, U.UnitParentId,
CTE.StudentId as StudentID, 'Unit ' as Type
from
Units U
join CTE
on U.UnitId = CTE.UnitParentId
)
select C.Type + ' ' + convert(varchar, C.UnitId),
sum(case when EventTypeId = 1 then 1 else 0 end) as E1,
sum(case when EventTypeId = 2 then 1 else 0 end) as E2,
sum(case when EventTypeId = 3 then 1 else 0 end) as E3,
sum(case when EventTypeId = 4 then 1 else 0 end) as E4
from
CTE C
left outer join events E on C.StudentId = E.StudentId
group by
C.Type, C.UnitId
SQL Fiddle
If you need also the hierarchy to be in order, you'll probably have add few extra CTEs to get the numbering from top down with something like #shA.t did. This gathers the hierarchy separately for each student, so it's not really possible to add the level numbers in a simple way.