How to select rows in a many-to-many relationship? (SQL) - sql

I have a Students table and a Courses table.
They have a many to many relationship between them and the StudentCourses table is the intermediary.
Now, I have a list of Course ids and want to select the Students that follow all Courses in my list.
How??
--CREATE TYPE CourseListType AS TABLE
--(
-- CourseID INT
--)
DECLARE
#CourseList CourseListType
CREATE TABLE #Students
(
ID INT
,Name CHAR(10)
)
CREATE TABLE #Courses
(
ID INT
,Name CHAR(10)
)
CREATE TABLE #StudentCourses
(
StudentID INT
,CourseID INT
)
INSERT INTO #CourseList (CourseID)
VALUES
(1) --English
,(2) --Math
INSERT INTO #Students (ID, Name)
VALUES
(1, 'John')
,(2, 'Jane')
,(3, 'Donald')
INSERT INTO #Courses (ID, Name)
VALUES
(1, 'English')
,(2, 'Math')
,(3, 'Geography')
INSERT INTO #StudentCourses (StudentID, CourseID)
VALUES
(1, 1)
,(1, 2)
,(2, 1)
,(2, 2)
,(3, 1)
,(3, 3)
In this example, I only want the result to be John and Jane, because they both have the two courses in my CourseList.
I dont want Donald, because he only has one of them.
Have tried this JOIN, construction, but it does not eliminate students that only have some of my desired courses.
SELECT
*
FROM
#CourseList CRL
INNER JOIN #Courses CRS ON CRS.ID = CRL.CourseID
INNER JOIN #StudentCourses STC ON STC.CourseID = CRS.ID
INNER JOIN #Students STD ON STD.ID = STC.StudentID

If you want students with all your required courses, you can use aggregation and having:
SELECT sc.StudentId
FROM #StudentCourses sc JOIN
#CourseList cl
ON sc.CourseID = cl.id
GROUP BY sc.StudentId
HAVING COUNT(DISTINCT sc.CourseId) = (SELECT COUNT(*) FROM #DcourseList);
If you want additional information about students, you can join in the Students table (or use a IN or a similar construct).
Note that this only needs the StudentCourses table. It has the matching ids. There is no need to join in the reference tables.

Related

need sql, a tricky one

suppose I have three relations
part(partno,partname,color) partno is P.K
supplier(supplierno,sname) supplierno is P.K
part_supplier(supplierno,partno) supplierno, partno is P.K.
Now i want to get the name of the suppliers who supply parts of only one color.
The having clause is your freind.
select field1, etc, count(*) records
from yourTables
where whatever
group by field1, etc
having count(*) = 1
SELECT s.supplierno
FROM supplier AS s
JOIN part_supplier AS ps
ON s.supplierno = ps.supplierno
JOIN part AS p
ON ps.partno = p.partno
GROUP BY s.supplierno
HAVING COUNT(DISTINCT p.color) = 1 -- only one color for all parts
Select sup.sname,count(*) from part_supplier Map
inner join supplier sup
on map.supplierno=sup.supplierno
inner join part par
on Map.partno=par.partno
group by sup.sname
having count(*)=1
Query:
SELECT S.sname
FROM part P, supplier S, part_supplier PS
WHERE S.supplierno = ps.supplierno AND
PS.partno = P.partno
GROUP BY P.partno
HAVING count(P.color) = 1
Tables :
CREATE TABLE supplier (
supplierno INT PRIMARY KEY,
suppliername VARCHAR(10) NOT NULL
);
CREATE TABLE part (
partno INT PRIMARY KEY,
partname VARCHAR(10) NOT NULL,
color INT NOT NULL
);
CREATE TABLE part_supplier (
supplierno INT NOT NULL REFERENCES supplier(supplierno),
partno INT NOT NULL REFERENCES part(partno),
PRIMARY KEY (supplierno, partno)
);
Some data for testing :
INSERT INTO part VALUES (1, 'a',1), (2, 'b',2), (3, 'c',3),
(4, 'd',1), (5, 'e',2), (6, 'c',3),
(7, 'd',1), (8, 'e',2), (9, 'e',3);
INSERT INTO supplier VALUES (1,'a'), (2,'b'), (3,'c');
INSERT INTO part_supplier VALUES (1,1), (2,1), (3,3),
(4,1), (5,1), (6,1),
(7,2), (8,2), (9,3);

Collecting entity attribute values with null for missing attributes in SQL

I have some entities in a table and their attributes and values in an other. I would like to create a select where I can see the value a specific attribute for every entity or null if that attribute is missing. How can I do this using standard SQL?
This is the setup:
create table person (id int not null, nick varchar(32) not null);
insert into person (id, nick) values (1, 'John');
insert into person (id, nick) values (2, 'Peter');
create table req_attributes (name varchar(32));
create table person_attributes (id int not null,
person_id int not null,
attribute varchar(32) not null,
value varchar(64) not null);
insert into person_attributes values (1, 1, 'age', '21');
insert into person_attributes values (2, 1, 'hair', 'brown');
insert into person_attributes values (3, 2, 'age', '32');
insert into person_attributes values (4, 2, 'music', 'jazz');
And this is my current select statement:
select * from person join person_attributes on
person.id = person_attributes.person_id
where attribute = 'hair';
Obviously Peter is not in the result set because we have no information about his hair. I would like to get him into the result set as well, but with null value.
The best would be if the result set was like
Person, Hair color
John, brown
Peter, null
I would like to avoid subqueries if possible, but if it is impossible to do with joins then they are welcome.
An outer join will do this:
select p.*, pa.value
from person p
left join person_attributes pa
on p.id = pa.person_id
and pa.attribute = 'hair';
Note that the condition for the "outer joined" table needs to go into the JOIN clause, not into the where clause. If the condition was in the where clause it would effectively turn the outer join into an inner join. This is because pa.attribute would be null due to the outer join, and the where would not match the null value thus eliminating all the rows that should actually stay in the result.
SQFiddle based on your example: http://sqlfiddle.com/#!12/d0342/1

SQL nested query in SQL server

i have a library database which wrote a query to display count of loaned books by employee like this:
select Emploee.[Emp-No],count(*) as ecount
from Emploee
inner join Loan on Emploee.[Emp-No]=Loan.[Emp-No]
inner join Book on Loan.ISBN=Book.ISBN group by Emploee.[Emp-No]
the result of above query is something like this:
Emp-No ecount
------------------
1000 4
1001 2
1002 3
now i want to modify the output and make a comparison between ecount column of each row of result with another query which give me count of loaned books based on specific published by that user
in other word the result im looking for is something like this
Emp-No ecount
-----------------
1000 4
assume Employee 1000 loaned all of his book from one publisher. he will be showen in the result.
something like this
"..... my query...." having ecount=
(select count(*) from books where publisher='A')
but i cant use the result ecount in another query :(
After clarification, I understood the question as follows: return those employees which only loaned books from a single publisher.
You can do that by using COUNT(DISTINCT publisher) in your HAVING clause.
Like so:
declare #employee table (id int);
declare #books table (isbn int, title varchar(50), publisher varchar(50));
declare #loan table (employee_id int, book_isbn int);
insert #employee values (1000);
insert #employee values (1001);
insert #employee values (1002);
insert #books values (1, 'Some Book', 'Publisher A');
insert #books values (2, 'Some Book', 'Publisher A');
insert #books values (3, 'Some Book', 'Publisher A');
insert #books values (4, 'Some Book', 'Publisher B');
insert #books values (5, 'Some Book', 'Publisher B');
insert #books values (6, 'Some Book', 'Publisher B');
insert #loan values (1000, 1);
insert #loan values (1000, 2);
insert #loan values (1001, 3);
insert #loan values (1001, 4);
insert #loan values (1001, 5);
-- Show the number of different publishers per employee
select e.id, count(*) as ecount, count(DISTINCT b.publisher) as publishers
from #employee e
inner join #loan l on e.id = l.employee_id
inner join #books b on l.book_isbn = b.isbn
group by e.id
-- yields: id ecount publishers
-- ----------- ----------- -----------
-- 1000 2 1
-- 1001 3 2
-- Filter on the number of different publishers per employee
select e.id, count(*) as ecount
from #employee e
inner join #loan l on e.id = l.employee_id
inner join #books b on l.book_isbn = b.isbn
group by e.id
having count(DISTINCT b.publisher) = 1
-- yields: id ecount
-- ----------- -----------
-- 1000 2
To refer to an aggregate in another query, the aggregate has to have an alias. SQL Server does not allow you to refer to an alias at the same level. So you need a subquery to define the alias, before you can use the alias in another subquery.
For example, the following SQL uses a subquery to define alias bookcount for count(*). Thanks to this first subquery, the second subquery in the where clause can use the bookcount:
declare #books table (title varchar(50), author varchar(50));
declare #author_filter table (name varchar(50), bookcount int);
insert #books values
('The Lord of the Rings', 'J.R.R. Tolkien'),
('The Silmarillion', 'J.R.R. Tolkien'),
('A Song for Arbonne', 'G.G. Kay');
insert #author_filter values
('2_books', 2);
select *
from (
select author
, count(*) as bookcount
from #books
group by
author
) authors
where '2_books_filter' =
(
select filter.name
from #author_filter filter
where authors.bookcount = filter.bookcount
)

Remove dups on one side but not on the other in SQL JOIN

code updated:
Added [score] column to #left table. The join must pull student name and score from #left table. Any one score is fine but it should not pull duplicate student_id. I am thinking cursor can do it?
I am running this report. The problem is similar to the the script that I created. To sum up, I have two tables lets call it #left and #right. The #right table is the key one which contains the amount. But the #left table contains the agent name which I must retrieve too.
create table #left (
id int not null primary key identity,
student_id int, name varchar(20),score int)
insert into #left values( 1, 'James',10)
insert into #left values( 2, 'Parker',20)
insert into #left values( 3, 'Smith',30)
insert into #left values( 4, 'Rog',40)
insert into #left values( 1, 'James',50)
insert into #left values( 2, 'Parker',60)
create table #right (
id int not null primary key identity,
student_id int,
amount decimal(5,2)
)
insert into #right values (1,5.25)
insert into #right values (3,7.25)
insert into #right values (4,3.25)
insert into #right values (1,5.25)
The problem here is I want to join the two tables on student_id but as you can see #left table contains James and Parker 2 times with the same id. Lets assume this was database mistake.
A similar problem can exists in the #right table. But that is not a mistake. It simply means the #left student was associated with two payments (even if the two payments are the same).
I want to remove dups from #left table and keep the dups in the #right table.
I came up with this query, which works
/* query 1 */
select student_id, amount from #right R
where exists
(
select student_id from #left L
where R.student_id = L.student_id
);
But the problem here is I also need to pull the student name from the other table. Is there a way to do it. It can be any syntax but efficient query is desirable.
This should work:
Solution to the original problem
Where #left table looked like this
create table #left (
id int not null primary key identity,
student_id int, name varchar(20))
insert into #left values( 1, 'James')
insert into #left values( 2, 'Parker')
insert into #left values( 3, 'Smith')
insert into #left values( 4, 'Rog')
insert into #left values( 1, 'James')
insert into #left values( 2, 'Parker')
Query: 1
SELECT A.student_id, A.amount, B.name
FROM #right A
LEFT JOIN (SELECT DISTINCT student_id, name FROM #left) B
ON A.student_id = B.student_id
UPDATED to the modified criteria Well, if any score is fine, then you can do this:
Query: 2
SELECT A.student_id, A.amount, B.name, B.score
FROM #right A
LEFT JOIN ( SELECT student_id, name, MIN(score) score
FROM #left
group by student_id, name) B
ON A.student_id = B.student_id
If there can exist students with no payments (no rows in #right table), you should start from the #left table I think with joining. For avoiding duplicate student rows you could use distinct as already suggested or aggregate functions like group by.
Some untested code for my idea:
select name, id, amount from
(
(select max(left.name) as name, left.student_id as id
from #left left
group by left.student_id)
left join #right right on left.student_id = right.student_id
)

SQL: How do I loop through the results of a SELECT statement?

How do I loop through the results of a SELECT statement in SQL? My SELECT statement will return just 1 column but n results.
I have created a fictional scenario below complete with the Pseudo code of what I'm trying to do.
Scenario:
Students are registering for their classes. They submit a form with multiple course selections (ie. select 3 different courses at once). When they submit their registration I need to ensure there is still room left int the courses they have selected (note I will do a similar check before presenting them with course selection UI but I need to verify afterwards in case somebody else has gone in and swipped up the remaining spots).
Pseudo Code:
DECLARE #StudentId = 1
DECLARE #Capacity = 20
-- Classes will be the result of a Select statement which returns a list of ints
#Classes = SELECT classId FROM Student.CourseSelections
WHERE Student.CourseSelections = #StudentId
BEGIN TRANSACTION
DECLARE #ClassId int
foreach (#classId in #Classes)
{
SET #SeatsTaken = fnSeatsTaken #classId
if (#SeatsTaken > #Capacity)
{
ROLLBACK; -- I'll revert all their selections up to this point
RETURN -1;
}
else
{
-- set some flag so that this student is confirmed for the class
}
}
COMMIT
RETURN 0
My real problem is a similar "ticketing" problem. So if this approach seems very wrong please feel free to recommend something more practical.
EDIT:
Attempting to implement the solution below. At this point it doesn't work. Always returns "reserved".
DECLARE #Students TABLE
(
StudentId int
,StudentName nvarchar(max)
)
INSERT INTO #Students
(StudentId ,StudentName)
VALUES
(1, 'John Smith')
,(2, 'Jane Doe')
,(3, 'Jack Johnson')
,(4, 'Billy Preston')
-- Courses
DECLARE #Courses TABLE
(
CourseId int
,Capacity int
,CourseName nvarchar(max)
)
INSERT INTO #Courses
(CourseId, Capacity, CourseName)
VALUES
(1, 2, 'English Literature'),
(2, 10, 'Physical Education'),
(3, 2, 'Photography')
-- Linking Table
DECLARE #Courses_Students TABLE
(
Course_Student_Id int
,CourseId int
,StudentId int
)
INSERT INTO #Courses_Students
(Course_Student_Id, StudentId, CourseId)
VALUES
(1, 1, 1),
(2, 1, 3),
(3, 2, 1),
(4, 2, 2),
(5, 3, 2),
(6, 4, 1),
(7, 4, 2)
SELECT Students.StudentName, Courses.CourseName FROM #Students Students INNER JOIN
#Courses_Students Courses_Students ON Courses_Students.StudentId = Students.StudentId INNER JOIN
#Courses Courses ON Courses.CourseId = Courses_Students.CourseId
DECLARE #StudentId int = 4
-- Ideally the Capacity would be database driven
-- ie. come from the Courses.Capcity.
-- But I didn't want to complicate the HAVING statement since it doesn't seem to work already.
DECLARE #Capacity int = 1
IF EXISTS (Select *
FROM
#Courses Courses INNER JOIN
#Courses_Students Courses_Students ON Courses_Students.CourseId = Courses.CourseId
WHERE
Courses_Students.StudentId = #StudentId
GROUP BY
Courses.CourseId
HAVING
COUNT(*) > #Capacity)
BEGIN
SELECT 'full' as Status
END
ELSE BEGIN
SELECT 'reserved' as Status
END
No loop needed. You're looking at a standard aggregate with COUNT and GROUP.
Of course, some details are needed but the principle is this...
DECLARE #StudentId = 1
DECLARE #Capacity = 20
-- Classes will be the result of a Select statement which returns a list of ints
IF EXISTS (SELECT *
FROM
Student.CourseSelections CS
JOIN
---this is where you find out course allocations somehow
ClassTable C ON CS.classId = C.classId
WHERE
Student.CourseSelections = #StudentId
GROUP BY --change this, it depends on where you find out course allocations
ClassID
HAVING
COUNT(*) > #Capacity)
'no'
ELSE
'yes'
Edit:
I've changed the link table. Course_Student_ID is usually not needed in link tables.
The JOIN now
gets the courses for that student
then looks at all students on this course and compares to capacity
Cut down version of above:
...
-- Linking Table
DECLARE #Courses_Students TABLE (
,CourseId int
,StudentId int)
INSERT INTO #Courses_Students
(StudentId, CourseId)
VALUES (1, 1), (1, 3), (2, 1), (2, 2), (3, 2), (4, 1), (4, 2)
DECLARE #StudentId int = 4
--straight list
SELECT
C.CourseName, C.Capacity, COUNT(*)
FROM
#Courses_Students CSThis
JOIN
#Courses C ON CSThis.CourseId = C.CourseId
JOIN
#Courses_Students CSOthers ON CSOthers.CourseId = C.CourseId
WHERE
CSThis.StudentId = #StudentId
GROUP BY
C.CourseName, C.Capacity
--oversubscribed list
SELECT
C.CourseName, C.Capacity, COUNT(*)
FROM
#Courses_Students CSThis
JOIN
#Courses C ON CSThis.CourseId = C.CourseId
JOIN
#Courses_Students CSOthers ON CSOthers.CourseId = C.CourseId
WHERE
CSThis.StudentId = #StudentId
GROUP BY
C.CourseName, C.Capacity
HAVING
COUNT(*) > C.Capacity
Avoid looping through result sets in SQL as much as you can. If you really can't (if you really are a standard programmer but profession leads you into SQL) use cursors. They don't smell nice, but are unavoidable at times.
Another option would be to implement a CHECK Constraint on your table that contains the Course information. The check constraint could call your existing function to check that there are free seats.
Wrap all of your Inserts/Updates in to one transaction. If any of the Inserts/Updates fails then the entire transaction will be rolled back.