Optimize Simple SQL Query - sql

Suppose you have a table of students and a gpa. The idea is return the student or students with the highest GPA. If only 1 student, the prize is $1000. Otherwise, the amount is split between the number of students sharing the highest gpa. The result below returns what I would expect, 3 students, and an amount of 333. I'm wondering if this is the best or most optimal way of writing the query?
CREATE TABLE Test (
PersonID int,
Name varchar(255),
GPA DECIMAL(3,2)
);
INSERT INTO Test(personid, name, gpa) VALUES(1, 'Frank', 2.7)
INSERT INTO Test(personid, name, gpa) VALUES(2, 'Barb', 3.7)
INSERT INTO Test(personid, name, gpa) VALUES(3, 'Tammy', 3.7)
INSERT INTO Test(personid, name, gpa) VALUES(4, 'Edward', 3.7)
Select name, gpa,
(Select Case When Count(*) = 1 Then '1000'
Else 1000/COUNT(*)
End
FROM Test
WHERE gpa = (SELECT MAX(gpa) FROM test)
) As 'Prize Amount'
FROM Test
Where gpa = (SELECT MAX(gpa) FROM test)
Results of query
I feel like it isn't efficient because of having to query twice. I'd like to just be able to divide by the number of rows. Something like below doesn't work (groupby issue) and adding a groupby on gpa, name would always display 1000, since each group of name/gpa has 1 record.
Select name, gpa,
Case When Count(*) = 1 Then '1000'
Else 1000/COUNT(*)
End As 'Prize Amount'
FROM Test
Where gpa = (SELECT MAX(gpa) FROM test)

I think you want window functions:
select t.*,
1000.0 / count(*) over ()
from t
where t.gpa = (select max(t2.gpa) from test t2);
With an index on gpa, this is probably the fastest solution.

Related

Removing redundancies in sql query that contains subquery

Suppose we have a table with scheme
student(id (primary key), name, math_score, english_score)
I am trying to get student information (id and name) with highest rank (ordered by highest sum of math score and english score). There may be several student with tie, and we want all of them. The way I thought about doing this is to use subquery to get a table with sum of scores, then find ids, names that have highest sum.
SELECT s.id, s.name
FROM (SELECT s.id, s.name, s.math_score+s.english_score as sum
FROM student s) s
WHERE s.sum = (SELECT max(s.sum)
FROM (SELECT s.id, s.name, s.math_score+s.english_score as sum
FROM student s) s)
This works, but seems very redundant and not efficient.
I just started learning sql language, and I would appreciate some insight on this problem!
Use WITH TIES
create table #student(
id int primary key identity(1,1),
name varchar(50),
math_score decimal,
english_score decimal
)
insert into #student
values
('Tom', 90, 90),
('Dick', 70, 70),
('Harry', 80, 100)
select TOP(1) WITH TIES
id,
name,
math_score,
english_score,
math_score + english_score as ScoreRank
from #student
order by
math_score + english_score desc
Gives the answer:
id|name|math_score|english_score|ScoreRank
1|Tom|90|90|180
3|Harry|80|100|180
This should accomplish it, you're adding in an unnecessary step.
select id,
name,
math_score+english_score as total_score
from student
where math_score+english_score=(select max(math_score+english_score)
from student)
SELECT id, name, math_score+english_score as 'sum'
FROM student
Order by math_score+english_score DESC;

Calculating precentage SQL [duplicate]

I have a SQL Server table that contains users & their grades. For simplicity's sake, lets just say there are 2 columns - name & grade. So a typical row would be Name: "John Doe", Grade:"A".
I'm looking for one SQL statement that will find the percentages of all possible answers. (A, B, C, etc...) Also, is there a way to do this without defining all possible answers (open text field - users could enter 'pass/fail', 'none', etc...)
The final output I'm looking for is A: 5%, B: 15%, C: 40%, etc...
The most efficient (using over()).
select Grade, count(*) * 100.0 / sum(count(*)) over()
from MyTable
group by Grade
Universal (any SQL version).
select Grade, count(*) * 100.0 / (select count(*) from MyTable)
from MyTable
group by Grade;
With CTE, the least efficient.
with t(Grade, GradeCount)
as
(
select Grade, count(*)
from MyTable
group by Grade
)
select Grade, GradeCount * 100.0/(select sum(GradeCount) from t)
from t;
I have tested the following and this does work. The answer by gordyii was close but had the multiplication of 100 in the wrong place and had some missing parenthesis.
Select Grade, (Count(Grade)* 100 / (Select Count(*) From MyTable)) as Score
From MyTable
Group By Grade
Instead of using a separate CTE to get the total, you can use a window function without the "partition by" clause.
If you are using:
count(*)
to get the count for a group, you can use:
sum(count(*)) over ()
to get the total count.
For example:
select Grade, 100. * count(*) / sum(count(*)) over ()
from table
group by Grade;
It tends to be faster in my experience, but I think it might internally use a temp table in some cases (I've seen "Worktable" when running with "set statistics io on").
EDIT:
I'm not sure if my example query is what you are looking for, I was just illustrating how the windowing functions work.
I simply use this when ever I need to work out a percentage..
ROUND(CAST((Numerator * 100.0 / Denominator) AS FLOAT), 2) AS Percentage
Note that 100.0 returns 1 decimal, whereas 100 on it's own will round up the result to the nearest whole number, even with the ROUND(...,2) function!
You have to calculate the total of grades
If it is SQL 2005 you can use CTE
WITH Tot(Total) (
SELECT COUNT(*) FROM table
)
SELECT Grade, COUNT(*) / Total * 100
--, CONVERT(VARCHAR, COUNT(*) / Total * 100) + '%' -- With percentage sign
--, CONVERT(VARCHAR, ROUND(COUNT(*) / Total * 100, -2)) + '%' -- With Round
FROM table
GROUP BY Grade
You need to group on the grade field. This query should give you what your looking for in pretty much any database.
Select Grade, CountofGrade / sum(CountofGrade) *100
from
(
Select Grade, Count(*) as CountofGrade
From Grades
Group By Grade) as sub
Group by Grade
You should specify the system you're using.
The following should work
ID - Key
Grade - A,B,C,D...
EDIT: Moved the * 100 and added the 1.0 to ensure that it doesn't do integer division
Select
Grade, Count(ID) * 100.0 / ((Select Count(ID) From MyTable) * 1.0)
From MyTable
Group By Grade
This is, I believe, a general solution, though I tested it using IBM Informix Dynamic Server 11.50.FC3. The following query:
SELECT grade,
ROUND(100.0 * grade_sum / (SELECT COUNT(*) FROM grades), 2) AS pct_of_grades
FROM (SELECT grade, COUNT(*) AS grade_sum
FROM grades
GROUP BY grade
)
ORDER BY grade;
gives the following output on the test data shown below the horizontal rule. The ROUND function may be DBMS-specific, but the rest (probably) is not. (Note that I changed 100 to 100.0 to ensure that the calculation occurs using non-integer - DECIMAL, NUMERIC - arithmetic; see the comments, and thanks to Thunder.)
grade pct_of_grades
CHAR(1) DECIMAL(32,2)
A 32.26
B 16.13
C 12.90
D 12.90
E 9.68
F 16.13
CREATE TABLE grades
(
id VARCHAR(10) NOT NULL,
grade CHAR(1) NOT NULL CHECK (grade MATCHES '[ABCDEF]')
);
INSERT INTO grades VALUES('1001', 'A');
INSERT INTO grades VALUES('1002', 'B');
INSERT INTO grades VALUES('1003', 'F');
INSERT INTO grades VALUES('1004', 'C');
INSERT INTO grades VALUES('1005', 'D');
INSERT INTO grades VALUES('1006', 'A');
INSERT INTO grades VALUES('1007', 'F');
INSERT INTO grades VALUES('1008', 'C');
INSERT INTO grades VALUES('1009', 'A');
INSERT INTO grades VALUES('1010', 'E');
INSERT INTO grades VALUES('1001', 'A');
INSERT INTO grades VALUES('1012', 'F');
INSERT INTO grades VALUES('1013', 'D');
INSERT INTO grades VALUES('1014', 'B');
INSERT INTO grades VALUES('1015', 'E');
INSERT INTO grades VALUES('1016', 'A');
INSERT INTO grades VALUES('1017', 'F');
INSERT INTO grades VALUES('1018', 'B');
INSERT INTO grades VALUES('1019', 'C');
INSERT INTO grades VALUES('1020', 'A');
INSERT INTO grades VALUES('1021', 'A');
INSERT INTO grades VALUES('1022', 'E');
INSERT INTO grades VALUES('1023', 'D');
INSERT INTO grades VALUES('1024', 'B');
INSERT INTO grades VALUES('1025', 'A');
INSERT INTO grades VALUES('1026', 'A');
INSERT INTO grades VALUES('1027', 'D');
INSERT INTO grades VALUES('1028', 'B');
INSERT INTO grades VALUES('1029', 'A');
INSERT INTO grades VALUES('1030', 'C');
INSERT INTO grades VALUES('1031', 'F');
SELECT Grade, GradeCount / SUM(GradeCount)
FROM (SELECT Grade, COUNT(*) As GradeCount
FROM myTable
GROUP BY Grade) Grades
In any sql server version you could use a variable for the total of all grades like this:
declare #countOfAll decimal(18, 4)
select #countOfAll = COUNT(*) from Grades
select
Grade, COUNT(*) / #countOfAll * 100
from Grades
group by Grade
You can use a subselect in your from query (untested and not sure which is faster):
SELECT Grade, COUNT(*) / TotalRows
FROM (SELECT Grade, COUNT(*) As TotalRows
FROM myTable) Grades
GROUP BY Grade, TotalRows
Or
SELECT Grade, SUM(PartialCount)
FROM (SELECT Grade, 1/COUNT(*) AS PartialCount
FROM myTable) Grades
GROUP BY Grade
Or
SELECT Grade, GradeCount / SUM(GradeCount)
FROM (SELECT Grade, COUNT(*) As GradeCount
FROM myTable
GROUP BY Grade) Grades
You can also use a stored procedure (apologies for the Firebird syntax):
SELECT COUNT(*)
FROM myTable
INTO :TotalCount;
FOR SELECT Grade, COUNT(*)
FROM myTable
GROUP BY Grade
INTO :Grade, :GradeCount
DO
BEGIN
Percent = :GradeCount / :TotalCount;
SUSPEND;
END
This one is working well in MS SQL. It transforms varchar to the result of two-decimal-places-limited float.
Select field1, cast(Try_convert(float,(Count(field2)* 100) /
Try_convert(float, (Select Count(*) From table1))) as decimal(10,2)) as new_field_name
From table1
Group By field1, field2;
I had a similar issue to this. you should be able to get the correct result multiplying by 1.0 instead of 100.See example Image attached
Select Grade, (Count(Grade)* 1.0 / (Select Count(*) From MyTable)) as Score From MyTable Group By Grade

find MIN without using min()

I am trying to find student who has min score which will be the result of the below query. However, I was asked to write the query without using MIN(). Spent several hours but I can't find the alternative solution :'(.
select s.sname
from student s
where s.score =
(select min(s2.score)
from score s2)
This is one way, which will work even if two students have same lowest score.
SELECT distinct s1.sname
FROM student s1
LEFT JOIN student s2
ON s2.score < s1.score
WHERE s2.score IS NULL
The below is the method using limit, which will return lowest score student, but only one of them if multiple of them have same score.
select sname
from student
order by score asc
limit 1
Here's a possible alternative to the JOIN approach:
select sname from student where score in
(select score from student order by score asc limit 1)
create table student (name varchar(10), score int);
insert into student (name, score) values('joe', 30);
insert into student (name, score) values('jim', 88);
insert into student (name, score) values('jack', 22);
insert into student (name, score) values('jimbo', 15);
insert into student (name, score) values('jo bob',15);
/* folks with lowest score */
select name, score from student where not exists(select 1 from student s where s.score < student.score);
/* the actual lowest score */
select distinct score from student
where not exists(select 1 from student s where s.score < student.score);
Note that not exists can be brutally inefficient, but it'll do the job on a small set.
One way of doing it would be to Order the results in Ascending order and take the first row.
But if you are looking at a more generic solution as a student will have more than one mark associated with him, So you need to find the total marks for each student and then find the student with the least total.
This is the first scenario, A student only has one row in the table.
CREATE TABLE Student
(
SLNO INT,
MARKS FLOAT,
NAME NVARCHAR(MAX)
)
INSERT INTO Student VALUES(1, 80, 't1')
INSERT INTO Student VALUES(2, 90, 't2')
INSERT INTO Student VALUES(3, 76, 't3')
INSERT INTO Student VALUES(4, 98, 't4')
INSERT INTO Student VALUES(5, 55, 't5')
SELECT * From Student ORDER BY MARKS ASC
The second scenario as specified above is, He has multiple rows in the table, So we insert two more rows into the table for existing users.
Then we select the users by taking the sum of their marks grouping the results by name and then ordering the results by their total
INSERT INTO Student VALUES(6, 55, 't1')
INSERT INTO Student VALUES(6, 90, 't5')
SELECT SUM(MARKS) AS TOTAL, NAME FROM Student
GROUP BY NAME
ORDER BY TOTAL
Hope the above is what you are looking for.
You can try stored procedure to find student with minimum score.

SQL statement to return student's best grade

I have a table as follows with student's exam results in:
I want a SQL statement that will return the student's best grade:
Student 1 - Maths A
Student 2 - Maths D
etc.....
I have tried MAX() on the date, DISTINCT, Grouping By... But I think I'm fumbling in the dark.
EDIT: I should say that MIN and MAX on the grade will not work as A* is a possible grade and alphabetically comes after A which is incorrect. Grades could be a variety of letters and numbers which have no logical ranking (e.g. L1 is better than EL1 but L3 is better than L1). Possibly a subtable with ranking is required?
SELECT StudentName
,MIN(Grade) Best_Grade
FROM Table_Name
GROUP BY StudentName
Please try below query in SQL :
create table #test_table(
name varchar(20),
subjects varchar(20),
grade varchar,
resultdate datetime
)
insert into #test_table
select 'student1', 'math', 'A','2012-09-01' union all
select 'student1', 'math', 'B','2013-09-01' union all
select 'student2', 'math', 'D','2014-09-01' union all
select 'student1', 'math', 'A','2014-09-01'
select * from(
select row_number() over(partition by name,subjects order by grade,resultdate desc) as rn,* from #test_table ) tbl where rn=1
drop table #test_table

How to calculate percentage with a SQL statement

I have a SQL Server table that contains users & their grades. For simplicity's sake, lets just say there are 2 columns - name & grade. So a typical row would be Name: "John Doe", Grade:"A".
I'm looking for one SQL statement that will find the percentages of all possible answers. (A, B, C, etc...) Also, is there a way to do this without defining all possible answers (open text field - users could enter 'pass/fail', 'none', etc...)
The final output I'm looking for is A: 5%, B: 15%, C: 40%, etc...
The most efficient (using over()).
select Grade, count(*) * 100.0 / sum(count(*)) over()
from MyTable
group by Grade
Universal (any SQL version).
select Grade, count(*) * 100.0 / (select count(*) from MyTable)
from MyTable
group by Grade;
With CTE, the least efficient.
with t(Grade, GradeCount)
as
(
select Grade, count(*)
from MyTable
group by Grade
)
select Grade, GradeCount * 100.0/(select sum(GradeCount) from t)
from t;
I have tested the following and this does work. The answer by gordyii was close but had the multiplication of 100 in the wrong place and had some missing parenthesis.
Select Grade, (Count(Grade)* 100 / (Select Count(*) From MyTable)) as Score
From MyTable
Group By Grade
Instead of using a separate CTE to get the total, you can use a window function without the "partition by" clause.
If you are using:
count(*)
to get the count for a group, you can use:
sum(count(*)) over ()
to get the total count.
For example:
select Grade, 100. * count(*) / sum(count(*)) over ()
from table
group by Grade;
It tends to be faster in my experience, but I think it might internally use a temp table in some cases (I've seen "Worktable" when running with "set statistics io on").
EDIT:
I'm not sure if my example query is what you are looking for, I was just illustrating how the windowing functions work.
I simply use this when ever I need to work out a percentage..
ROUND(CAST((Numerator * 100.0 / Denominator) AS FLOAT), 2) AS Percentage
Note that 100.0 returns 1 decimal, whereas 100 on it's own will round up the result to the nearest whole number, even with the ROUND(...,2) function!
You have to calculate the total of grades
If it is SQL 2005 you can use CTE
WITH Tot(Total) (
SELECT COUNT(*) FROM table
)
SELECT Grade, COUNT(*) / Total * 100
--, CONVERT(VARCHAR, COUNT(*) / Total * 100) + '%' -- With percentage sign
--, CONVERT(VARCHAR, ROUND(COUNT(*) / Total * 100, -2)) + '%' -- With Round
FROM table
GROUP BY Grade
You need to group on the grade field. This query should give you what your looking for in pretty much any database.
Select Grade, CountofGrade / sum(CountofGrade) *100
from
(
Select Grade, Count(*) as CountofGrade
From Grades
Group By Grade) as sub
Group by Grade
You should specify the system you're using.
The following should work
ID - Key
Grade - A,B,C,D...
EDIT: Moved the * 100 and added the 1.0 to ensure that it doesn't do integer division
Select
Grade, Count(ID) * 100.0 / ((Select Count(ID) From MyTable) * 1.0)
From MyTable
Group By Grade
This is, I believe, a general solution, though I tested it using IBM Informix Dynamic Server 11.50.FC3. The following query:
SELECT grade,
ROUND(100.0 * grade_sum / (SELECT COUNT(*) FROM grades), 2) AS pct_of_grades
FROM (SELECT grade, COUNT(*) AS grade_sum
FROM grades
GROUP BY grade
)
ORDER BY grade;
gives the following output on the test data shown below the horizontal rule. The ROUND function may be DBMS-specific, but the rest (probably) is not. (Note that I changed 100 to 100.0 to ensure that the calculation occurs using non-integer - DECIMAL, NUMERIC - arithmetic; see the comments, and thanks to Thunder.)
grade pct_of_grades
CHAR(1) DECIMAL(32,2)
A 32.26
B 16.13
C 12.90
D 12.90
E 9.68
F 16.13
CREATE TABLE grades
(
id VARCHAR(10) NOT NULL,
grade CHAR(1) NOT NULL CHECK (grade MATCHES '[ABCDEF]')
);
INSERT INTO grades VALUES('1001', 'A');
INSERT INTO grades VALUES('1002', 'B');
INSERT INTO grades VALUES('1003', 'F');
INSERT INTO grades VALUES('1004', 'C');
INSERT INTO grades VALUES('1005', 'D');
INSERT INTO grades VALUES('1006', 'A');
INSERT INTO grades VALUES('1007', 'F');
INSERT INTO grades VALUES('1008', 'C');
INSERT INTO grades VALUES('1009', 'A');
INSERT INTO grades VALUES('1010', 'E');
INSERT INTO grades VALUES('1001', 'A');
INSERT INTO grades VALUES('1012', 'F');
INSERT INTO grades VALUES('1013', 'D');
INSERT INTO grades VALUES('1014', 'B');
INSERT INTO grades VALUES('1015', 'E');
INSERT INTO grades VALUES('1016', 'A');
INSERT INTO grades VALUES('1017', 'F');
INSERT INTO grades VALUES('1018', 'B');
INSERT INTO grades VALUES('1019', 'C');
INSERT INTO grades VALUES('1020', 'A');
INSERT INTO grades VALUES('1021', 'A');
INSERT INTO grades VALUES('1022', 'E');
INSERT INTO grades VALUES('1023', 'D');
INSERT INTO grades VALUES('1024', 'B');
INSERT INTO grades VALUES('1025', 'A');
INSERT INTO grades VALUES('1026', 'A');
INSERT INTO grades VALUES('1027', 'D');
INSERT INTO grades VALUES('1028', 'B');
INSERT INTO grades VALUES('1029', 'A');
INSERT INTO grades VALUES('1030', 'C');
INSERT INTO grades VALUES('1031', 'F');
SELECT Grade, GradeCount / SUM(GradeCount)
FROM (SELECT Grade, COUNT(*) As GradeCount
FROM myTable
GROUP BY Grade) Grades
In any sql server version you could use a variable for the total of all grades like this:
declare #countOfAll decimal(18, 4)
select #countOfAll = COUNT(*) from Grades
select
Grade, COUNT(*) / #countOfAll * 100
from Grades
group by Grade
You can use a subselect in your from query (untested and not sure which is faster):
SELECT Grade, COUNT(*) / TotalRows
FROM (SELECT Grade, COUNT(*) As TotalRows
FROM myTable) Grades
GROUP BY Grade, TotalRows
Or
SELECT Grade, SUM(PartialCount)
FROM (SELECT Grade, 1/COUNT(*) AS PartialCount
FROM myTable) Grades
GROUP BY Grade
Or
SELECT Grade, GradeCount / SUM(GradeCount)
FROM (SELECT Grade, COUNT(*) As GradeCount
FROM myTable
GROUP BY Grade) Grades
You can also use a stored procedure (apologies for the Firebird syntax):
SELECT COUNT(*)
FROM myTable
INTO :TotalCount;
FOR SELECT Grade, COUNT(*)
FROM myTable
GROUP BY Grade
INTO :Grade, :GradeCount
DO
BEGIN
Percent = :GradeCount / :TotalCount;
SUSPEND;
END
This one is working well in MS SQL. It transforms varchar to the result of two-decimal-places-limited float.
Select field1, cast(Try_convert(float,(Count(field2)* 100) /
Try_convert(float, (Select Count(*) From table1))) as decimal(10,2)) as new_field_name
From table1
Group By field1, field2;
I had a similar issue to this. you should be able to get the correct result multiplying by 1.0 instead of 100.See example Image attached
Select Grade, (Count(Grade)* 1.0 / (Select Count(*) From MyTable)) as Score From MyTable Group By Grade