Calculating Grades in SQL - sql

Very simply, I need to find student grades using SQL.
If, for example, I have following table that define grades
Marks (int)
Grade (Char)
and the data like this:
Marks | Grade
__90 | A+
__80 | A
__70 | A-
__60 | B
__50 | C
__40 | D
Okay, having said that, if I have a student that gained marks 73, how do I calculate her grade using above gradings in SQL.
Thank you so much...

You want the highest value below or equal to your value, substitue 73 for your value...
select top 1 Grade from TableName where Mark <= 73 order by Mark desc

Assuming your GradeCutoff table is created with something like:
CREATE TABLE GradeCutoff
( mark int
, grade char(3)
)
and you want to check #studentMark
SELECT grade
FROM GradeCutoff
WHERE mark =
( SELECT max(mark)
FROM gradeCutoff
WHERE #studentMark >= mark
)
;
Note: you may also have to add a (0, 'E') row in your cutoff table.

I think you should define a UDF for this which takes student markes as parameter and returns the grade according to the table given.
Then you can get grade for any student from student table as -
select studentID, getGrade(studentMarks) from student
Here getGrade(studentMarks) is UDF and studentMarks is column in student table with marks (for eg: in your case it is 73)
HINT: You need to use CASE construct in the UDF to get the grade.

SELECT Grade FROM 'table name here' WHERE student_mark <= 79 AND student_mark >= 70 - in order to be more specific I would need to see the actual layout of the tables. But, something to that affect would work.

If the marks are actually regular multiples of 10, you could look into SQLs MOD function

Since I didn't find any way to do it using MySQL, I had to do some PHP programming to achieve the result. The goal is to get the closest value from gradings.
Okay suppose, we have the grades as defined in my question.
$MarksObt = 73 <- Marks obtained by the student:
Step 1: Get grades from mysql ordered by Marks in ASC order (ASC order is important):
SELECT marks, grade FROM gradings ORDER BY marks
Step 2: Create an array variable "$MinGrades". Loop through the result of above query. Suppose MySQL results are stored in "$Gradings" array. Now on each iteration, do the following:
Subtract the $Grading['marks'] From $MarksObt
If result is greater than or equal to 0, add the result to "$MinGrades" array
Step 3: When loop ends, the "$MinGrades" array's first element will be the closest value ... DONE
Below is the PHP code that implements the above:
$MinGrades = array();
foreach($Gradings as $Key=>$Grading){
$Subtract = $MarksObt - $Grading['marks'];
if( $Subtract >= 0 )
array_push($MinGrades, array($Key=>$Subtract))
}
$GradeKey = key($MinGrades[0]); // Get key of first element in the array
print $Gradings[$GradeKey]['grade'];
If you have some better approach, please mention here.
Thanks for your contribution...

Related

Generate columns from values returned by SELECT

I've got a query that returns data like so:
student
course
grade
a-student
ENG-W05
100
a-student
MAT-W05
85
a-student
ENG-W06
100
b-student
MAT-W05
90
b-student
SCI-W05
75
The data is grouped by student and course. Ideally, I'd like to have the above data transformed into the below:
student
ENG-W05
MAT-W05
ENG-W06
SCI-W05
a-student
100
85
100
NULL
b-student
NULL
90
NULL
75
So, after the transformation, each student only has one record, with all of their grades (and any missing courses graded as null).
Does anyone have any ideas? Obviously, this is fairly simple to do if I take the data out and transform it in a language (like Python), but I'd love to get the data in the desired format with an SQL query.
Also, would it be possible to have the columns order alphabetically (ascending)? So, the final output would be:
student
ENG-W05
ENG-W06
MAT-W05
SCI-W05
a-student
100
100
85
NULL
b-student
NULL
NULL
90
75
EDIT: To clarify, the values in course aren't known. The ones I provided are just examples. So ideally, if more course values found there way into that first query result (the first table), they would still be mapped to columns in the final result (without needing to change the query). In reality, I actually have >1k distinct values for the course column, and so I can't manually write out each one.
demos:db<>fiddle
You can use conditional aggregation for that:
SELECT
student,
SUM(grade) FILTER (WHERE course = 'ENG-W05') as eng_w05,
SUM(grade) FILTER (WHERE course = 'MAT-W05') as mat_w05,
SUM(grade) FILTER (WHERE course = 'ENG-W06') as eng_w06,
SUM(grade) FILTER (WHERE course = 'SCI-W05') as sci_w05
FROM mytable
GROUP BY student
The FILTER clause allows to aggregate only some specific records. So this one aggregates all records for a specific course.
Finding the correct aggregate function could be difficult. Here SUM() does the job, as there's only one value per group. MAX() or MIN() would do it as well. It depends on your real requirement. If there's really only one value per group, it doesn't matter, you just need to do any aggregation.
Instead of FILTER clause, which is Postgres specific, you could use the more SQL standard fitting CASE clause:
SELECT
student,
SUM(
CASE
WHEN course = 'ENG-W05' THEN grade
END
) AS eng_w05,
...
You can use the conditional aggregation as follows:
select student,
max(case when course = 'ENG-W05' then grade end) as "ENG-W05",
max(case when course = 'MAT-W05' then grade end) as "MAT-W05",
max(case when course = 'ENG-W06' then grade end) as "ENG-W06",
max(case when course = 'SCI-W05' then grade end) as "SCI-W05"
from (your_query) t
group by student

Tricky aggregation Oracle 11G

I am using Oracle 11G.
Here is my data in table ClassGrades:
ID Name APlusCount TotalStudents PctAplus
0 All 44 95 46.31
1 Grade1A 13 24 54.16
2 Grade1B 11 25 44.00
3 Grade1C 8 23 34.78
4 Grade1D 12 23 52.17
The data (APlusCount,TotalStudents) for ID 0 is the sum of data for all classes.
I want to calculate how each class compares to other classes except itself.
Example:
Take Grade1A that has PctAplus = 54.16.
I want to add all values for Grade1B,Grade1C and Grade1D which is;
((Sum of APlusCount for Grade 1B,1C,1D)/(Sum of TotalStudents for Grade 1B,1C,1D))*100
=(31/71)*100=> 43.66%
So Grade1A (54.16%) is doing much better when compared to its peers (43.66%)
I want to calculate Peers collective percentage for each Grade.
How do I do this?
Another approach might be to leverage the All record for totals (self cross join as mentioned in the comments), i.e.,
WITH g1 AS (
SELECT apluscount, totalstudents
FROM grades
WHERE name = 'All'
)
SELECT g.name, 100*(g1.apluscount - g.apluscount)/(g1.totalstudents - g.totalstudents)
FROM grades g, g1
WHERE g.name != 'All';
However I think that #Wernfried's solution is better as it doesn't depend on the existence of an All record.
UPDATE
Alternately, one could use an aggregate along with a GROUP BY in the WITH statement:
WITH g1 AS (
SELECT SUM(apluscount) AS apluscount, SUM(totalstudents) AS totalstudents
FROM grades
WHERE name != 'All'
)
SELECT g.name, 100*(g1.apluscount - g.apluscount)/(g1.totalstudents - g.totalstudents)
FROM grades g, g1
WHERE g.name != 'All';
Hope this helps. Again, the solution using window functions is probably the best, however.
I don't know how to deal with "All" record but for the others this is an approach:
select Name,
100*(sum(APlusCount) over () - APlusCount) /
(sum(TotalStudents) over () - TotalStudents) as result
from grades
where name <> 'All';
NAME RESULT
=================================
Grade1A 43.661971830986
Grade1B 47.142857142857
Grade1C 50
Grade1D 44.444444444444
See example in SQL Fiddle

How to execute a LIKE query against a DECIMAL (or INTEGER) field?

Is it possible to execute a LIKE statement against a table column that contains DECIMAL types? Or else, what would be the best way to select matching rows given a number in a decimal (or integer) field?
E.g.:
Name Age
... ...
John 25
Mary 76
Jim 45
Erica 34
Anna 56
Bob 55
Executing something like SELECT * FROM table WHERE age LIKE 5 would return:
Name Age
John 25
Jim 45
Anna 56
Bob 55
It is not clear from your question what exactly you are trying to achieve, but based on the example query, the filtering you need to do should be achievable using normal arithmetic operators.
SELECT * FROM table WHERE MOD(age, 10) = 5 -- All records where the age ends in 5
Or:
SELECT * FROM table WHERE MOD(age, 5) = 0 -- All records where age is divisible by 5
Now that you clarified that though you are using a DECIMAL field you are not actually using it as a numeric value (as if you would, the requirement wouldn't exist), the answers given by others are reasonable - convert the field to a text value and use LIKE on it.
Alternatively, change the type of the field to something that is more suitable to the way you are using it.
You can convert your decimal field to varchar and then apply like.
If you create a query
select name from table where age like '%5%'
you could achieve this (at least in mysql and db2)
But if you prefer to match a number you should use something like:
select name from table where age > minimum and age < maximum
Or try to compare against a modulo if you are really interested in querying on the last number.

Possible To Run an SQL Loop to Increment The Value Being Selected?

I've looked into Dynamic SQL and the inc() function, but neither are really what I'm after.
Say I have a database like this:
grade name age
9 Bob 9
10 Sue 11
11 Larry 15
9 Joe 8
10 Carrot 10
I want to create a table that first selects all the rows with the lowest grade (9) then displays the oldest. It then goes through and searches for the next highest grade (10) and displays the oldest. Then goes to the next highest grade (11) and displays the oldest.
I'd like for them all to be in the same table and not have to write out a separate SQL call and different PHP variables for each grade.
This is the SQL call I have right now:
$query = "SELECT * FROM horses WHERE grade='1' ORDER BY points DESC LIMIT 1" or die(mysql_error());
Is there a way I can make the grade column increment until it reaches the highest number in the database?
Thanks for any suggestions.
You don't need a loop for this if I understand your request. Instead, you need a MAX() aggregate grouped by grade. The following method should work independently of your RDBMS. It relies on a JOIN against a subquery which returns the greatest age per group to get the age/group pair and join that back against the main table to retrieve the name (and other columns as needed).
SELECT
horses.grade,
horses.name,
horses.age
FROM
horses
JOIN (
SELECT grade, MAX(age) as maxage
FROM horses
GROUP BY grade
) ma ON horses.grade = ma.grade AND horses.age = ma.maxage
ORDER BY grade ASC
Here is an example on SQLFiddle.com
Returns:
GRADE NAME AGE
9 Bob 9
10 Sue 11
11 Larry 15
It is generally far faster an less resource-intensive to do one query instead of multiple queries in a loop, so this should be the approach whenever possible.

How to retrieve data that is not in the same order as the query in SQL?

I am trying to retrieve a record from a table in SQL.
Here is what I want. For example:
I have a table name studentScore with two columns:
studentName ----- Scores
John Smith ----- 75,83, 96
I want to do this: When I type the score in a search box, I want it to show me the name of the student. For example: I could type "83, 96, 75", (the scores can be in any order) and this should show me the student name "John Smith". But I'm wondering how we could specify in the WHERE clause so that it picks up the correct record, if what we type in the box is not in the same order as the original data in the column?
Your issue is that your data is not properly normalized. You are putting a 1 to n relationship into a single table. If you'd reorganize your tables like such:
Table Students
id name
1 John Smith
Table Scores
studentId score
1 75
1 83
1 96
You could do a query like:
select st.name from Students st, Score sc where st.id = sc.studentId and sc.score in ("83", "75", "96")
This also helps if you want to do other queries, like find out which students have a score of at least X, which would be otherwise impossible with your existing table layout.
If you must stick with your existing layout, which I don't recommend, however you could split up the user input and then do a query like
select from studentScore where score like '%75%' or score like '%83%' or score like '%96%'
But i really would refrain from doing so.
I suppose it is solvable, but it would be simpler if the scores for each student were stored as separate rows, for example in a scores table. Otherwise, the code would have to permute the entry into every conceivable order. Or the scores entry would have to be in a standard order somehow.
If you do not want to create a new table for Scores, - e.g. with StudentId, Score columns -, you may sort the numbers before storing them.
This way, when someone types a query, you sort those numbers as well and just compare it to the stored strings.
If you need the original position of the scores, you can store those in a separate field.
Improve your database schema...this does not satisfy even the first normal form (http://en.wikipedia.org/wiki/Database_normalization#Normal_forms).
Improving the schema will save you plenty of headaches in the future (stemming from update anomalies).
No sql table should have multiple values for an attribute (in the same column). Are the scores stored as a string? If so, your query will be more complicated and you're wasting the point of the DB.
however, to your question:
SELECT col4, col3, col2 FROM students WHERE col1 = 57;
this will return columns 4, 3, and 2 in that order (4,3,2) even if they are saved in the order 1, 2, 3, 4. SQL returns the things you ask for in the order you ask for them.
So yeah, I agree with everyone else that this design is crap. If you were to normalize this table properly, you would be able to very easily get the data you need.
However, this is how you could do it with the current structure. Split the user input into discrete scores. Then, pass each value into the procedure.
CREATE PROCEDURE FindStudentByScores
(
#score1 AS VARCHAR(3) = NULL
,#score2 AS VARCHAR(3) = NULL
,#score3 AS VARCHAR(3) = NULL
)
AS
BEGIN
SELECT *
FROM [Students]
WHERE ( #score1 IS NULL
OR [Scores] LIKE '%' + #score1 + '%' )
AND ( #score2 IS NULL
OR [Scores] LIKE '%' + #score2 + '%' )
AND ( #score3 IS NULL
OR [Scores] LIKE '%' + #score3 + '%' )
END
You could use Regular expressions or the Like operator
A regexp solution could look like
SIMILAR TO '%(SCORE1|SCORE2|SCORE3)%'
That's the easiest way to go
but I recommend you changing your entire table structure as been mentioned now
a couple of times, since you have no possibility to take advantage of an index or key
which will exhaust the computer in matter of a couple tens of visitors
This is an example of where database normalization should help you a lot.
You could store your data like this
(Edit: if you want to keep the order you can add an order column)
studentName Scores Order
John Smith 75 1
John Smith 83 2
John Smith 96 3
Foo bar 73 1
Foo bar 34 2
........
But if you are stuck with the current model your next best option is to have the Scores column sorted, then you just need to take the search string from the textbox, sort and format it correctly, then you can search.
Lastly if the scores is not sorted in the table you can create all possible combinations
75, 83, 96
75, 96, 83
83, 75, 96
83, 96, 75
96, 75, 83
96, 83, 75
and search for them all with OR.