Better Database design for hibernatre? - mysql

I am new to database design. I am using MySQL
I have a Form table which contains a field Country
Since the values in this columns can be one from the country list only, so I made this column int instead of varchar and put the list in a new table. shown as below:
Instead of having country column like
country varchar(255) NOT NULL
I made it countryCode with datatype INT and added a Foreign Key to table CountryMaster which is shown as below
...
countryCode INT NOT NULL,
FOREIGN KEY (countryCode) REFERENCES CountryMaster (countryCode),
...
create table CountryMaster(
countryCode INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255)
);
It is working fine, but the issue is that the same table contains many more column like formStatus (with possible values Draft, Saved, Approved), gender (Male, Female), bloodGroup (A, B, O), rHFactor (+ve, -ve), occupation (Service, Business, Student, Unemployed) etc. If I will make separate table for each such column I will end up in 20 to 25 Master tables.
I also tried to put all such Master Date in a single table i.e. CommonMaster with columns type, key and value shown as below:
create table CommonMaster(
commonCode INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
type VARCHAR(255) NOT NULL,
key INT NOT NULL,
value VARCHAR(255) NOT NULL
);
with values like:
code type key value
7 BLOOD_GROUP 1 A
8 BLOOD_GROUP 2 B
9 BLOOD_GROUP 3 O
10 RH_FACTOR 1 +ve
11 RH_FACTOR 2 -ve
12 OCCUPATION 1 Service
13 OCCUPATION 2 Profession
14 OCCUPATION 3 Business
15 OCCUPATION 4 Student
16 OCCUPATION 5 Unemployeed
I want to know that what is a better approach and why. I also have to use hibernate orm in the project. So the design should be pertaining to that.

I prefer a hundreds master tables than a common table with all the types; more structure, more organization, more adaptive for add, delete and update the value of then, also for reports.

Related

In MySQL, how do I join two fields in one table to the same table's primary key?

I am working with a MySQL backend (version 5.7.19), and a LibreOffice Base frontend(version 7.0.6.2 x64) on 64-bit Windows. I have a table that lists personnel with a primary key id. I also have a workorders table that has an "entered by" field and a "reviewed by" field, both of which need to store the id of the personnel who complete those tasks. If I wanted to have two foreign keys in one table pointing to the same table's primary key, what would my SELECT statement need to look like?
In my case, I have a table 'personnel' with two fields with ID as the primary key, thus:
ID
Name
1
John Smith
2
John Adams
3
Samuel Adams
which can be created and populated thus:
CREATE TABLE orders(
workorder int(10) unsigned NOT NULL AUTO_INCREMENT,
entered_by int(10) unsigned NOT NULL,
reviewed_by int(10) unsigned NOT NULL,
PRIMARY KEY (workorder),
FOREIGN KEY (entered_by) REFERENCES personnel(id),
FOREIGN KEY (reviewed_by) REFERENCES personnel(id)
);
ALTER TABLE orders AUTO_INCREMENT = 1;
INSERT INTO personnel(name) VALUES('John Smith');
INSERT INTO personnel(name) VALUES('John Adams');
INSERT INTO personnel(name) VALUES('Samuel Adams');
Also, a table 'orders' with three fields with entered_by and reviewed_by as foreign keys to personnel.id
workorder
entered_by
reviewed_by
1
2
3
2
3
1
which can be created and populated thus:
CREATE TABLE orders(
workorder int(10) unsigned NOT NULL AUTO_INCREMENT,
entered_by int(10) unsigned NOT NULL,
reviewed_by int(10) unsigned NOT NULL,
PRIMARY KEY (workorder),
FOREIGN KEY (entered_by) REFERENCES personnel(id),
FOREIGN KEY (reviewed_by) REFERENCES personnel(id)
);
INSERT INTO orders(entered_by, reviewed_by) VALUES (2,3);
INSERT INTO orders(entered_by, reviewed_by) VALUES (3,1);
I know how to
SELECT workorder, personnel.name AS entered
FROM orders JOIN personnel
ON personnel.id = orders.entered_by
ORDER BY orders.workorder;
which results in
workorder
entered
1
John Adams
2
Samuel Adams
and how to
SELECT workorder, personnel.name AS entered
FROM orders JOIN personnel
ON personnel.id = orders.entered_by
ORDER BY orders.workorder;
which yields:
workorder
reviewed
1
Samuel Adams
2
John Smith
but I'm not sure how to put them into a single query (that I can use in a query form in Base), so that it will display:
workorder
entered
reviewed
1
John Adams
Samuel Adams
2
Samuel Adams
John Smith
Yes, according to relational algebra every pair of tables can have multiple relationships between them.
For example, the typical illustration of this case, is a money_transfer table that records money flowing from one account to another. In this case this table will have two foreign keys against the account table: one to indicate where the money is coming from, and the other to indicate where money is going to.
Other pairs of tables can have many more relationships between them. I've seen cases for authorization purposes and auditing, that have many FKs.
For example, the requirements stated that the app needed to record who entered the data, who verified it, who accepted it, and who executed the transaction; sometimes it even has "first-level of approval" (for amounts above US$10K) and "second-level of approval" (for amounts above $100K).
EDIT - Joining the Same Table Multiple Times
As requested, when joining the same table multiple times you need to assign different names to each "instance" of the table. Typically this is done by adding an alias to each table instance according to its role.
In this case the roles are "entered by" and "reviewed by", so the query can use the aliases e and r respectively. The query could take the form:
select o.*, e.name, r.name
from workorders o
join personnel e on e.id = o.entered_by
join personnel r on r.id = o.reviewed_by

modeling issue with field with 4 values (1 or more)

lets say I have an account object in my application, which currently represented as:
CREATE TABLE Account (
accountId int NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
PRIMARY KEY (accountId)
);
Now, Account object need to also have Solution field...and Status have 4 different possible values:
Solution1, Solution2, Solution3, Solution4
What would be the right way to represent it in the database?
Account can have few statuses, and status can have few accounts...
So at first I thought create in the db table of Solutions and than have another table to hold the relationship, but its seems too complicated for a field that have only 4 possible values...
Create a junction table to represent the relationships between accounts and solutions:
CREATE TABLE account_solution (
accountId int NOT NULL,
solutionId int NOT NULL
PRIMARY KEY (accountId, solutionId)
)
For your solution table, since there are only 4 values, you might be able to take advantage of MySQL's enum type, e.g.
CREATE TABLE solution
solutionId int NOT NULL PRIMARY KEY,
status ENUM('Solution1', 'Solution2', 'Solution3', 'Solution4')
);
You can use set Mysql SET type
CREATE TABLE Account (
accountId int NOT NULL AUTO_INCREMENT,
name varchar(255) NOT NULL,
status set('Solution1','Solution2','Solution3','Solution4') NOT NULL,
PRIMARY KEY (accountId)
);
And if you want to select a specific status
SELECT *
FROM `Account`
WHERE FIND_IN_SET( 'Solution2', `status` ) >0

MySQL query to get all (additional) symptoms and diseases [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 7 years ago.
Improve this question
I have a database which contains main symptoms, additional symptoms and diseases. I need to replace 2 queries because I am sure my first query is not efficient and my 2nd is not correct at all. I hope anyone can help me because I am new to this area..
Database explanation:
The database is being used by a medical app:
The user selects a specific bodypart
The app lists all main symptoms of that specific bodypart
The user selects a main symptom (common or less common)
The app lists all diseases of the selected main symptom. There also appear 2 checkboxes (additional symptoms) that can be checked by the user. The order of the listed diseases (d_weight) depends on age, gender, selected main symptom and what boxes the user has checked. A disease with d_weight <= 5 is considered to be a common disease. A disease with d_weight > 5 is considered to be less-common. The possibilities of user input (age, gender, bodypart, main symptom) is stored in the symptom_disease_combi table.
asa_id is the id of all symptom that apply (addtional symptoms that are checked by the user)
asc_id is the id of all possibilities of additional symptoms that belong to the specific main symptom. For example, asc_id = 0, in case there is no additional symptom selected. asc_id = 1, in case only additional symptom 'Insomnia' is selected. asc_id = 2, in case both 'Insomnia' and 'Blowing up' are selected.
1. Query to get all symptoms of a specific bodypart (can be improved):
SELECT DISTINCT s.name
, s.id
, sdc.s_common
FROM symptom as s
, symptom_disease_combi as sdc
WHERE sdc.age = ".$age."
AND sdc.gender = ".$gender."
AND sdc.bodypart = ".$bodypart."
AND sdc.s_id = s.id
2. Query to get all diseases and additional symptoms of selected symptom (doesn't work):
SELECT DISTINCT d.name
, d.id
, sdc.d_weight
, adls.id
, adls.name
FROM disease as d
, symptom_disease_combi as sdc
, symptom as s
, adlsymptom as adls
WHERE sdc.age = ".$age."
AND sdc.gender = ".$gender."
AND sdc.bodypart = ".$bodypart."
AND s.id = ".$sid."
AND sdc.s_id = s.id
3. Database structure (please let me know if my design can be improved)
CREATE TABLE symptom
(id INT NOT NULL AUTO INCREMENT
,name VARCHAR(100) DEFAULT NULL
,critical INT NOT NULL
,PRIMARY KEY (id)
) ENGINE=MyISAM;
id name critical
1 Behavioral disturbances 1
2 Ear pressure 0
3 Elevated temperature 0
4 Fainting 0
5 Head pain 0
CREATE TABLE disease (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(100) DEFAULT NULL,
critical int(11) NOT NULL,
description text NOT NULL,
tests text NOT NULL,
treatment text NOT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM AUTO_INCREMENT=19 DEFAULT CHARSET=latin1
id name critical description tests treatment
1 Adjustment disorder 0 lorem ipsum lorem ipsum lorem ipsum
2 ADHD 0 lorem ipsum lorem ipsum lorem ipsum
3 Drug reaction 0 lorem ipsum lorem ipsum lorem ipsum
4 Seizure (epilepsy) 1 lorem ipsum lorem ipsum lorem ipsum
CREATE TABLE adlsymptom (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(100) NOT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=latin1
id name
1 Insomnia
2 Blowing up
3 Depressed
4 Drug abuse
CREATE TABLE adlsymptom_apply (
id int(11) NOT NULL,
as_id int(11) NOT NULL,
PRIMARY KEY (id,as_id),
KEY FK_additional_symptom_that_apply (as_id)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
id as_id
1 1
1 2
CREATE TABLE adlsymptom_combi (
id int(11) NOT NULL,
as_id int(11) NOT NULL,
PRIMARY KEY (id,as_id),
KEY FK_additional_symptom_combination (as_id)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
id as_id
1 1
2 2
3 1
3 2
CREATE TABLE symptom_disease_combi (
id int(11) NOT NULL AUTO_INCREMENT,
age int(11) NOT NULL,
gender int(11) NOT NULL,
bodypart int(11) NOT NULL,
s_id int(11) NOT NULL,
s_common int(11) NOT NULL,
asc_id int(11) NOT NULL,
asa_id int(11) NOT NULL,
d_id int(11) NOT NULL,
d_weight int(11) NOT NULL,
PRIMARY KEY (id),
KEY FK_symptom (s_id),
KEY FK_additional_symptom_combination (asc_id),
KEY FK_additional_symptom_that_apply (asa_id),
KEY FK_disease (d_id)
) ENGINE=MyISAM AUTO_INCREMENT=65 DEFAULT CHARSET=latin1
id age gender bodypart s_id s_common asc_id asa_id d_id d_weight
1 1 1 1 1 1 0 1 1 1
2 1 1 1 1 1 0 1 2 2
3 1 1 1 1 1 0 1 3 3
4 1 1 1 1 1 0 1 11 4
UPDATE 1:
critical in disease and symptom is created to tell the user that they need to go to the hospital immediately, when they click on the disease or symptom
age, gender, bodypart are id's, so age = 1, means 0-5, age = 2 means 6-17, age = 3 means 18-59 and age = 4 means 60+.
Please take a look at the design of the application, it will help a lot to understand the design of the database: http://i.imgur.com/p9QP1av.png Btw, in the design; cause == disease...
asa_id refers to id of adlsymptom_apply
asc_id referst to id of adlsymptom_combi
The 'distinct' was used to get all symptoms/diseases just 1 time. I am sure it's not needed, but I don't know how to fix it.
ditch 2 symptom tables, go with one (symptom) and 1 intersect table (sdc)
i would not add a new column to symptom, such as status/level trying to jack up a symptom
in importance to main or secondary, despite the temptation, because this could easily
make it not flexible.
for instance, fainting may seem main for once disease/condition but it could skew it in general
go for generality, therefore 1 table for symptoms, you correctly have a d_weight in sdc
i like your sdc.d_weight concept.
for instance, fainting might have some weight with epilepsy, but not for the flu. the whole concept
is screwed up when someone with the flu is prescribed Zarontin and not Tamiflu
since i like your sdc.d_weight concept, i wonder why you went with the additional symptoms table
in table sdc, you have keys/indexes with names starting with "FK_". Hopefully you have actual FK constrainst
and not just naming conventions that make you think you have them (FK's, you don't have them)
for instance, real FK's for bodypart, symptom, and disease/condition.
as the user selects symptoms, remove them from the GUI's ability to add that symptom again for search
this cuts down work and simplifies querying by ditching the secondary table (line 1 of what i wrote and suggest)
note, again, that your use of KEY (which is a synonym for index, not foreign key constraint), just creates
an index ...
create table symptom
(
id int not null auto_increment primary key,
name varchar(100) not null, -- if you can't put a name to it, don't have it
critical int not null
)ENGINE=MyISAM default charset=latin1;
create table disease
(
id int not null auto_increment primary key, -- don't mix and match int with int(11)
name varchar(100) not null, -- if you can't put a name to it, don't have it
critical int not null,
-- etc columns, text
)ENGINE=MyISAM default charset=latin1;
.
create table symptom_disease_combi
( -- a.k.a. sdc
id int not null auto_increment primary key, -- don't mix and match int with int(11)
age int not null,
gender char(1) not null, -- int(11) is overkill
bodypart int not null,
s_id int not null,
s_common int not null,
asc_id int not null,
asa_id int not null,
d_id int not null,
d_weight int not null,
-- additional indexes (note pk above, so it is there and happy)
-- note that some of your indexes (your KEYS) are worthless for the queries in question
-- however they may be useful for other queries in your system
-- for instance your asc and asa indexes will not be useful as they won't be picked up
-- in relation to the question posed
--
-- you will need to find the proper balance of index bloat based on system needs
INDEX idx_sdc_siddid (s_id,d_id,bodypart), -- composite, general purpose
INDEX idx_sdc_didsid (d_id,s_id,bodypart), -- ditto but reverse
INDEX idx_sdc_ascid (asc_id),
INDEX idx_sdc_asaid (asa_id),
-- put constraints here:
CONSTRAINT `fk_sdc_bodypart` FOREIGN KEY (bodypart) REFERENCES bodypart(id),
CONSTRAINT `fk_sdc_sid` FOREIGN KEY (s_id) REFERENCES symptom(id), -- don't mix and match int and int(11)
CONSTRAINT `fk_sdc_did` FOREIGN KEY (d_id) REFERENCES disease(id)
-- are there others, such as asc asa tables ??
)ENGINE=MyISAM default charset=latin1;
i think you need to think long and hard about the age column
is sdc going to be loaded with a new row, one for each age, 50 to 120, for gender='M', for prostrate cancer?
i am basing this on your age=xxxxx line, maybe you mean >=. or for leukemia, <= ... i think
your schema needs a bit of thought
so i am excluding age from the below query
-- for the next query, i wonder why you need a distinct?
-- is there more than 1 sdc.s_common for given symptom row ? only you would know
-- if not, ditch the distinct
-- the order of your join on clause matters, make it follow a good index so results come back fast
-- i am not suggesting these are covered indexes, but you will minimize table scan activity
select distinct s.name, s.id, sdc.s_common
from symptom s
join symptom_disease_combi sdc
on sdc.s_id=s.id and sdc.bodypart=".$bodypart." and sdc.gender= ".$gender."
-- not my comment above about age
-- incorporate weights as needed in a computed column
those are my suggestions, and good luck

correct way to place data in table OneToONe

I am confused about the correct/most efficient way to place data in my dababase table when there is a OneToOne relationship.
For example, I have a users table.
I now wish for each user to be able to state his current country location.
i then want to be able to search the datatable for users by current location.
The way that I have done this is to create 3 separate tables. i.e
table one - users : just contains the user information:
CREATE TABLE users(
id MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
firstName VARCHAR(30) NOT NULL,
lastName VARCHAR(40) NOT NULL,
);
Table two country list: a list of countries and respective Ids for each country
PHP Code:
CREATE TABLE countrylist(
country_id MEDIUMINT UNSIGNED NOT NULL,
country VARCHAR(60) NOT NULL,
INDEX country_id ( country_id, country ),
INDEX countrylist (country, country_id ),
UNIQUE KEY (country)
);
Table 3; contains the userId and the countryId he lives in:
PHP Code:
CREATE TABLE user_countrylocation(
country_id VARCHAR(60) NOT NULL,
id MEDIUMINT UNSIGNED NOT NULL,
INDEX country_id (country_id, id ),
INDEX user_id (id, country_id )
);
Alternatively, should I place the countryId in the users table and completely get rid of the user_countrylocation. i.e in each user column, I will place a country_id for the country he lives in.
The problem is that I have over 20 similar tables as above that give details on users; i.e languages spoken, age-group, nationality etc.
My concerns is that if I place this unique information in each users column in the user table, then what would be the most efficient way to search the database: that is why I opted for the style above.
So, I really request for some advice on the most efficient/correct way to plan the database.
If you are going to have a huge data then you should keep the same approach and use the following method to keep the one to one constraint satisfied
if you don't have a huge data then you should keep the look up tables like country and use the reference for user in a column. but then you may need to allow them nulls that is make such optional information columns nullable.
The most efficient and exactly correct way is to first delete the data from the third table "user_countrylocation" for the user to be updated. Then insert the new location for the user. don't forget to use transaction.
your table 3 should have
country_id MEDIUMINT UNSIGNED NOT NULL,
instead of
country_id VARCHAR(60) NOT NULL,
and also change tyhe column name from id to user_id in all tables.
if you are using a stored procedure it would be like
create procedure sp_UpdateUserCurrentCountry (
#userID MEDIUMINT UNSIGNED,
#CountryID MEDIUMINT UNSIGNED)
begin
as
delete from user_countrylocation
where user_id = #userID
insert into user_countrylocation
(
country_id,
user_id
)
values
(
#CountryID,
#userID
)
END
One to One relations are usually mapped via Foreign Keys linking the two tables together. A third mapping table is only required for Many to Many relationships. So, you should ideally have a Foreign Key Country_ID in your Users table.
Your SELECT query would then look like
SELECT * FROM Users
WHERE Country_ID = (
SELECT Country_ID FROM Countries
WHERE Country_Name = 'USA'
);

MySQL rating database structure

I'm trying to create a database that stores the students grade for each homework assignment I want to be able to store the grade and how many times that student got a certin grade for example a student got an A+ 30 times, for each student, as well as how many grades a student got. And how much a grade is worth in points for example an A is worth 4.3.
So I was wondering what is the best way to build my MySQL database what do I need to add and what do I need to drop and how can I store numbers like 4.3 for example.
My database so far
CREATE TABLE grades (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
grade INT UNSIGNED NOT NULL,
student_work_id INT UNSIGNED NOT NULL,
student_id INT UNSIGNED NOT NULL,
date_created DATETIME NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE work (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
student_id INT UNSIGNED NOT NULL,
title TEXT NOT NULL,
content LONGTEXT NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS student (
id int(8) NOT NULL auto_increment,
student varchar(20) NOT NULL,
PRIMARY KEY (`id`)
)
example of output.
student | grade | count
1 A 10
1 C 2
1 F 4
2 B 20
2 B- 3
2 C+ 10
student | grade | points
1 A 4.3
2 B+ 3.3
3 B- 2.7
4 D+ 1.3
student | total grades
1 90
2 80
3 100
4 1
I think a student - grade - assignment schema would be ok. You should count the number of grades of a student with a query and not maintain that information in your database.
At a conceptual level (not strict mysql), I would do something like this.
table student (
id int (primary key)
varchar name
... // other student attributes (e.g. phone number)
)
table grade (
id int (primary key)
name varchar // e.g. "A+", "B-", etc.
float points // grade value in points
)
table assignment (
id int (primary key)
name varchar // assignment name
student_id int (foreign key) // student that completed the particular assignment
grade_id int (foreign key) // grade achieved in particular assignment
... // other assignment attributes (e.g. due date)
)
Many would suggest that you have a student table, a grade table and a table that relates the student and grade table. You could then do counts based on the table that relates the two with a procedure. Many believe that having a grade and a student in the same table is poor relational design.
Unless you want to use a view, which is really not necessary in this case, you wouldn't store the amount of times a student got a certain grade; rather, you would make a query that gave you that information for each student when you needed it. One example using your current schema, to tell you how many times a particular student having id st_id got each grade, would be like
SELECT COUNT(1) FROM grades WHERE student_id = st_id GROUP BY grade
This will look at all the rows where student st_id got each grade and count them up separately.
As far as storing a mapping from letter grade to the amount of points that its worth, you could do one of two things - either have a table mapping from letter grade to point amount, or just store the point amounts directly. Personally, I think the former is a little bit cleaner as it allows you to change point amounts if the scale changes (not that that should happen often).
Other than that, I think you're doing pretty good. The only thing I would say is that if there is always a one-to-one mapping between grade and work, I might make grade part of the work table rather than separate them. That is, only if there isn't information in the grade table that doesn't belong in the work table.
Looking at your tables it appears that there's some normalizing that you can do, or I might be misunderstanding the objective. I would assume that students and work items are only connected by a grade, which would make your grades essentially a cross-reference table. As a result, you wouldn't reference the student_id column within the work table. Off the cuff, I would change it such that the tables look more like this:
CREATE TABLE student (
student_id int(8) NOT NULL auto_increment,
student varchar(20) NOT NULL,
PRIMARY KEY (student_id)
);
CREATE TABLE work (
work_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
title TEXT NOT NULL,
content LONGTEXT NOT NULL,
PRIMARY KEY (work_id)
);
CREATE TABLE grades (
grade_id INT UNSIGNED NOT NULL AUTO_INCREMENT,
grade decimal(4,2) UNSIGNED NOT NULL,
work_id INT UNSIGNED NOT NULL,
student_id INT UNSIGNED NOT NULL,
date_created DATETIME NOT NULL,
PRIMARY KEY (id)
);
Note that I changed the grade column to a decimal (I'm a SQL Server guy, so MySQL might be different), but any type that can handle non-integer numbers works. With that design, you can then pull your queries as needed to get the types of output you need.
Josh
assuming -
there's more than one student
there's more than one assignment / piece of work to be graded against
more than one student can complete a specific set work/assignment
only one grade can be earned per assignment
I'd go with -
a table for the definition of each set piece of work / a specific assignment.
a table to define each student
a third table to record the result of each assignment per student, ie to store the grade per assignment.
eg -
CREATE TABLE work (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
student_id INT UNSIGNED NOT NULL,
assignment_id int unsigned not null,
grade DECIMAL(1,1)
gradedesc VARCHAR(1)
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS student (
id int(8) NOT NULL auto_increment,
student varchar(20) NOT NULL
PRIMARY KEY (`id`)
)
Create table assignment (
id int(8) not null auto_increment,
title TEXT NOT NULL,
content LONGTEXT NOT NULL
primary key(id)
Then, create some views to display the output in the format you want, eg -
Create view graded_students as
(select s.student,
w.gradedesc as grade,
w.grade as points,
w.title
from work w,
student s
where w.student_id = s.id
and w.grade is not null )
Create view aggregate_grades as
(select s.student,
sum(w.grade) as "total grades",
from work w,
student s
where w.student_id = s.id
and w.grade is not null
group by s.student)
create view grades_by_assignment as
(select s.student,
a.title,
w.gradedesc as grade
from student s,
work w,
assignment a
where a.id = w.assignment_id
and s.id = w.student_id )
I'm more of an oracle and sql server person, so this may not be 100% mysql compliant (and I haven't tested it) but something like that would do the trick I think.
If you want to normalize the data even further you could break out the grade description/value to another table too, or just display the grade description in your application based on the stored value (eg grade value 4.5 always displays as "A", etc)