How can I delimit the possible values of a foreign key? - mysql

I have three tables in a MySQL DB.
This is the main table with organisation related stuff. Every Organisation has an unique identifier which is also the foreign key in some tables.
org
+------------+-------------+
| org_id | name |
+------------+-------------+
| 1 | a |
| 2 | b |
| 3 | c |
+------------+-------------+
This is the groups table. Organisations can have many groups.
groups
FOREIGN KEY (ORG_ID) REFERENCES ORG (ID);
+------------+-------------+----------+
| ID | org_id | name |
+------------+-------------+ ---------+
| 1 | 1 | Group1 |
| 2 | 2 | Group2 |
| 3 | 2 | Group3 |
+------------+-------------+----------+
And this is the feed table in which I would like to perform an update.
A feed can have only one associated group.
feed
FOREIGN KEY (GROUP_ID) REFERENCES GROUPS (ID);
+------------+-------------+--------------+
| ID | org_id | group_id |
+------------+-------------+ -------------+
| 1 | 1 | 1 |
| 2 | 2 | 2 |
| 3 | 1 | NULL |
| 4 | 2 | 3 |
+------------+-------------+--------------+
So now there is one problem, that i can't solve. When I INSERT or UPDATE a row, I set the groups_id, but this can also be a groups_id which not belongs to the organisation.
This happens, because all ID's in GROUPS are valid FK values. That's a thing I want to avoid. It should only be possible to insert or update a row with a groups_id which has also the same org_id as in feeds.org_id.
As you can see, the data is now fine. But when I try to make this INSERT INTO feed VALUES (4, 2, 1) it were nice to see an error. Yeah, right, I'm missing an lovely error....
It is difficult for me to make an connection between them. There seems one information or method that I'm missing. I've been looking for a lot, but I don't know the words to describe my problem.
So I ask you, could you give me a tip?
EDIT:
All feeds and all groups are related to an organisation, which has an identifier. An organisation can create feeds/messages. When this feeds are not associated with a group, this feed ist public. For special feeds they can create a group. This group is related to this special organisation.
This works and everything is good:
UPDATE feed
SET title = "Title", message = "Message", groups_id = "1"
WHERE id = "1" AND org_id = "1"
But this works also:
UPDATE feed
SET title = "Title", message = "Message", groups_id = "2"
WHERE id = "1" AND org_id = "1"
The problem is, that it is possible to associate a group to a feed (which is associated to org 1), while the group is not associated with the org (group 2 is associated with org 2).
So my thought was, is there a way to solve this through FOREIGN KEY or similar (checks, joins, subqueries). Or should I think about my db design?

I think a composed foreign key solves your problem:
create table agroup (
id int primary key,
orgid int,
UNIQUE (id,orgid)
);
create table feed (
id int primary key,
groupid int,
orgid int,
FOREIGN KEY (groupid, orgid) REFERENCES agroup(id, orgid)
);
insert into agroup values (10, 1), (20, 1), (30, 2), (40, NULL);
insert into feed values (100,10,1), (101, 20, 1);
insert into feed values (102, 40, NULL); # works
insert into feed values (103, NULL, 1); # works as well
# insert into feed values (110,10,2); # yields error "Cannot add or update a child row: a foreign key constraint fails"
Note the UNIQUE(id,orgid), which seems to be necessary. Though I do not understand why agroup(id primary key) is not sufficient to make also agroup(id,orgid) unique, I got a compiler error without this explicit unique(id,orgid)-constraint. Documentation says that the referenced attributes must be indexed. Anyway, your problem should be solved.
EDIT: Extended example, which now demonstrates also the case of NULL-values in referencing attributes.
At least in MySQL, a composite foreign key constraint permits NULL values in the referencing (child) rows, regardless of whether the parent table contain rows with corresponding NULL-values or not. If one inserts a row with NULL-values for foreign-key attributes, the foreign key constraint is simply ignored. Confer mysql foreign key semantics, which says:
"... MySQL essentially implements the semantics defined by MATCH SIMPLE, which permit a foreign key to be all or partially NULL. In that case, the (child table) row containing such a foreign key is permitted to be inserted, and does not match any row in the referenced (parent) table. It is possible to implement other semantics using triggers."

Related

in MySQL table, force a value in a record to be one of few options, affected by different value in the same record

I'll make it clearer..I have a table 'cameras' with the following structure:
camera(int) | day_of_week(int) | course_id(int) | open_time(TIME)| close_time(TIME)
and the second table 'students_courses' with the current following structure:
student_id(int) | course_id(int)
now, student_id and course_id are FOREIGN KEY's referenced to 'students' and 'courses' tables , and i want to add two column's to 'students_courses' - open_time and close_time, but the catch is open_time has to be a value existing in 'cameras' with the relevant course_id, and close time has to be a result of the open_time so for example:
if there is this two records in 'cameras' :
`10132 (camera)| 1(day) | 123(course_id) | 14:00:00(open_time) | 16:00:00(close_time)
10442 (camera)| 1(day) | 123(course_id) | 18:00:00(open_time) | 19:30:00(close_time)
and i want to insert new record to 'students_courses' with the course_id = "123" than 'open_time' can only be one of the two => '14:00:00' OR '18:00:00' and if i choose '14:00:00' than 'close_time' can only be '16:00:00', obviously if 'open_time' is 18:00:00 than 'close_time' can only be '19:30:00'...
how am i implementing this logic?...thx
FOREIGN KEY (course_id,open_time,close_time) REFERENCES cameras (course_id, open_time, close_time)
Although I'm not sure why you don't just want to FK to a primary key on cameras ...

SQL result table, match in second table SET type

The following two tables are not liked by any type of constraint.
First i have a table called subscription_plans that looks like this:
name | price | ID
-------------------
plan_A | 9.99 | 1
Plan_B | 19.99 | 2
plan_C | 29.99 | 3
I have a second table called pricing_offers. The subscription_plan_ID is a of type SET and can only contain values that match the ID's of the subscription_plans.ID (column from the above table). This table looks like this:
p_o_name | subscription_plan_ID | ID
-----------------------------------------
free donuts | 1 | 1
extra sauce | 1,2,3 | 2
pony ride | 3 | 3
bus fare -50% | 1,2,3 | 4
I'm trying to do a query to select everything (all fields *) from the first table and all names from the second table and the resulting rows should look like this:
name | price | p_o_name | ID
-------------------------------------------------------------
plan_A | 9.99 | free donuts, extra sauce, bus fare -50% | 1
Plan_B | 19.99 | extra_sauce, bus fare -50% | 2
plan_C | 29.99 | extra_sauce, pony ride, bus fare -50% | 3
The idea being that it should, for each row in the subscription_plans table, look ID field. Then go trough the second table and see what rows contain in the subscription_plan_ID, the ID of the row above. Gather those into a field caller p_o_name and insert its values to the matching response rows.
I tried doing this:
SELECT subscription_plans.*, pricing_offers.name
FROM subscription_plans INNER JOIN pricing_offers ON
FIND_IN_SET(subscription_plans.ID,subscription_plan_ID)
but i get instead of:
plan_A | 9.99 | free donuts, extra sauce, bus fare -50% | 1
this:
plan_A | 9.99 | free donuts | 1
plan_A | 9.99 | extra sauce | 1
plan_A | 9.99 | bus fare -50% | 1
Note: i get a response with all rows, but i just put the first one here to exemplify the difference.
Now, while i could do the processing in the response on my PHP page, i'm interested in knowing if i get the DB engine to output my desired result.
Do i need to create a type of constraint between the tables? If so how would i do it? I would be grateful for any help that would help me get to my proffered output result (even a better title for the question!).
If there are any unclear points, please let me know and i will clarify them.
Example of junction/intersect table usage.
create table subscription_plans
(
id int not null auto_increment primary key, -- common practice
name varchar(40) not null,
description varchar(255) not null,
price decimal(12,2) not null
-- additional indexes:
);
create table pricing_offers
(
id int not null auto_increment primary key, -- common practice
name varchar(40) not null,
description varchar(255) not null
-- additional indexes:
);
create table so_junction
( -- intersects mapping subscription_plans and pricing_offers
id int not null auto_increment primary key, -- common practice
subId int not null,
offerId int not null,
-- row cannot be inserted/updated if subId does not exist in parent table
-- the fk name is completely made up
-- parent row cannot be deleted and thus orphaning children
CONSTRAINT fk_soj_subplans
FOREIGN KEY (subId)
REFERENCES subscription_plans(id),
-- row cannot be inserted/updated if offerId does not exist in parent table
-- the fk name is completely made up
-- parent row cannot be deleted and thus orphaning children
CONSTRAINT fk_soj_priceoffer
FOREIGN KEY (offerId)
REFERENCES pricing_offers(id),
-- the below allows for only ONE combo of subId,offerId
CONSTRAINT soj_unique_ids unique (subId,offerId)
-- additional indexes:
);
insert into subscription_plans (name,description,price) values ('plan_A','description',9.99);
insert into subscription_plans (name,description,price) values ('plan_B','description',19.99);
insert into subscription_plans (name,description,price) values ('plan_C','description',29.99);
select * from subscription_plans;
insert into pricing_offers (name,description) values ('free donuts','you get free donuts, limit 3');
insert into pricing_offers (name,description) values ('extra sauce','extra sauce');
insert into pricing_offers (name,description) values ('poney ride','Free ride on Wilbur');
insert into pricing_offers (name,description) values ('bus fare -50%','domestic less 50');
select * from pricing_offers;
insert so_junction(subId,offerId) values (1,1); -- free donuts to plans
insert so_junction(subId,offerId) values (1,2),(2,2),(3,2); -- extra sauce to plans
insert so_junction(subId,offerId) values (3,3); -- wilbur
insert so_junction(subId,offerId) values (1,4),(2,4),(3,4); -- bus to plans
select * from so_junction;
-- try to add another of like above to so_junction
-- Error Code 1062: Duplicate entry
-- show joins of all
select s.*,p.*
from subscription_plans s
join so_junction so
on so.subId=s.id
join pricing_offers p
on p.id=so.offerId
order by s.name,p.name
-- show extra sauce intersects
select s.*,p.*
from subscription_plans s
join so_junction so
on so.subId=s.id
join pricing_offers p
on p.id=so.offerId
where p.name='extra sauce'
order by s.name,p.name
Basically you insert and delete from the junction table (no good really updating ever in this example).
Clean and fast joins without having to mess with slow, unwieldy sets without indexes
No one can ride the Wilbur the Poney anymore? Then
delete from so_junction
where offerId in (select id from pricing_offers where name='poney ride')
Ask if you have any questions.
And good luck!

How to add a N:M relationship to my MySQL symptoms and diseases database?

I am working on a health app and I've created a database (MySQL), which stores symptoms and diseases.
There should be a N:M relationship between the symptoms and diseases, so each symptom could have multiple diseases and vice versa. Symptoms should also have common and less common diseases and because of that, they should have a ranking (1 to infinity). How should I design/add this new feature?
Current table structure:
disease: d_id, d_name
symptom: s_id, s_name
You require an ordinary Associative table to resolve the logical n::m relationship. The PK of such tables is the two PKs of the parent tables. That gives the required row uniqueness, etc.
Second, it has data columns, so it is no longer an Associative table, it becomes an ordinary table, a binary relation (two parents).
I understand that in the symptoms-per-disease or diseases-per-symptom result sets, the rows will be ranked, but the column containing the value in the table is not rank. It is an indicator of weight given to each symptom::disease. The rank is the order in the result set, from 1 to no-of-rows, it is derived, it will change all the time. The weight is a stable value in the database, from 1 to infinity.
I recommend you give all the constraints names.
Could you show me a query example regarding the constraint names?
Sure. Simply use explicit CONSTRAINT clauses, after the columns, rather than placing each constraint with a single column. You have to do that in the case of compound Keys, such as this one. The naming convention I use for FK constraints is:
<parent>_<verb_phrase>_<child>_fk
The relevance of named constraints, as well as this naming convention, will become clear to you when you have many tables, when you are administering the various objects in the database.
The Verb Phrase comes from the data model (had the data been modelled), it describes the Action between the subject table and the object table.
In SQL the DDL looks like this (you will have to translate for your NONsql):
CREATE TABLE symptom_disease ( -- associative table
s_id INT NOT NULL,
d_id INT NOT NULL,
weight BIGINT NOT NULL, -- plus a data column
CONSTRAINT UC_PK -- named constraints
PRIMARY KEY ( s_id, d_id ),
CONSTRAINT symptom_indicates_symptom_disease_fk
FOREIGN KEY ( s_id )
REFERENCES symptom ( s_id ),
CONSTRAINT disease_is_indicated_by_symptom_disease_fk
FOREIGN KEY ( d_id )
REFERENCES disease ( d_id )
)
For the PK, you want U Unique and C Clustered. I have chosen ( s_id, d_id ) on the assumption that you will have more queries that look up diseases per symptom (than symptoms per disease). If the converse is true, change the clustering to ( d_id, s_id ).
You need another table, lets call it symptom_disease which shows the relation between a symptom and a disease. In another word each row of this table will show that a symptom is related to a disease with a specified rank. So look at this:
symptom_disease (s_id,d_id,rank);
In which s_id is foreign key to symptom.s_id and d_id is a foreign key to disease.d_id.
Lets have an example. Consider we have tow diseases like this:
| d_id | name |
| 1 | cold |
| 2 | cancer |
And tow symptoms:
| s_id | name |
| 1 |Stomach Ache|
| 2 | headache |
Now look at symptom_disease table:
| s_id | d_id | rank |
| 1 | 1 | 0 |
| 2 | 1 | 0 |
| 1 | 2 | 5 |
thees rows shows that stomach ache relates to cold by rank of 0 and to cancer by rank of 5; also headache relates to cold by rank of 0.

Is it possible to give a whole table one specific ID?

New at this, so bear with me here..
I'm trying to create a playlist table which is comprised of songs from a song table.
So, I've got a basic setup like this.
songs
|song_id | title | artist |
| 1 | MyTitle | MyArtist |
That table would theoretically hold all of the available songs. And the playlists would be made from those available songs.
That table looks like
playlists
| id | playlist_name | playlist_songs |
But I'm not sure how to make the playlist_songs table. I assume it would look something simple like this:
playlist_songs
| id | song_id (foreign key) |
Where it's just a bunch of rows each referencing the song table. Problem with this is that I don't know how to "target" a specific table like that.
The other option would be storing all of the foreign song_ids "flatly" in a single field.
playlist songs
| id | song_ids |
| 1 | 1, 5, 4, 2, 11, 30, ...|
| 2 | 44, 43, 22, 1, 2, 40...|
However, everything I've read about databases thus far points to this being a poor solution, as it's not in normal form.. but.. I don't know how to put it in normal form!
First of all your playlist_songs should be a relation table:
playlist_songs
| playlist_id (foreign key) | song_id (foreign key) |
since a song can belong to more than one playlist and a playlist can have more than one song.
Then about your concern, the reference for a specific table you're looking for is ensured in the definition of the foreign key.
If you look closely in the syntax:
ALTER TABLE `playlist_songs`
ADD CONSTRAINT `playlist_songs_playlist` FOREIGN KEY (`playlist_id`) REFERENCES `playlist` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `playlist_songs_song` FOREIGN KEY (`song_id`) REFERENCES `song` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
the table is referenced along with the field to map the parents and children records.
Your three table structure is the correct way to do it. With a little deviation from what you had:
songs
|song_id | title | artist |
playlists
| playlist_id | playlist_name |
playlist_songs
| playlist_id | song_id |
Then to get the information for the songs in a particular playlist you would use (SQL Fiddle):
SELECT pl.playlist_name, s.artist, s.title
FROM playlists AS pl
JOIN playlist_songs AS pls ON pl.playlist_id = pls.playlist_id
JOIN songs AS s ON pls.song_id = s.song_id
WHERE pl.playlist_id = 1
You definitely do NOT want to: store all of the foreign song_ids "flatly" in a single field. It is better to normalize the data. But, just looking at that method, you would have to write unnecessary complex queries to parse out the songs from that field.

Update multiple records, with temporary key overlap

I've a table on DB with the following fields as PK: DOC_ID, SECTION_ID, SET_ID, CELL_ID. As you can deduce, this is referred to a set of Excel spreadsheets.
In addition to those fields, I have a CELL_ROW field and CELL_COL field.
With the SET_ID (unique), they form an alternative key.
So, al least in theory, if I want to swap two cells' coordinates, I need either to release that constraint, or use a third temporary free position (say A100000 for example).
Suppose I have the following cells:
DOC_ID | SECTION_ID | SET_ID | CELL_ID | CELL_ROW | CELL_COL
--------|------------|--------|---------|----------|----------
5 | 456 | 8778 | 15045 | 5 | 4
5 | 456 | 8778 | 15048 | 5 | 5
And suppose I have the following temporary table from which I perform an UPDATE to the main table:
DOC_ID | SECTION_ID | SET_ID | CELL_ID | CELL_ROW | CELL_COL
--------|------------|--------|---------|----------|----------
5 | 456 | 8778 | 15045 | 5 | 5
5 | 456 | 8778 | 15048 | 5 | 4
In theory, that UPDATE should raise an exception...
But just tried, it works!
Can you explain me why? Does Oracle performs it as an atomic operation, so constraints are checked only after the whole operation (instead of record per record)?
How MS SQL Server 2008 r2 and Postgres behave in this kind of situations?
PostgreSQL will throw an error, unless you defer the unique constraint (and then you can't use it as a foreign key). I am not sure about SQL Server.
As a brief aside, you probably want to call it Postgres or PostgreSQL. Calling it Postgre is a sign that you have had too little contact with the community to be corrected.
More info on PostgreSQL
PostgreSQL checks tuple constraints at tuple update time. This means that unique constraints will be violated even in cases where a set is updated in an atomic way that does not violate a unique constraint. This leads to some interesting workarounds such as multiplying an integer key by -1 for the set and then in the next update multiply by -1 again and adding one.
I tried it in Postgresql and as expected a duplicate key error is raised:
create table t (
doc_id integer,
section_id integer,
set_id integer,
cell_id integer,
cell_row integer,
cell_col integer,
primary key (doc_id, section_id, set_id, cell_id),
unique (set_id, cell_row, cell_col)
);
insert into t (doc_id, section_id, set_id, cell_id, cell_row, cell_col)
values
(5, 456, 8778, 15045, 5, 4),
(5, 456, 8778, 15048, 5, 5);
create temporary table t_temp (
doc_id integer,
section_id integer,
set_id integer,
cell_id integer,
cell_row integer,
cell_col integer
);
insert into t_temp (doc_id, section_id, set_id, cell_id, cell_row, cell_col)
values
(5, 456, 8778, 15045, 5, 5),
(5, 456, 8778, 15048, 5, 4);
update t
set
cell_col = t_temp.cell_col
from t_temp
where
t.doc_id = t_temp.doc_id
and t.section_id = t_temp.section_id
and t.set_id = t_temp.set_id
and t.cell_id = t_temp.cell_id
;
ERROR: duplicate key value violates unique constraint "t_set_id_cell_row_cell_col_key"
DETAIL: Key (set_id, cell_row, cell_col)=(8778, 5, 5) already exists.
I could do it if the constraint was set as deferrable. Check your create table statement for that keyword.