Repeated use of an alias within a join - sql

I am trying to append a column created by using COUNT(*) and GROUP BY to the original selection that was counted. However, the selection is pretty complex (much more than the WHERE ... line I've included in my example) so I'd rather not duplicate the code.
SQL Server doesn't approve of my using an alias t1 inside of the left join statement. Any suggestions?
select t1.school, t1.grade,t1.individual,t2.cnt as 'class size' from (
select * from students
where (students.age < 16 and students.ACT_score is not null)
) as t1
left join (
select distinct school, grade, count(*) as 'cnt' from t1
group by school, grade
) as t2
on t1.school = t2.school and t1.grade = t2.grade

If it's 2005 or newer, use a CTE:
;WITH MyCTE AS
(
<Your complicate query here>
)
SELECT fields
FROM MyCTE
JOIN (subquery referencing MyCTE)
...

It might be easier to maintain if you use COUNT(*) with an OVER clause as follows:
with cntAppended as (
select
*,
count(*) over (partition by school, grade)
from students
)
select
school,
grade,
individual,
cnt as "class size"
from cntAppended
where (age < 16 and ACT_score is not null)
Don't be tempted to avoid WITH and putting the WHERE clause in one query with COUNT. If you do, you will only count students in each school and grade who are < 16 and have ACT scores. It looks like you want to count all students for the [class size] column, but only see data for certain ones in your result.
If and when T-SQL supports the QUALIFY keyword, queries like this may be even easier:
select
school,
grade,
individual,
count(*) over (partition by school, grade) as "class size"
from students
QUALIFY (age < 16 and ACT_score is not null)

I rewrote your query in a simpler form, no CTE needed:
SELECT t1.school
,t1.grade
,t1.individual
,t2.cnt AS 'class size'
FROM students t1
LEFT JOIN (
SELECT school
,grade
,count(*) AS 'cnt'
FROM students
GROUP BY school, grade
) AS t2 ON t1.school = t2.school
AND t1.grade = t2.grade
WHERE t1.age < 16
AND t1.ACT_score IS NOT NULL

Related

How many types of SQL subqueries are there?

In an effort to understand what types of subqueries can be correlated I wrote the SQL query shown below. It shows all types of subqueries I can think of a SQL select statement can include.
Though the example shown below runs in Oracle 12c, I would prefer to keep this question database agnostic. In the example below I included all 7 types of subqueries I can think of:
with
daily_login as ( -- 1. Independent CTE [XN]
select user_id, trunc(login_time) as day, count(*) from shopper_login
group by user_id, trunc(login_time)
),
frequent_user as ( -- 2. Dependent CTE [XN]
select user_id, count(*) as days from daily_login group by user_id
having count(*) >= 2
),
referrer (frequent_id, id, rid, ref_level) as ( -- 3. Recursive CTE [XN]
select fu.user_id, s.id, s.ref_id, 1 from frequent_user fu
join shopper s on fu.user_id = s.id
union all
select r.frequent_id, s.id, s.ref_id, r.ref_level + 1 from referrer r
join shopper s on s.id = r.rid
)
select s.id, s.name, r.id as original_referrer,
( -- 4. Scalar Subquery [CN]
select max(login_time) from shopper_login l
where l.user_id = s.id and l.success = 1
) as last_login,
m.first_login
from shopper s
join referrer r on r.frequent_id = s.id
join ( -- 5. Table Expression / Inline View / Derived Table [XN]
select user_id, min(login_time) first_login from shopper_login
where success = 1 group by user_id
) m on m.user_id = s.id
where r.rid is null
and s.id not in ( -- 6. Traditional Subquery [CN]
select user_id from persona
where description = 'Fashionista'
and id in ( -- 7. Nested subquery [CN]
select user_id from users where region = 'NORTH')
);
Legend:
[C]: Can be correlated
[X]: Cannot be corretaled
[N]: Can include nested subqueries
My questions are:
Did I get all possible types? Are there alternative names for these types of subqueries?
Am I correct thinking that only Scalar (#4), Traditional (#6), and Nested (#7) subqueries can be correlated?
Am I correct thinking Table Expressions and CTEs (#1, #2, and #3) cannot be correlated? (however, they can include Nested subqueries that can be correlated)
Correlated subquery:
FROM shopper s
...
AND EXISTS (SELECT *
FROM otherTable t
WHERE t.id = s.id)

Get Limited Records For Sub Query Each Result

I am trying to query some data from SQL Server 2012 using sub query. I am trying to get first 3 records for each Id returned by Sub Query but I am not getting the idea how to do so for now I write this query:
Select * from Student Where TeacherId in (Select TeacherId from Teacher)
I am not sure if this is achievable by using such query or do I have to write a function or any thing else ?
Any Suggestions would be great and sorry for my bad explanation skills.
You should join the Teacher to Student table, and then use an analytic function to get the first there records for each teacher:
SELECT *
FROM
(
SELECT
s.*, t.TeacherId, t.TeacherName,
ROW_NUMBER() OVER (PARTITION BY t.TeacherId ORDER BY some_col) rn
FROM Teacher t
INNER JOIN Student s
ON t.TeacherId = s.TeacherId
) t
WHERE rn = 3;
I assume that there is a column in one of the two tables some_col which you want to use for ordering. It does not make much sense to speak of the first three records without also defining some ordering.
I guess you want the top 3 rows for each ids, not top 3 rows for entire result set
WITH CTE AS
(
SELECT *,
ROW_NUMBER() OVER (PARTITION BY t.TeacherId ORDER BY ?) Seq
FROM Student s
INNER JOIN Teacher t ON t.TeacherId = s.TeacherId
)
SELECT * FROM CTE
WHERE Seq between 1 AND 3
? placeholder requires column_name to generate the sequence_number to get the result with boundary of 1 to 3 .

Combining FREQUENCY count and INNER CASE in SQL

How do I combine both in order to show the student name (sname) and section number (sectno) for a class that has more than 6 student? So far I have this
SELECT student.sname, enroll.sectno,
FROM student
INNER JOIN enroll
ON student.sid=enroll.sid
with
SELECT grade,
COUNT(grade)AS Frequency
FROM enroll
GROUP BY grade
HAVING COUNT(grade)>6
looks like you were very close. I think the below should work for you:
SELECT
student.sname,
enroll.sectno,
COUNT(enroll.grade) AS Frequency
FROM student
INNER JOIN enroll ON student.sid=enroll.sid
GROUP BY student.sname, enroll.sectno
HAVING COUNT(enroll.grade)>6
In your 2md Select there's no relation to a class that has more than 6 student, IMHO the GROUP BY should be based on column like classid. And then you can simply combine both using a Windowed Aggregate:
with cte as
(
SELECT student.sname, enroll.sectno,
-- get the count per class, might be a different column than sectno
count(*) over (partition by enroll.sectno) as cnt
FROM student
INNER JOIN enroll
ON student.sid=enroll.sid
)
select * from cte
where cnt > 6

Returning the Min() of a Count()

I am studying for an SQL test and the previous year has the final question:
Name the student who has studied the least number of papers. How many
papers have they studied?
So far, this is the select query that I have created:
select min(Full_Name), min(Amount)
from (select st.ST_F_Name & ' ' & st.ST_L_Name as Full_Name, count(*) as Amount
from (student_course as sc
inner join students as st
on st.ST_ID=sc.SC_ST_ID)
group by st.ST_F_Name & ' ' & st.ST_L_Name)
This works perfectly for returning the result I want but I'm not sure if this is the way I should be doing this query? I feel like calling min() on the Full_Name could potentially backfire on me under certain circumstances. Is there a better way to be doing this? (this is in MS Access for unknown reasons)
If you want only 1 of such students if there are multiple, this is probably the simplest:
select st.ST_F_Name, st.ST_L_Name, count(*) as Amount
from student_course as sc
inner join students as st
on st.ST_ID=sc.SC_ST_ID
group by st.ST_ID
order by Amount ASC LIMIT 1
However, if you want to find all stuch students, you follow a different approach. We use a WITH clause to simplify things, that defines a CTE (Common Table Expression) computing the number of courses per-student. And then we select students where their number equals to the minimum in that CTE:
with per_student as (
select st.ST_F_Name, st.ST_L_Name, count(*) as Amount
from student_course as sc
inner join students as st
on st.ST_ID=sc.SC_ST_ID
group by st.ST_ID
)
select * from per_student
where amount = (select min(amount) from per_student)
But the real trick in that question is that there might be students that didn't take ANY courses. But with approaches presented so far you'll never see them. You want something like this:
with per_student as (
select st.ST_F_Name, st.ST_L_Name, count(sc.SC_ST_ID) as Amount
from student_course as sc
right outer join students as st
on st.ST_ID=sc.SC_ST_ID
group by st.ST_ID
)
select * from per_student
where amount = (select min(amount) from per_student)
You can order by count(*) to get the student with the least # of papers:
i.e.
select * from students where st_id in (
select top 1 sc_st_id
from student_course
group by sc_st_id
order by count(*)
)
if you also need the # of papers studied, then join a derived table containing the min count:
select * from students s
left join (
select top 1 sc_st_id, count(*)
from student_course
group by sc_st_id
order by count(*)
) t on t.sc_st_id = s.st_id

SQL Query Help Part 2 - Add filter to joined tables and get max value from filter

I asked this question on SO. However, I wish to extend it further. I would like to find the max value of the 'Reading' column only where the 'state' is of value 'XX' for example.
So if I join the two tables, how do I get the row with max(Reading) value from the result set. Eg.
SELECT s.*, g1.*
FROM Schools AS s
JOIN Grades AS g1 ON g1.id_schools = s.id
WHERE s.state = 'SA' // how do I get row with max(Reading) column from this result set
The table details are:
Table1 = Schools
Columns: id(PK), state(nvchar(100)), schoolname
Table2 = Grades
Columns: id(PK), id_schools(FK), Year, Reading, Writing...
I'd think about using a common table expression:
WITH SchoolsInState (id, state, schoolname)
AS (
SELECT id, state, schoolname
FROM Schools
WHERE state = 'XX'
)
SELECT *
FROM SchoolsInState AS s
JOIN Grades AS g
ON s.id = g.id_schools
WHERE g.Reading = max(g.Reading)
The nice thing about this is that it creates this SchoolsInState pseudo-table which wraps all the logic about filtering by state, leaving you free to write the rest of your query without having to think about it.
I'm guessing [Reading] is some form of numeric value.
SELECT TOP (1)
s.[Id],
s.[State],
s.[SchoolName],
MAX(g.[Reading]) Reading
FROM
[Schools] s
JOIN [Grades] g on g.[id_schools] = s.[Id]
WHERE s.[State] = 'SA'
Group By
s.[Id],
s.[State],
s.[SchoolName]
Order By
MAX(g.[Reading]) DESC
UPDATE:
Looking at Tom's i don't think that would work but here is a modified version that does.
WITH [HighestGrade] (Reading)
AS (
SELECT
MAX([Reading]) Reading
FROM
[Grades]
)
SELECT
s.*,
g.*
FROM
[HighestGrade] hg
JOIN [Grades] AS g ON g.[Reading] = hg.[Reading]
JOIN [Schools] AS s ON s.[id] = g.[id_schools]
WHERE s.state = 'SA'
This CTE method should give you what you want. I also had it break down by year (grade_year in my code to avoid the reserved word). You should be able to remove that easily enough if you want to. This method also accounts for ties (you'll get both rows back if there is a tie):
;WITH MaxReadingByStateYear AS (
SELECT
S.id,
S.school_name,
S.state,
G.grade_year,
RANK() OVER(PARTITION BY S.state, G.grade_year ORDER BY Reading DESC) AS ranking
FROM
dbo.Grades G
INNER JOIN Schools S ON
S.id = G.id_schools
)
SELECT
id,
state,
school_name,
grade_year
FROM
MaxReadingByStateYear
WHERE
state = 'AL' AND
ranking = 1
One way would be this:
SELECT...
FROM...
WHERE...
AND g1.Reading = (select max(G2.Reading)
from Grades G2
inner join Schools s2
on s2.id = g2.id_schools
and s2.state = s.state)
There are certainly more.