Students enrolled in 3 or more course - sql-server

I new here
I have 2 tables Subject and student 123 and I want to list the name(s) of subject and number of students taking it only if the subject has 3 or more students enrolled
SELECT
B.Subject_Name ,A.Student_Name ,A.Subjct_ID
FROM [dbo].[Student 123] AS A
LEFT JOIN [dbo].[Subject] AS B ON A.Subjct_ID = B.Subject_ID
GROUP BY A.Student_Name, B.Subject_Name, A.Subjct_ID
I'm stuck here, not quite sure how to get the list; any help will be appreciated

First step is to identify the Subject_ID with 3 or more students. You use GROUP BY and HAVING to do this
SELECT A.Subject_ID
FROM Subject as A
INNER JOIN [Student 123] as B
ON A.Subject_ID = B.Subject_ID
GROUP BY A.Subject_ID
HAVING COUNT(*) >= 3
Next, with the above result, you JOIN back to the Student 123 to show the Student taking these Subject_ID

You want subject and number of students taking it. You can apply GROUP BY with HAVING clause.
SELECT B.Subjct_Name, COUNT(A.StudentName) as StudentCount
FROM [dbo].[Student 123] AS A
LEFT JOIN [dbo].[Subject] AS B ON A.Subjct_ID = B.Subject_ID
GROUP BY B.Subjct_ID, B.Subjct_Name
HAVING COUNT(A.StudentName) > 3
I have provided sample below:
DECLARE #subject table(subjectid int, subjectname varchar(10))
declare #student table(studentid int, subjectid varchar(10))
insert into #subject
values (1, 'maths'), (2, 'science')
insert into #student
values (1,1),(1,2),(2,1),(3,1),(4,1);
SELECT sub.subjectname,count(*) as studentcount
FROM #student as stu
left join #subject as sub
on sub.subjectid = stu.subjectid
group by sub.subjectid, sub.subjectname
having count(*) > 3
subjectname
studentcount
maths
4

Related

Sql Server - Join and group with table which has more records

I have below 4 tables :
User
Student
Department
Marks
I am trying to fetch Total number of students and sum of total marks by DepartmentId. For ex my result set should say Department Id 1 has 50 students and 1200 total Marks scored by all students in all exams across department.
Below query gives me correct result when i need Total Marks by Department
SELECT DepartmentId, SUM([Value]) AS TotalMarks FROM [dbo].[Marks] M
WHERE CollegeId = 3
GROUP BY DepartmentId
Below query gives correct result when i need only Total number of Students by Department.
SELECT S.[DepartmentId], COUNT(U.[StudentId]) AS TotalStudents
FROM [dbo].User U
INNER JOIN dbo.[Student] S
ON U.[UserId] = S.[UserId]
INNER JOIN [dbo].Department D
ON D.[DepartmentId] = S.[DepartmentID]
WHERE D.[CollegeId] = 3 AND U.[IsFullTimeStudent] = 1
GROUP BY S.[DepartmentId]
Now when i want to get Total Number of Students and Total Marks by Department in a single result using below query i am facing issues. My Marks table can have multiple number of record for single user and for that reason it giving redundant result.
SELECT S.[DepartmentId], COUNT(U.[StudentId]) AS TotalStudents, SUM(M.[Value]) AS TotalMarks
FROM [dbo].User U
INNER JOIN dbo.[Student] S
ON U.[UserId] = S.[UserId]
INNER JOIN [dbo].Department D
ON D.[DepartmentId] = S.[DepartmentID]
INNER JOIN [dbo].[Marks] M
ON D.[DepartmentId] = M.[DeprtmentId]
WHERE D.[CollegeId] = 3 AND U.[IsFullTimeStudent] = 1
GROUP BY S.[DepartmentId]
My marks table has UserId, DepartmentId ,CollegeId, Value fields.
For Ex :- If there are 110 entries for DepartmentId 1 in marks table and 1 Student who is an FTE student then in this case TotalUsers i am getting 110 Total Students though that Department has only 1 student because there are 110 entries in Marks i am getting as 110 Total Students
Is there any simpler way to get this resolved ?
Some sample data and table definitions would be useful. I invented my own sample data, it should almost fit your table definitions.
Using cross apply or outer apply (documentation and examples) allows to combine the count and sum results.
Sample data
create table departments
(
departmentid int,
departmentname nvarchar(20)
);
insert into departments (departmentid, departmentname) values
(1000, 'Business Faculty'),
(2000, 'Science Faculty' ),
(3000, 'Maintenance' );
create table users
(
departmentid int,
userid int,
username nvarchar(10),
isfulltimestudent bit
);
insert into users (departmentid, userid, username, isfulltimestudent) values
(1000, 1, 'Alice', 1),
(1000, 2, 'Bob', 0),
(2000, 3, 'Clarence', 1),
(2000, 4, 'Britt', 0);
create table students
(
userid int,
studentid int
);
insert into students (userid, studentid) values
(1, 100),
(2, 200),
(3, 300);
create table marks
(
departmentid int,
userid int,
mark int
);
insert into marks (departmentid, userid, mark) values
(1000, 1, 15),
(1000, 1, 8),
(1000, 2, 13),
(1000, 2, 12),
(2000, 3, 10),
(2000, 3, 7),
(2000, 3, 15),
(2000, 4, 10);
Solution
select d.departmentname,
ts.TotalStudents,
tm.TotalMarks
from departments d
outer apply ( select count(1) as TotalStudents
from users u
where u.departmentid = d.departmentid
and u.isfulltimestudent = 1 ) ts
outer apply ( select sum(m.mark) as TotalMarks
from marks m
where m.departmentid = d.departmentid ) tm;
Fiddle to see it in action.
Applied solution
Untested query that merges the queries from your question:
SELECT d.DepartmentId,
tm.TotalMarks,
ts.TotalStudents
FROM dbo.Department d
OUTER APPLY ( SELECT SUM(m.[Value]) AS TotalMarks
FROM dbo.Marks m
WHERE m.DepartmentId = d.DepartmentId ) tm
OUTER APPLY ( SELECT COUNT(u.StudentId) AS TotalStudents
FROM dbo.User u
JOIN dbo.Student s
ON u.UserId = s.UserId
WHERE u.IsFullTimeStudent = 1
AND s.DepartmentId = d.DepartmentId ) ts
WHERE d.CollegeId = 3;

SQL Function to return sequential id's

Consider this simple INSERT
INSERT INTO Assignment (CustomerId,UserId)
SELECT CustomerId,123 FROM Customers
That will obviously assign UserId=123 to all customers.
What I need to do is assign them to 3 userId's sequentially, so 3 users get one third of the accounts equally.
INSERT INTO Assignment (CustomerId,UserId)
SELECT CustomerId,fnGetNextId() FROM Customers
Could I create a function to return sequentially from a list of 3 ID's?, i.e. each time the function is called it returns the next one in the list?
Thanks
Could I create a function to return sequentially from a list of 3 ID's?,
If you create a SEQUENCE, then you can assign incremental numbers with the NEXT VALUE FOR (Transact-SQL) expression.
This is a strange requirement, but the modulus operator (%) should help you out without the need for functions, sequences, or altering your database structure. This assumes that the IDs are integers. If they're not, you can use ROW_NUMBER or a number of other tactics to get a distinct number value for each customer.
Obviously, you would replace the SELECT statement with an INSERT once you're satisfied with the code, but it's good practice to always select when developing before inserting.
SETUP WITH SAMPLE DATA:
DECLARE #Users TABLE (ID int, [Name] varchar(50))
DECLARE #Customers TABLE (ID int, [Name] varchar(50))
DECLARE #Assignment TABLE (CustomerID int, UserID int)
INSERT INTO #Customers
VALUES
(1, 'Joe'),
(2, 'Jane'),
(3, 'Jon'),
(4, 'Jake'),
(5, 'Jerry'),
(6, 'Jesus')
INSERT INTO #Users
VALUES
(1, 'Ted'),
(2, 'Ned'),
(3, 'Fred')
QUERY:
SELECT C.Name AS [CustomerName], U.Name AS [UserName]
FROM #Customers C
JOIN #Users U
ON
CASE WHEN C.ID % 3 = 0 THEN 1
WHEN C.ID % 3 = 1 THEN 2
WHEN C.ID % 3 = 2 THEN 3
END = U.ID
You would change the THEN 1 to whatever your first UserID is, THEN 2 with the second UserID, and THEN 3 with the third UserID. If you end up with another user and want to split the customers 4 ways, you would do replace the CASE statement with the following:
CASE WHEN C.ID % 4 = 0 THEN 1
WHEN C.ID % 4 = 1 THEN 2
WHEN C.ID % 4 = 2 THEN 3
WHEN C.ID % 4 = 3 THEN 4
END = U.ID
OUTPUT:
CustomerName UserName
-------------------------------------------------- --------------------------------------------------
Joe Ned
Jane Fred
Jon Ted
Jake Ned
Jerry Fred
Jesus Ted
(6 row(s) affected)
Lastly, you will want to select the IDs for your actual insert, but I selected the names so the results are easier to understand. Please let me know if this needs clarification.
Here's one way to produce Assignment as an automatically rebalancing view:
CREATE VIEW dbo.Assignment WITH SCHEMABINDING AS
WITH SeqUsers AS (
SELECT UserID, ROW_NUMBER() OVER (ORDER BY UserID) - 1 AS _ord
FROM dbo.Users
), SeqCustomers AS (
SELECT CustomerID, ROW_NUMBER() OVER (ORDER BY CustomerID) - 1 AS _ord
FROM dbo.Customers
)
-- INSERT Assignment(CustomerID, UserID)
SELECT SeqCustomers.CustomerID, SeqUsers.UserID
FROM SeqUsers
JOIN SeqCustomers ON SeqUsers._ord = SeqCustomers._ord % (SELECT COUNT(*) FROM SeqUsers)
;
This shifts assignments around if you insert a new user, which could be quite undesirable, and it's also not efficient if you had to JOIN on it. You can easily repurpose the query it contains for one-time inserts (the commented-out INSERT). The key technique there is joining on ROW_NUMBER()s.

Assigning data to groups based on percentage splits for count and total balance allocated

This might be a nice Friday afternoon SQL Puzzle for someone? ;D
I have a requirement for assigning customers to debt collection agencies (DCAs). Basically we agree a split, e.g. 65%:35%, or 50%:50%, or even 60%:30%:10% (as in my example) where the percentages govern two things:
the percentage of the number of customers assigned to each DCA;
the total percentage of debt assigned to each DCA.
My challenge is to come up with the best match I can find, e.g. if I had four customers:
Customer A owes £100;
Customer B owes £500;
Customer C owes £550;
Customer D owes £50.
The "ideal" match for a 50%:50% split would be:
DCA 1
- Customers A and B = 2 customers with a total debt of £600;
DCA 2
- Customers C and D = 2 customers with a total debt of £600.
...and a "bad" match would be:
DCA 1
- Customers A and D = 2 customers with a total debt of £150;
DCA 2
- Customers B and C = 2 customers with a total debt of £1050.
I can think of ways to solve this problem in .NET, and also "loop" based solutions for SQL scripts. These all work on the logic:
make random assignments by numbers only, i.e. put the right number of customers in each DCA "bucket", but don't attempt to split the balance properly;
does swapping one customer with another in a different bucket improve the balance assignment? If it does swap them;
repeat until all swaps have been considered.
I can't come up with a set-based solution to the problem.
Here is some test data, and a sample of what I have tried so far:
--Create some test data
DECLARE #table TABLE (
account_number VARCHAR(50),
account_balance NUMERIC(19,2));
INSERT INTO #table SELECT '1', 100;
INSERT INTO #table SELECT '2', 1200;
INSERT INTO #table SELECT '3', 500;
INSERT INTO #table SELECT '4', 300;
INSERT INTO #table SELECT '5', 200;
INSERT INTO #table SELECT '6', 250;
INSERT INTO #table SELECT '7', 400;
INSERT INTO #table SELECT '8', 300;
INSERT INTO #table SELECT '9', 280;
INSERT INTO #table SELECT '10', 100;
--Define the split by debt collection agency
DECLARE #split TABLE (
dca VARCHAR(10),
dca_split_percentage INT);
INSERT INTO #split SELECT 'dca1', 60;
INSERT INTO #split SELECT 'dca2', 30;
INSERT INTO #split SELECT 'dca3', 10;
--Somewhere to store the results
DECLARE #results TABLE (
dca VARCHAR(10),
account_number VARCHAR(50),
account_balance NUMERIC(19,2));
--Populate the results
WITH DCASplit AS (
SELECT dca, dca_split_percentage / 100.0 AS percentage FROM #split),
DCAOrdered AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY dca) AS dca_id FROM DCASplit),
Customers AS (
SELECT COUNT(*) AS customer_count FROM #table),
Random AS (
SELECT *, NEWID() AS random FROM #table),
Ordered AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY random) AS order_id FROM Random),
Splits AS (
SELECT TOP 1 d.dca_id, d.dca, 1 AS start_id, CONVERT(INT, c.customer_count * d.percentage) AS end_id FROM DCAOrdered d CROSS JOIN Customers c
UNION ALL
SELECT d.dca_id, d.dca, s.end_id + 1 AS start_id, CONVERT(INT, s.end_id + c.customer_count * d.percentage) AS end_id FROM Splits s INNER JOIN DCAOrdered d ON d.dca_id = s.dca_id + 1 CROSS JOIN Customers c)
INSERT INTO
#results
SELECT
d.dca,
o.account_number,
o.account_balance
FROM
DCAOrdered d
INNER JOIN Splits s ON s.dca_id = d.dca_id
INNER JOIN Ordered o ON o.order_id BETWEEN s.start_id AND s.end_id;
--Show the raw results
SELECT * FROM #results;
--Show the aggregated results
WITH total_debt AS (SELECT SUM(account_balance) AS total_balance FROM #results)
SELECT r.dca, COUNT(*) AS customers, SUM(r.account_balance), SUM(r.account_balance) / t.total_balance AS percentage_debt FROM #results r CROSS JOIN total_debt t GROUP BY r.dca, t.total_balance;
I know this works for splitting by the number of customers, but it makes no attempt to try and split out the balances using the percentage split. In fact the split is entirely random each time the script runs.
For example, I got these results from one run:
dca account_number account_balance
dca1 8 £300.00
dca1 2 £1200.00
dca1 5 £200.00
dca1 3 £500.00
dca1 9 £280.00
dca1 6 £250.00
dca2 10 £100.00
dca2 7 £400.00
dca2 1 £100.00
dca3 4 £300.00
But when I add that all up I get:
dca customers debt_assigned percentage_debt
dca1 6 £2730.00 75%
dca2 3 £600.00 17%
dca3 1 £300.00 8%
i.e. the 60:30:10 split is fine for customer counts, but I have assigned 75% of the debt to DCA1 when they should only be getting 60%, etc.
I could use a "brute force" solution, which would be to run that query multiple times, then pick the results set with the closest match by debt. However, the performance of that would be poor, it still wouldn't guarantee I would get the "best split", and I am dealing with hundreds of thousands of customers in real life.
Any ideas for how to solve this problem in a single set-based query?

Duplicate parent/child data

I have some parent/child data across two tables. I need to copy the parent rows back into the parent table, but then also copy the child rows as child rows of the new rows created.
I have been searching this site and Google, but can only find examples from Oracle or that use XML (or have many warnings about not being reliable), so am posting here for a complete easy-to-refer-back-to solution.
Take the following code (SqlFiddle):
DECLARE #tbl_person TABLE
(
ID int IDENTITY(1,1),
person nvarchar(20)
);
DECLARE #tbl_drinks TABLE
(
ID int IDENTITY(1,1),
personID int,
drink nvarchar(20)
);
DECLARE #i int;
INSERT INTO #tbl_person (person) VALUES ('Bob');
SET #i = SCOPE_IDENTITY();
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Beer');
INSERT INTO #tbl_person (person) VALUES ('Wendy');
SET #i = SCOPE_IDENTITY();
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Champage');
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Water');
INSERT INTO #tbl_person (person) VALUES ('Mike');
SET #i = SCOPE_IDENTITY();
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Beer');
INSERT INTO #tbl_drinks (personID, drink) VALUES (#i, 'Lemonade');
SELECT * FROM #tbl_person;
SELECT * FROM #tbl_drinks;
This produces this output:
ID person
----------- --------------------
1 Bob
2 Wendy
3 Mike
ID personID drink
----------- ----------- --------------------
1 1 Beer
2 2 Champage
3 2 Water
4 3 Beer
5 3 Lemonade
I know how to easily duplicate a single person plus their drinks, but not multiple people. Assuming I need to duplicate Bob and Wendy I need to get to this output:
ID person
----------- --------------------
1 Bob
2 Wendy
3 Mike
4 Bob
5 Wendy
ID personID drink
----------- ----------- --------------------
1 1 Beer
2 2 Champage
3 2 Water
4 3 Beer
5 3 Lemonade
6 4 Beer
7 5 Champagne
8 5 Water
I cannot figure out how to compare the old and new parent ID columns in order to get the child data.
The problem is that INSERT doesn't really have a "from table" that you could reference in the OUTPUT clause. But you could achieve the same with MERGE statement:
declare #tbl_IDmap table (newID int, oldID int)
merge #tbl_person as target
using (
select ID, person from #tbl_person where ID in (1,2)
) as source(ID, person)
on 1=0
when not matched then
insert (person) values(person)
output inserted.ID, source.ID into #tbl_IDmap;
And then duplicate the drinks with the new IDs:
insert into #tbl_drinks(personID, drink)
select m.newID, d.drink
from #tbl_drinks d
inner join #tbl_IDmap m
on m.oldID = d.personID
Here is your SqlFiddle updated.
Determined to add some additional solutions (and having thought about this most of the night!) I am posting an additional solution that doesn't use MERGE, hopefully to help users with older versions of SQL. It's more verbose than #TomT's suggestion but works okay.
SQLFiddle
-- Gather the people we need to copy
DECLARE #tbl_IdsToCopy TABLE
(
[counter] int IDENTITY(1,1),
[existingId] int
);
INSERT INTO #tbl_IdsToCopy (existingId) VALUES (1),(2); -- Bob & Wendy
-- Table to save new person ID's
DECLARE #tbl_newIds TABLE
(
[counter] int IDENTITY(1,1),
[newId] int
);
-- Create new people and save their new Id's
INSERT INTO #tbl_person
(
person
)
OUTPUT
INSERTED.ID
INTO
#tbl_newIds
(
[newId]
)
SELECT
p.person
FROM
#tbl_person p INNER JOIN
#tbl_IdsToCopy c ON c.existingId = p.ID
ORDER BY
c.[counter]; -- use counter to preserve ordering
-- map the old ID's to the new ID's and find the drinks for the old ID's
INSERT INTO #tbl_drinks
(
personID,
drink
)
SELECT
n.[newId],
d.drink
FROM
#tbl_IdsToCopy c INNER JOIN
#tbl_newIds n ON c.[counter] = n.[counter] INNER JOIN -- <-- map the old person ID to the new person Id
#tbl_drinks d ON d.personID = c.existingId; -- <-- find the drinks of the old person Id
-- Results
SELECT
p.ID,
p.person,
d.ID,
d.drink
FROM
#tbl_person p INNER JOIN
#tbl_drinks d ON d.personID = p.ID;

Arrange rows in T-SQL

How to arrange rows manually in T-SQL?
I have a table result in order like this:
Unknown
Charlie
Dave
Lisa
Mary
but the expected result is supposed to be:
Charlie
Dave
Lisa
Mary
Unknown
edited:
My whole query is:
select (case when s.StudentID is null then 'Unknown' else s.StudentName end) as StudentName from Period pd full join Course c on pd.TimeID = c.TimeID full join Student s on c.StudentID = s.StudentID
group by s.StudentName, s.StudentID
order by case s.StudentName
when 'Charlie' then 1
when 'Dave' then 2
when 'Lisa' then 3
when 'Mary' then 4
when 'Unknown' then 5
end
but it didn't work. I think the problem root is because Unknown is from NULL value, as I wrote in that query that when StudentID is null then change "NULL" to "Unknown". Is this affecting the "stubborn" order of the result? By the way I also have tried order by s.StudentName asc but also didn't work.
Thank you.
Try the following...
SELECT os.StudentName
FROM ( SELECT CASE WHEN s.StudentID IS NULL THEN 'Unknown'
ELSE s.StudentName
END AS StudentName
FROM Period pd
FULL JOIN Course c ON pd.TimeID = c.TimeID
FULL JOIN Student s ON c.StudentID = s.StudentID
GROUP BY s.StudentName ,
s.StudentID
) AS os
ORDER BY os.StudentName
Edit: based on comment...
When I use this, it works fine...notice the Order By has no identifier
declare #tblStudent TABLE (StudentID int, StudentName varchar(30));
insert into #tblStudent values (null, '');
insert into #tblStudent values (1, 'Charlie');
insert into #tblStudent values (2, 'Dave');
insert into #tblStudent values (3, 'Lisa');
insert into #tblStudent values (4, 'Mary');
SELECT CASE WHEN s.StudentID IS NULL THEN 'Unknown'
ELSE s.StudentName
END AS StudentName
FROM #tblStudent s
GROUP BY s.StudentName ,
s.StudentID
ORDER BY StudentName
As I see your rows must be ordered alphabetically, so just add in the end of the query: ORDER BY p.StudentName.
If this not help, please add whole query, so we can find out the problem.
So when I see query I can explain. You try to sort by column p.StudentName. This column contains NULL. Try to sort by StudentName without p in front. This is alias of the expression which contains Unknown.
just put the following clause in you SQL statement:
order by p.StudentName
Sql server will order the column alphabetically.

Resources