Database design: Foreign keys dilemma - mysql

I am building a web based application for a big company and i have a question about the database part. Lets first introduce you to the situation:
There are 4 very tables that are connected with foreign keys:
Table 1: roadlists
roadlist (roadListNumber, vehicleId, driverId, date, start, end, fuel,...)
roadListNumber - primaryKey
vehicleId - foreignKey
driverId - foreignKey
Table 2: drivers
drivers(driverId, firstName, middleName, lastName, and so on...)
driverId - primaryKey
Table 3: vehicles
vehicles(vehicleId, group, type, model, gpsDevice, and so on...)
vehicleId - primaryKey
Table 4: cargo_zones
cargo_zones(zoneId, name, permiter, area, latitures, longtitudes, and so on...)
zoneId - primaryKey
Table 5: roadLists_cargoZones_mapping
roadLists_cargoZones_mapping(roadListNumber, zoneId, spentFuel, tkm, mcm,...)
roadListNumber - foreignKey
So here is the problem:
If i delete a driver from drivers table the value in the roadlists table will be set to NULL or the whole row will be deleted based on the constraints. In the first case if it is set to NULL if somebody lists all the reports for some date he will not be able to see the driver's name who was on duty because its id does not link to any row in the drivers table. In the seconds case if the whole row is removed from the roadlists table the system loses important data.
So how do i deal with this kind of situations?

The preferred way (but not the only way):
Mark a driver as deleted (have a boolean deleted column, or call it IsActive), without actually deleting the driver or related information.
This way you can still historically report. One way to approach this is create views over the table; one view for AllDrivers and one for ActiveDrivers and join to these rather than the table directly, or just add the IsActive predicate to your query WHERE clauses.

The way you deal with this situation is DON'T DELETE the row from the Driver table.
This is information you want to keep. So, don't remove it.
What you might need is a way to mark the Driver row as inactive, archived, unavailable, or whatever. You've got a status change, NOT a delete.
Any queries where you need that driver EXCLUDED, you can add a predicate (a condition in the WHERE clause) to exclude those rows.

Related

MySQL : One to One Relationship Tables Merging

I am trying to simplify an application's database. In that database I have two tables let's say Patient and MedicalRecord. I know that two tables are said to be in One-to-One relationship iff that any given row from Table-A can have at most one row ine Table-B(It means there can be zero matchings).
But in my case, it is not at most, it is exactly. i.e., Every row in Patient should have exactly one row in MedicalRecord(no patient exist without a medical record).
Patient table has all personal details of the patient with his id as PK.
MedicalRecord talbe has details like his blood-group, haemoglobin, bp etc with his id as both PK and FK to the Patient.
My Question is, can I merge those two tables and create one table like,
PatientDetails : personal_details and blood-group, haemoglobin, bp etc
"bp" = "Blood pressure"? Then you must not combine the tables. Instead, it is 1:many -- each patient can have many sets of readings. It is very important to record and plot trends in the readings.
Put only truly constant values in the Patient -- name, birthdate (not age; compute that), sex, race (some races are more prone to certain diseases than others), not height/weight. Etc.
Sure, a patient may have a name change (marriage, legal action, etc), but that is an exception that does not affect the schema design, except to force you to use patient_id, not patient_name as a unique key.
Every patient must have a MedicalRecord? That is "business logic"; test it in the application; do not depend (in this case) on anything in the Database.
Both tables would have patient_id. Patients would have it as the PRIMARY KEY; MedicalRecord would haveINDEXed`. That's all it takes to have 1:many.
In situations where the tables are really 1:1 (optionally 1:0/1), I do recommend merging the table. (There are exceptions.)
If two tables have the same set of subrow values for a shared set of columns that is a superkey in both (SQL PRIMARY KEY or UNIQUE) then you can replace the two tables by their natural join. ("Natural join" is probably what you mean by "merge" but that is not a defined technical term.) Each original table will equal the projection of the join on that original's columns.
(1:1 means total on both sides, it does not mean 1:0-or-1, although most writing about cardinalities is sloppy & unclear.)

Insert/Update on table with autoincrement and foreign key

I have a table as such:
id entity_id first_year last_year sessions_attended age
1 2020 1996 2008 3 34.7
2 2024 1993 2005 2 45.1
3 ... ... ...
id is auto-increment primary key, and entity_id is a foreign key that must be unique for the table.
I have a query that calculates first and last year of attendance, and I want to be able to update this table with fresh data each time it is run, only updating the first and last year columns:
This is my insert/update for "first year":
insert into my_table (entity_id, first_year)
( select contact_id, #sd:= year(start_date)
from
( select contact_id, event_id, start_date from participations
join events on participations.event_id = events.id where events.event_type_id = 7
group by contact_id order by event_id ASC) as starter)
ON DUPLICATE KEY UPDATE first_year_85 = #sd;
I have one similar that does "last year", identical except for the target column and the order by.
The queries alone return the desired values, but I am having issues with the insert/update queries. When I run them, I end up with the same values for both fields (the correct first_year value).
Does anything stand out as the cause for this?
Anecdotal Note: This seems to work on MySQL 5.5.54, but when run on my local MariaDB, it just exhibits the above behavior...
Update:
Not my table design to dictate. This is a CRM that allows custom fields to be defined by end-users, I am populating the data via external queries.
The participations table holds all event registrations for all entity_ids, but the start dates are held in a separate events table, hence the join.
The variable is there because the ON DUPLICATE UPDATE will not accept a reference to the column without it.
Age is actually slightly more involved: It is age by the start date of the next active event of a certain type.
Fields are being "hard" updated as the values in this table are being pulled by in-CRM reports and searches, they need to be present, can't be dynamically calculated.
Since you have a 'natural' PK (entity_id), why have the id?
age? Are you going to have to change that column daily, or at least monthly? Not a good design. It would be better to have the constant birth_date in the table, then compute the ages in SELECT.
"calculates first and last year of attendance" -- This implies you have a table that lists all years of attendance (yoa)? If so, MAX(yoa) and MIN(yoa) would probably a better way to compute things.
One rarely needs #variables in queries.
Munch on my comments; come back for more thoughts after you provide a new query, SHOW CREATE TABLE, EXPLAIN, and some sample data.

MS-Access show only items that meet multiple criteria

I am new to Access and I am looking for a solution that is beyond the ability of the others in my company and may be beyond what access can do.
I have the following fields.
Date: Last Name: First Name: Test1: Test2: Test3:
I am looking for the following to happen.
On any single date a user may test multiple times.
If the user passes all three tests do not show any records with fails or any duplicate passes.
If the user fails any of the three tests, but has multiple failed records only show one.
If the user has the statement "NotUsed" in any field, but a pass in any other keep a single record for that date.
Thank You,
First, you need a primary key column in order to be able to easily and unambiguously identify each record. In Access this is easily achievable with a Autonumber column. Also, in the table designer, click the key symbol for this column. This creates a primary key index. A primary key is a must for every table.
Let us call this column TestID and let's assume that the table is named tblTest.
The problem is that your condition refers to several records; however, SQL expects a WHERE clause that specifies the conditions for each single record. So let’s try to reformulate the conditions:
Keep the record with the most passes for each user.
Keep records with "NotUsed" in any test field.
The first condition can be achieved like this:
SELECT First(TestID)
FROM
(SELECT TestID, [Last Name], [First Name] FROM tblTest
ORDER BY IIf(Test1='pass',1,0) + IIf(Test2='pass',1,0) + IIf(Test3='pass',1,0) DESC)
GROUP BY [Last Name], [First Name]
This gives you the TestID for each user with the most passes. Now, this is not the final result yet, but you can use this query as a subquery in the final query
SELECT * FROM tblTest
WHERE
Test1='NotUsed' OR Test2='NotUsed' OR Test3='NotUsed' OR
TestID IN ( <place the first query here...> )
Is this what you had in mind?
Another thought is about normalization. Your table is not normalized. You are using your table like an Excel sheet. As your database grows you'll get more and more into trouble.
You have two kinds of non-normalization.
One relates to the fact that each user's first name and last name might occur in several records. If, in future, you want to add more columns, like user address and phone number, then you will have to repeat these entries for each user record. It will become increasingly difficult to keep this information synchronized over all the records. The way to go is to have at least two tables: a user table and a test table where the user table has a UserID as primary key and the test table has this UserID as foreign key. Now a user can have many test records but still always has only one unique user record.
The other one (non-normalization) occurs because you have 3 Test fields in a single record. This is less of a problem if your tests always have the same structure and always require 3 tests per date, but even here you have to fall back to the "NotUsed" entries. There are several ways to normalize this, because a database can have different degrees of normalization. The tree ways:
Only one test table with the fields: TestID (PK), UserID (FK), Date, Result, TestNumber.
A test day table with the fields: TestDayID (PK), UserID (FK), Date + a test result table with the fields: TestResultID (PK), TestDayID (FK), Result, TestNumber
Then you can combine the two previous with this addition: Instead of having a TestNumber field, introduce a lookup table containing information on test types with the fields: TestTypeID (PK), TestNo, Description and in the other tables replace the column TestNumber with a column TestTypeID (FK).
See: How to normalize a table using Access - Part 1 of 4 or look at many other articles on this subject.

A correct way to make relationships between three tables

I am building a library database and I am stuck on one particular thing.
I have three tables :BookCopy, BookLoan and Members. It is not clear to me how to make the relationships between them, so a member can borrow a book(or books) and all this to be correctly reflected in my database.
My idea was to have a two many-to-many tables, so I add BoakLoansMembers and BookCopiesBookLoans . I am not sure if this is correct, and even if it is, I have no idea how to scipt so many tables.
So, now I am wondering what would be the best thing to be done in this case and why?
I'm guessing your BookCopy is to account for having X copies of book Y, and in that sense "books" are not loaned, "copies" of them are, right?
I think the best course of action is probably to realize the BookLoan table should be the many-to-many table. A copy is loaned to a member at a time and then returned. BookLoad should have the id's for the copy and the member, and the date loaned (as you have now, though it should be a datetime field NOT a varchar one) & date returned (like the loaned date, it should be a datetime, but should also be nullable to represent unreturned copies). You should also keep the unique (presumably auto-increment) id of the loan as it is very possible a member might check out the same copy multiple times.
I am guessing that perhaps you were originally conceptualizing the "loan" similar to a sales transaction, which could work; but you would want a loanCopies table, and wouldn't want the dateReturned on the loan then since different copies could be returned independently.
Edit (additional observations):
isAvailable may be redundant if it is only based on whether the copy is checked out (if you want to withhold the book from circulation it might be appropriate though)
ISBN maxes at 13 characters according to wikipedia (char van be a little more efficient than varchar under some circumstances)
you might want to consider a languages table that the copy can reference rather than using a string type field.
Edit (re: isAvailable):
If you just need to find the copies not loaned out, a simple query like this is all you need.
SELECT *
FROM BookCopy
WHERE idBookCopy NOT IN (
SELECT idBookCopy
FROM BookLoan
WHERE dateReturned IS NULL
);
The subquery gets the list of copies loaned out, and the NOT IN makes sure the copies in the results are not in that list.
If you want to prevent a copy from being loaned out (damaged, vandalized, etc...) an isAvailable "flag" could be a simple way to add such functionality; just add AND isAvailable = 1 to the outer query's WHERE conditions.
You can just have an m:m relationship between Members and BookCopy and use your BookLoan Table as your cross join table. So you basically just have to add the references from the tables Members and Bookcopy to the Table BookLoan
BookLoan
---------------
idBookLoan
dateLoaned
dateReturned
idBookCopy FK -- add these two
idMember FK
And also consider making idBookCopy, idMember and dateLoaned the primary keys of your BookLoan Table

Adding a database record with foreign key

Let's say there is a database with two tables: one customer table and one country table. Each customer row contains (among other things) a countryId foreign key. Let's also assume that we are populating the database from a data file (i.e., it is not an operator that is selecting a country from a UI).
What is the best practice for this?
Should one query the database first and get all ID's for all countries, and then just supply the (now known) country id's in the insert query? This is not a problem for my 'country' example, but what if there is a large number of records in the table that is being referred?
Or should the insert query use a sub query to get the country id based on the country name? If so, what if the record for the country does not exist yet and has to be added?
Or another approach? Or does it depend? :)
I would suggest using a join in your insert query to get the country id based on the country name. However, I don't know if that's something possible with every SGBD and you don't give more precision on the one you're using.