MySQL recursive procedure to delete a record - mysql

I have a table, Models that consists of these (relevant) attributes:
-- -----------------------------------------------------
-- Table `someDB`.`Models`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `someDB`.`Models` (
`model_id` MEDIUMINT UNSIGNED NOT NULL AUTO_INCREMENT,
`type_id` SMALLINT UNSIGNED NOT NULL,
-- someOtherAttributes
PRIMARY KEY (`model_id`),
ENGINE = InnoDB;
+---------+---------+
| model_id| type_id |
+---------+---------+
| 1 | 4 |
| 2 | 4 |
| 3 | 5 |
| 4 | 3 |
+---------+---------+
And table Model_Hierarchy that shows the parent & child relationship (again, showing only the relevant attributes):
-- -----------------------------------------------------
-- Table `someDB`.`Model_Hierarchy`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `someDB`.`Model_Hierarchy` (
`parent_id` MEDIUMINT UNSIGNED NOT NULL,
`child_id` MEDIUMINT UNSIGNED NOT NULL,
-- someOtherAttributes,
INDEX `fk_Model_Hierarchy_Models1_idx` (`parent_id` ASC),
INDEX `fk_Model_Hierarchy_Models2_idx` (`child_id` ASC),
PRIMARY KEY (`parent_id`, `child_id`),
CONSTRAINT `fk_Model_Hierarchy_Models1`
FOREIGN KEY (`parent_id`)
REFERENCES `someDB`.`Models` (`model_id`)
ON DELETE CASCADE
ON UPDATE NO ACTION,
CONSTRAINT `fk_Model_Hierarchy_Models2`
FOREIGN KEY (`child_id`)
REFERENCES `someDB`.`Models` (`model_id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
+-----------+----------+
| parent_id | child_id |
+-----------+----------+
| 1 | 2 |
| 2 | 4 |
| 3 | 4 |
+-----------+----------+
If there is a Model that is not a parent or child (at some point) of another Model whose type is 5, it is not valid and hence should be deleted.
This means that Model 1, 2 should be deleted because at no point do they have a model as parent or child with type_id = 5.
There are N levels in this hierarchy, but there are no circular relationship (ie. 1 -> 2; 2 -> 1 will not exist).
Any idea on how to do this?

Comments are dispersed throughout the code.
Schema:
CREATE TABLE `Models`
( -- Note that for now the AUTO_INC is ripped out of this for ease of data insertion
-- otherwise we lose control at this point (this is just a test)
-- `model_id` MEDIUMINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
`model_id` MEDIUMINT UNSIGNED PRIMARY KEY,
`type_id` SMALLINT UNSIGNED NOT NULL
)ENGINE = InnoDB;
CREATE TABLE `Model_Hierarchy`
( -- OP comments state these are more like components
--
-- #Drew imagine b being a product and a and c being two different ways to package it.
-- Hence b is contained in both a and c respectively and separately (ie. customer can buy
-- both a and c), however, any change (outside of the scope of this question) to b is
-- reflected to both a and c. `Model_Hierarchy can be altered, yes (the project is
-- in an early development). Max tree depth is unknown (this is for manufacturing...
-- so a component can consist of a component... that consist of further component etc.
-- no real limit). How many rows? Depends, but I don't expect it to exceed 2^32.
--
--
-- Drew's interpretation of the the above: `a` is a parent of `b`, `c` is a parent of `b`
--
`parent_id` MEDIUMINT UNSIGNED NOT NULL,
`child_id` MEDIUMINT UNSIGNED NOT NULL,
INDEX `fk_Model_Hierarchy_Models1_idx` (`parent_id` ASC),
INDEX `fk_Model_Hierarchy_Models2_idx` (`child_id` ASC),
PRIMARY KEY (`parent_id`, `child_id`),
key(`child_id`,`parent_id`), -- NoteA1 pair flipped the other way (see NoteA2 in stored proc)
CONSTRAINT `fk_Model_Hierarchy_Models1`
FOREIGN KEY (`parent_id`)
REFERENCES `Models` (`model_id`)
ON DELETE CASCADE
ON UPDATE NO ACTION,
CONSTRAINT `fk_Model_Hierarchy_Models2`
FOREIGN KEY (`child_id`)
REFERENCES `Models` (`model_id`)
ON DELETE NO ACTION
ON UPDATE NO ACTION
)ENGINE = InnoDB;
CREATE TABLE `GoodIds`
( -- a table to determine what not to delete from models
`id` int auto_increment primary key,
`model_id` MEDIUMINT UNSIGNED,
`has_been_processed` int not null,
dtFinished datetime null,
-- index section (none shown, developer chooses later, as he knows what is going on)
unique index(model_id), -- supports the "insert ignore" concept
-- FK's below:
foreign key `fk_abc_123` (model_id) references Models(model_id)
)ENGINE = InnoDB;
To drop and start over from the top:
-- ------------------------------------------------------------
-- reverse order is happier
drop table `GoodIds`;
drop table `Model_Hierarchy`;
drop table `Models`;
-- ------------------------------------------------------------
Load Test Data:
insert Models(model_id,type_id) values
(1,1),(2,1),(3,1),(4,1),(5,1),(6,1),(7,1),(8,1),(9,5),(10,1),(11,1),(12,1);
-- delete from Models; -- note, truncate does not work on parents of FK's
insert Model_Hierarchy(parent_id,child_id) values
(1,2),(1,3),(1,4),(1,5),
(2,1),(2,4),(2,7),
(3,2),
(4,8),(4,9),
(5,1),
(6,1),(6,2),
(7,1),(7,10),
(8,1),(8,12),
(9,11),
(10,11),
(11,12);
-- Set 2 to test (after a truncate / copy paste of this below to up above):
(1,2),(1,3),(1,4),(1,5),
(2,1),(2,4),(2,7),
(3,2),
(4,8),(4,9),
(5,1),
(6,1),(6,2),
(7,1),(7,10),
(8,1),(8,12),
(9,1),
(10,11),
(11,12);
-- truncate table Model_Hierarchy;
-- select * from Model_Hierarchy;
-- select * from Models where type_id=5;
Stored Procedure:
DROP PROCEDURE if exists loadUpGoodIds;
DELIMITER $$
CREATE PROCEDURE loadUpGoodIds()
BEGIN
DECLARE bDone BOOL DEFAULT FALSE;
DECLARE iSillyCounter int DEFAULT 0;
TRUNCATE TABLE GoodIds;
insert GoodIds(model_id,has_been_processed) select model_id,0 from Models where type_id=5;
WHILE bDone = FALSE DO
select min(model_id) into #the_Id_To_Process from GoodIds where has_been_processed=0;
IF #the_Id_To_Process is null THEN
SET bDone=TRUE;
ELSE
-- First, let's say this is the parent id.
-- Find the child id's that this is a parent of
-- and they qualify as A Good Id to save into our Good table
insert ignore GoodIds(model_id,has_been_processed,dtFinished)
select child_id,0,null
from Model_Hierarchy
where parent_id=#the_Id_To_Process;
-- Next, let's say this is the child id.
-- Find the parent id's that this is a child of
-- and they qualify as A Good Id to save into our Good table
insert ignore GoodIds(model_id,has_been_processed,dtFinished)
select child_id,0,null
from Model_Hierarchy
where child_id=#the_Id_To_Process;
-- NoteA2: see NoteA1 in schema
-- you can feel the need for the flipped pair composite key in the above
UPDATE GoodIds set has_been_processed=1,dtFinished=now() where model_id=#the_Id_To_Process;
END IF;
-- safety bailout during development:
SET iSillyCounter = iSillyCounter + 1;
IF iSillyCounter>10000 THEN
SET bDone=TRUE;
END IF;
END WHILE;
END$$
DELIMITER ;
Test:
call loadUpGoodIds();
-- select count(*) from GoodIds; -- 9 / 11 / 12
select * from GoodIds limit 10;
+----+----------+--------------------+---------------------+
| id | model_id | has_been_processed | dtFinished |
+----+----------+--------------------+---------------------+
| 1 | 9 | 1 | 2016-06-28 20:33:16 |
| 2 | 11 | 1 | 2016-06-28 20:33:16 |
| 4 | 12 | 1 | 2016-06-28 20:33:16 |
+----+----------+--------------------+---------------------+
Mop up calls, can be folded into stored proc:
-- The below is what to run
-- delete from Models where model_id not in (select null); -- this is a safe call (will never do anything)
-- the above is just a null test
delete from Models where model_id not in (select model_id from GoodIds);
-- Error 1451: Cannot delete or update a parent row: a FK constraint is unhappy
-- hey the cascades did not work, can figure that out later
-- Let go bottom up for now. Meaning, to honor FK constraints, kill bottom up.
delete from Model_Hierarchy where parent_id not in (select model_id from GoodIds);
-- 18 rows deleted
delete from Model_Hierarchy where child_id not in (select model_id from GoodIds);
-- 0 rows deleted
delete from Models where model_id not in (select model_id from GoodIds);
-- 9 rows deleted / 3 remain
select * from Models;
+----------+---------+
| model_id | type_id |
+----------+---------+
| 9 | 5 |
| 11 | 1 |
| 12 | 1 |
+----------+---------+

Related

Delete From in delete after trigger not deleting from another table

I am trying to make a DELETE TRIGGER so that when a row is deleted from a table Bike it will delete a row in another table called Available as long as it is not in a different table called in Rental. If it is in Rental table then it will cancel the delete (if that's possible I'm very new to sql).
BEGIN
DELETE FROM Available
Where old.bnumber = Available.bnumber;
END
right now I am getting
1451: Cannot delete or update a parent row: a foreign key constraint
fails (cecs535project.available, CONSTRAINT bnumber FOREIGN KEY
(bnumber) REFERENCES bike (bnumber))
bnumber is a foreign key in Available that references Bike.
Any help is appreciated.
CREATE TABLE `Bike` (
`bnumber` int NOT NULL,
`make` varchar(64) DEFAULT NULL,
`color` varchar(8) DEFAULT NULL,
`year` int DEFAULT NULL,
PRIMARY KEY (`bnumber`)
)
CREATE TABLE `Available` (
`bnumber` int NOT NULL,
`rack-id` int DEFAULT NULL,
PRIMARY KEY (`bnumber`),
KEY `bnumber_idx` (`rack-id`),
KEY `bnumber_idx1` (`bnumber`),
CONSTRAINT `bnumber` FOREIGN KEY (`bnumber`) REFERENCES `Bike` (`bnumber`),
CONSTRAINT `rack-id` FOREIGN KEY (`rack-id`) REFERENCES `Rack` (`id`)
)
CREATE TABLE `Rental` (
`date` date NOT NULL,
`time` time NOT NULL,
`bnumber` int NOT NULL,
`cust-id` int NOT NULL,
`src` int DEFAULT NULL,
PRIMARY KEY (`bnumber`,`cust-id`,`date`,`time`),
KEY `bnumber_idx` (`bnumber`),
KEY `cust-id_idx` (`cust-id`),
KEY `src_idx` (`src`),
CONSTRAINT `bike` FOREIGN KEY (`bnumber`) REFERENCES `Bike` (`bnumber`),
CONSTRAINT `cust-id` FOREIGN KEY (`cust-id`) REFERENCES `Customer` (`id`),
CONSTRAINT `src` FOREIGN KEY (`src`) REFERENCES `Rack` (`id`)
)
I am not a big fan of your current design, and I think it can be simplified. Consider just having a single table for all bike assets, with one column maintaining whether or not it be currently rented out, e.g.
Bike
id | name | type | rented (bit)
1 | bike1 | road | 1
2 | bike2 | mountain | 0
...
Now to record a bike being rented or not, you simply have to update the rented bit column above. Should you want to delete from your inventory, refraining from doing so if the bike be on loan, you can use:
DELETE
FROM Bike
WHERE rented = 0; -- AND your other conditions here
2 possible approaches
Use signal to identify where rental exists an throw an error https://dev.mysql.com/doc/refman/8.0/en/signal.html
test for rental existence in delete;
In both a before trigger is used since the constraint test occurs before an after trigger fires.
DROP TABLE IF EXISTS AVAILABLE;
drop table if exists rental;
drop table if exists BIKE;
CREATE TABLE `Bike` (
`bnumber` int NOT NULL,
`make` varchar(64) DEFAULT NULL,
`color` varchar(8) DEFAULT NULL,
`year` int DEFAULT NULL,
PRIMARY KEY (`bnumber`)
);
insert into bike values
(10,'aaa','red',2020),(20,'bbb','yell',2020);
CREATE TABLE `Available` (
`bnumber` int NOT NULL,
`rack-id` int DEFAULT NULL,
PRIMARY KEY (`bnumber`),
KEY `bnumber_idx` (`rack-id`),
KEY `bnumber_idx1` (`bnumber`),
CONSTRAINT `bnumber` FOREIGN KEY (`bnumber`) REFERENCES `Bike` (`bnumber`)#,
#CONSTRAINT `rack-id` FOREIGN KEY (`rack-id`) REFERENCES `Rack` (`id`)
) ;
insert into available values
(10,100),(20,200);
CREATE TABLE `Rental` (
#`date` date NOT NULL,
#`time` time NOT NULL,
`bnumber` int NOT NULL,
#`cust-id` int NOT NULL,
#`src` int DEFAULT NULL,
#PRIMARY KEY (`bnumber`,`cust-id`,`date`,`time`),
KEY `bnumber_idx` (`bnumber`),
#KEY `cust-id_idx` (`cust-id`),
#KEY `src_idx` (`src`),
CONSTRAINT `bike` FOREIGN KEY (`bnumber`) REFERENCES `Bike` (`bnumber`)#,
#CONSTRAINT `cust-id` FOREIGN KEY (`cust-id`) REFERENCES `Customer` (`id`),
#CONSTRAINT `src` FOREIGN KEY (`src`) REFERENCES `Rack` (`id`)
) ;
insert into rental values
(10);
Approach 1
drop trigger if exists t;
delimiter $$
create trigger t before delete on bike
for each row
begin
if exists (select 1 from rental r where r.bnumber = old.bnumber) then
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'An error occurred Rental exists';
end if;
delete a from available a where a.bnumber = old.bnumber;
end $$
delimiter ;
delete b from bike b;
ERROR 1644 (45000): An error occurred Rental exists
+---------+------+-------+------+
| bnumber | make | color | year |
+---------+------+-------+------+
| 10 | aaa | red | 2020 |
| 20 | bbb | yell | 2020 |
+---------+------+-------+------+
2 rows in set (0.001 sec)
select * from available;
+---------+---------+
| bnumber | rack-id |
+---------+---------+
| 10 | 100 |
| 20 | 200 |
+---------+---------+
2 rows in set (0.001 sec)
select * from rental;
+---------+
| bnumber |
+---------+
| 10 |
+---------+
1 row in set (0.001 sec)
Approach 2
drop trigger if exists t;
delimiter $$
create trigger t before delete on bike
for each row
begin
delete a from available a where a.bnumber = old.bnumber;
end $$
delimiter ;
delete b from bike b where
#bnumber = 10 and
not exists(select 1 from rental r where r.bnumber = b.bnumber);
select * from bike;
+---------+------+-------+------+
| bnumber | make | color | year |
+---------+------+-------+------+
| 10 | aaa | red | 2020 |
+---------+------+-------+------+
1 row in set (0.001 sec)
select * from available;
+---------+---------+
| bnumber | rack-id |
+---------+---------+
| 10 | 100 |
+---------+---------+
1 row in set (0.001 sec)
select * from rental;
+---------+
| bnumber |
+---------+
| 10 |
+---------+
1 row in set (0.001 sec)
Creating an AFTER DELETE instead could also help to skip the Foreign Key Constraint issue
CREATE TRIGGER trigger_name
AFTER DELETE
ON table_name FOR EACH ROW
trigger_body;
ref: https://www.mysqltutorial.org/mysql-triggers/mysql-after-delete-trigger/

soft delete on update in mysql with multiple schemas

I am in this situation:
Background
I have 2 database schemas called "prod" and "stg".
"prod" contains 2 tables called "parent" and "child"
"stg" only has the "parent" table
"parent" table defination is the same across "prod" and "stg" schemas.
In the case of deleting records, "parent" table is defined as soft delete (logically deletion, i.e. set delete_flg as "1") whereas the "child" table is true delete (physically remove the record)
Goal
I am trying to achieve the following goal:
when and only when both "prod"."parent" and "stg"."parent" are deleted (no matter physically or logically, or does not exist on one side) then automatically cascade a delete operation(physically remove) to the record in "prod"."child" table whose "SP_ID" matches the value in "parent".
For example, assuming I have
"prod"."parent"
+----+---------+--------+
| SP_ID | SP_NAME | DELETE_FLG |
+----+---------+--------+
| 1 | 1 | 1 |
+----+---------+--------+
"prod"."parent"
+----+---------+--------+
| SP_ID | SP_NAME | DELETE_FLG |
+----+---------+--------+
| 1 | 1 | 1 |
+----+---------+--------+
"stg"."parent"
+----+---------+--------+
| SP_ID | SP_NAME | DELETE_FLG |
+----+---------+--------+
| 1 | 1 | 0 |
+----+---------+--------+
"prod"."child"
+----+---------+
| SP_ID | JOB_KEY |
+----+---------+
| 1 | key |
+----+---------+
, if I execute a sql update "stg"."parent" set DELETE_FLG = 1 where SP_ID = 1, which logically delete the last "existing" record in "parent" table that has SP_ID 1, then the record in "prod"."child" will also be automatically phycially deleted by mysql.
Question
I have been thinking about making the SP_ID in the child table as a foreign key referencing the one in parent able (https://dev.mysql.com/doc/refman/8.0/en/create-table-foreign-keys.html)
however,
a) I don't know whether it is possible to reference multiple tables in differnet schemas, and
b) It seems mysql only support cascading same operation, i.e. delete on parent then delete the child OR update on parent then update the child. But in my case, I want a update on parent then delete the child.
Could somebody help me out here please?
Is this possible to be achieved in mysql? or I have to do this in application layer?
Table definition
CREATE TABLE `prod`.`parent` (
`SP_ID` varchar(20) NOT NULL COMMENT '',
`SP_NAME` varchar(100) NOT NULL COMMENT '',
`DELETE_FLG` tinyint(1) NOT NULL DEFAULT '0' COMMENT '',
PRIMARY KEY (`SP_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=''
CREATE TABLE `prod`.`child` (
`SP_ID` varchar(20) NOT NULL COMMENT '',
`JOB_KEY` varchar(11) NOT NULL,
PRIMARY KEY (`SP_ID`,`JOB_KEY`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=''
CREATE TABLE `stg`.`parent` (
`SP_ID` varchar(20) NOT NULL COMMENT '',
`SP_NAME` varchar(100) NOT NULL COMMENT '',
`DELETE_FLG` tinyint(1) NOT NULL DEFAULT '0' COMMENT '',
PRIMARY KEY (`SP_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT=''
with the hint of using triggers, This is my solution which works:
Add 2 triggers(one is after update, on is after delete) to both prod and stg parent tables.
# after update trigger
CREATE DEFINER=`root`#`localhost` TRIGGER `stg`.`parent_AFTER_UPDATE` AFTER UPDATE ON `parent` FOR EACH ROW
BEGIN
IF (
select
count(*)
from(
select
*
from `prod`.`parent`
where `prod`.`parent`.id = old.id and `prod`.`parent`.delete_flg = 0
Union all
select
*
from `stg`.`parent`
where `stg`.`parent`.id = old.id and `stg`.`parent`.delete_flg = 0
) as a
) = 0 THEN
DELETE FROM `prod`.`child` WHERE `prod`.`child`.id = old.id;
END IF;
END
# after delete trigger
CREATE DEFINER=`root`#`localhost` TRIGGER `stg`.`parent_AFTER_DELETE` AFTER DELETE ON `parent` FOR EACH ROW
BEGIN
IF (
select
count(*)
from(
select
*
from `prod`.`parent`
where `prod`.`parent`.id = old.id and `prod`.`parent`.delete_flg = 0
Union all
select
*
from `stg`.`parent`
where `stg`.`parent`.id = old.id and `stg`.`parent`.delete_flg = 0
) as a
) = 0 THEN
DELETE FROM `prod`.`child` WHERE `prod`.`child`.id = old.id;
END IF;
END

How to do this with foreign keys?

I have a table called categories, with the following fields:
id (primary key, identifies the categories)
user_id (foreign key, users.id, the creator of the current category)
category_id (foreign key, categories.id, the id of the parent category id)
Inside this table I have some records that are accessible to everyone.
For these records, both the user_id and the category_id fields are NULL.
In addition, users can create their own records (where the user_id field is not NULL), but every user can access only those that he has made himself.
Each record must meet these conditions:
user_id values must be valid
if the category_id is given then it must be a valid categories.id and that record must be created by the given user
the value of the category_id can be a categoies.id where the user_id is NULL
How can I do this? I think I need more foreign keys for this, but not sure how to do it.
Some examples that may help you to understand my problem:
Lets say I have the following records inside the categories table:
+--------+-------------+-----------------+
| id | user_id | category_id |
+--------+-------------+-----------------+
| 1 | NULL | NULL |
+--------+-------------+-----------------+
| 2 | NULL | NULL |
+--------+-------------+-----------------+
| 3 | 1 | 1 |
+--------+-------------+-----------------+
| 4 | 2 | 1 |
+--------+-------------+-----------------+
and lets say that I want to insert these records:
+-------------+-----------------+----------------------------------------------------------------+
| user_id | category_id | is it insertable?
+-------------+-----------------+----------------------------------------------------------------+
| 1 | NULL | yes, because the value of the user_id is valid id |
+-------------+-----------------+----------------------------------------------------------------+
| 1 | 1 | yes, because the record with id of 1 is created by NULL |
+-------------+-----------------+----------------------------------------------------------------+
| 1 | 3 | yes, because the record with id of 3 is created by user #1 |
+-------------+-----------------+----------------------------------------------------------------+
| 1 | 4 | no, because the record with id of 4 is created by another user |
+-------------+-----------------+----------------------------------------------------------------+
Categories table:
CREATE TABLE `categories` (
`id` int(11) NOT NULL,
`user_id` int(11) DEFAULT NULL,
`category_id` int(11) DEFAULT NULL,
`name` varchar(150) NOT NULL,
`type` enum('income','expense') NOT NULL DEFAULT 'income',
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
ALTER TABLE `categories`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `category_id` (`category_id`,`user_id`,`name`),
ADD KEY `user_id` (`user_id`);
ALTER TABLE `categories`
ADD CONSTRAINT `categories_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
ADD CONSTRAINT `categories_ibfk_2` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
Unfortunately, MySQL foreign keys can go so far. This is a bit of advanced logic. Might I recommend using a MySQL trigger?
DELIMITER $$
CREATE PROCEDURE `check_categories_user_id`(IN p_category_id INT(11), IN p_user_id INT(11))
BEGIN
IF (p_category_id IS NOT NULL) THEN
SET #other_user_id = (SELECT user_id
FROM categories
WHERE id = p_category_id);
IF (p_user_id <> #other_user_id) THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'check constraint on categories.user_id failed';
END IF;
END IF;
END$$
CREATE TRIGGER `categories_before_update` BEFORE UPDATE ON `categories`
FOR EACH ROW
BEGIN
CALL check_categories_user_id(new.category_id, new.user_id);
END$$
CREATE TRIGGER `categories_before_insert` BEFORE INSERT ON `categories`
FOR EACH ROW
BEGIN
CALL check_categories_user_id(new.category_id, new.user_id);
END$$
DELIMITER ;
See dbfiddle here. (If you remove the last INSERT query, the SELECT query works as expected)
Additionally, to avoid some overhead (The SELECT query in the trigger), you can just enforce NULL on user_id when category_id IS NOT NULL (Using triggers like above). If category_id is NOT NULL, you'll just fetch the user_id from the parent row (Or the root row (parent's parent row...etc), if nested).

CREATE several tables by looping through ROWS in a particular table in MySQL

How can I, in MySQL using loop through the Subject table and create a table for each subject as shown below using Math, Econs, Chem as examples. Bring in php only as a last resort.
Table: SUBJECTS
| Subject | unit | staff |
| Math | 3 | Mr James |
| Econs | 1 | Dr Smith |
| Chem | 2 | Mrs Aisha |
table: MATHS
Student | TEST1 | TEST2 | EXAM |
101 | 10 | 20 | 30 |
105 | 11 | 09 | 45 |
table: ECONS
Student | TEST1 | TEST2 | EXAM |
101 | 10 | 20 | 30 |
105 | 11 | 09 | 45 |
table: CHEM
Student | TEST1 | TEST2 | EXAM |
101 | 10 | 20 | 30 |
105 | 11 | 09 | 45 |
When designing your database you first need to identify all the entities that will be needed to support your goals. In your case you need to keep track of students and how their results in various courses/subjects.
Based on this we can identify the following entities:
Subjects such as math,chemistry etc.
Students who take tests and exams for those subjects.
Staff members who are responsible for teaching the subjects and preparing the exams/test.
Different types of tests i.e. class test, exams and so forth.
The results of students for a particular type of test in a particular subject.
So based on this (rather simplified_ scenario we only need 5 tables. The second thing is to identify the relationships between the entities. Lets review them.
A subject can only be presented by one staff member but a staff member can present many subjects. For example math is only taught by Mr Jones but Mr Jones teaches math and science. This is what we call a one to many relationship.
A student can have many results in many subject and a subject can have many students with many results. For example Student 1 took an exam and a test in math with two different results. The math subject might have 100's of student each with one or more result recorded. This is a many to many relationship.
Each result of a student will be of some type either a exam or a test. Thus there is one to many relationship here. There is only one exam type but math,science,geography and history all use this exam type.
So based on all of this have created a small demo database. I will take you through it.
Here is a visual representation of the structure:
Here is the code for the database.
CREATE SCHEMA IF NOT EXISTS `coursedb` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci ;
USE `coursedb` ;
-- -----------------------------------------------------
-- Table `coursedb`.`staff`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `coursedb`.`staff` (
`staff_ID` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`staff_name` VARCHAR(64) NOT NULL,
`staff_surname` VARCHAR(64) NOT NULL,
`staff_title` VARCHAR(64) NOT NULL,
PRIMARY KEY (`staff_ID`))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `coursedb`.`subject`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `coursedb`.`subject` (
`subject_ID` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`subject_name` VARCHAR(64) NULL,
`staff_ID` INT UNSIGNED NOT NULL,
PRIMARY KEY (`subject_ID`),
INDEX `fk_subject_staff_idx` (`staff_ID` ASC),
CONSTRAINT `fk_subject_staff`
FOREIGN KEY (`staff_ID`)
REFERENCES `coursedb`.`staff` (`staff_ID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION
)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `coursedb`.`student`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `coursedb`.`student` (
`student_ID` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`student_name` VARCHAR(64) NOT NULL,
PRIMARY KEY (`student_ID`))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `coursedb`.`subject_result_type`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `coursedb`.`subject_result_type` (
`subject_result_type_ID` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`subject_result_type_name` VARCHAR(64) NOT NULL,
PRIMARY KEY (`subject_result_type_ID`))
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `coursedb`.`subject_student_result`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `coursedb`.`subject_student_result` (
`subject_ID` INT UNSIGNED NOT NULL,
`student_ID` INT UNSIGNED NOT NULL,
`subject_student_result_date` DATE NOT NULL,
`subject_student_result_score` INT NOT NULL,
`subject_result_type_ID` INT UNSIGNED NOT NULL,
PRIMARY KEY (`subject_ID`, `student_ID`, `subject_student_result_date`),
INDEX `fk_subject_student_result_subject1_idx` (`subject_ID` ASC),
INDEX `fk_subject_student_result_student1_idx` (`student_ID` ASC),
INDEX `fk_subject_student_result_subject_result_type1_idx` (`subject_result_type_ID` ASC),
CONSTRAINT `fk_subject_student_result_subject1`
FOREIGN KEY (`subject_ID`)
REFERENCES `coursedb`.`subject` (`subject_ID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_subject_student_result_student1`
FOREIGN KEY (`student_ID`)
REFERENCES `coursedb`.`student` (`student_ID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT `fk_subject_student_result_subject_result_type1`
FOREIGN KEY (`subject_result_type_ID`)
REFERENCES `coursedb`.`subject_result_type` (`subject_result_type_ID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
CREATE TABLE IF NOT EXISTS `coursedb`.`subject` (
`subject_ID` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`subject_name` VARCHAR(64) NULL,
`staff_ID` INT UNSIGNED NOT NULL,
PRIMARY KEY (`subject_ID`),
INDEX `fk_subject_staff_idx` (`staff_ID` ASC),
CONSTRAINT `fk_subject_staff`
FOREIGN KEY (`staff_ID`)
REFERENCES `coursedb`.`staff` (`staff_ID`)
ON DELETE NO ACTION
ON UPDATE NO ACTION)
ENGINE = InnoDB;
Lets start adding some data so you can see how this all fits together. First lets add two staff members Mr Jones and Miss Grumpy.
Execute the following code:
INSERT INTO `coursedb`.`staff`
(
`staff_name`,
`staff_surname`,
`staff_title`
)
SELECT 'James', 'Jones','Mr'
UNION
SELECT 'Pot','Grumpy','Miss';
This will insert the two staff members. If you look at the newly inserted data by executing a SELECT statement you will find the following data:
staff_ID staff_name staff_surname staff_title
'1', 'James', 'Jones', 'Mr'
'2', 'Pot', 'Grumpy', 'Miss'
Notice how the database has assigned a ID number to these staff members? This is the primary key for the staff member and we use this to identify the row in the staff tables. I am keeping this to auto numbers for the moment but there are several options here.
We now need to create the courses that these two staff members teach. Mr Jones teaches math and science and Miss Grump teaches Art. Lets create the subjects and link them to the staff members. This can be done by the following code:
INSERT INTO `coursedb`.`subject`
(
`subject_name`,
`staff_ID`
)
VALUES
(
'Math',
1
);
INSERT INTO `coursedb`.`subject`
(
`subject_name`,
`staff_ID`
)
VALUES
(
'Science',
1
);
INSERT INTO `coursedb`.`subject`
(
`subject_name`,
`staff_ID`
)
VALUES
(
'Art',
2
);
You can now see which subjects Mr Jones teaches by executing the following query:
SELECT *
FROM staff stf
INNER JOIN `subject` sub
ON stf.staff_ID = sub.staff_ID
WHERE stf.staff_ID =1;
So before we can track how a student is doing in their courses there is two pieces of information that is missing. The type of result i.e. exam or test and of course some students. So lets add them.
INSERT INTO `coursedb`.`student`
(
`student_name`
)
VALUES
(
'Student 1'
);
INSERT INTO `coursedb`.`subject_result_type`
(
`subject_result_type_name`
)
SELECT 'Test'
UNION
SELECT 'Exam'
With this out of the way lets record some results for Student 1. Student one wrote 2 exams one in science one in math and Student 1 also did a art test. The following SQL will insert this data:
INSERT INTO `coursedb`.`subject_student_result`
(`subject_ID`,
`student_ID`,
`subject_student_result_date`,
`subject_student_result_score`,
`subject_result_type_ID`
)
VALUES
(
1,
1,
CURDATE(),
80,
2
);
INSERT INTO `coursedb`.`subject_student_result`
( `subject_ID`,
`student_ID`,
`subject_student_result_date`,
`subject_student_result_score`,
`subject_result_type_ID`
)
VALUES
(
2,
1,
CURDATE(),
60,
2
);
INSERT INTO `coursedb`.`subject_student_result`
( `subject_ID`,
`student_ID`,
`subject_student_result_date`,
`subject_student_result_score`,
`subject_result_type_ID`
)
VALUES
(
3,
1,
CURDATE(),
80,
1
);
You can now draw a report on a student and their results by executing the following query:
SELECT *
FROM subject_student_result ssr
INNER JOIN student std
ON ssr.student_ID = std.student_ID
INNER JOIN `subject` sub
ON ssr.subject_ID = sub.subject_ID
INNER JOIN subject_result_type srt
ON ssr.subject_result_type_ID = srt.subject_result_type_ID
INNER JOIN staff stf
ON sub.staff_ID = stf.staff_ID
I suggest you work through this model and really understand what I am showing you. It will make your designs simpler and cleaner and much less maintenance.

Fill MySQL records one-to-many related tables in one action

I have two MySQL tables with an one-to-many relationship between them. For example:
CREATE TABLE test1 (
pk1 INTEGER AUTO_INCREMENT PRIMARY KEY,
testvalue1 INTEGER
);
CREATE TABLE test2 (
pk2 INTEGER AUTO_INCREMENT PRIMARY KEY,
testvalue2 VARCHAR(50),
fk2 INTEGER NOT NULL,
FOREIGN KEY (fk2) REFERENCES test1 (pk1)
);
If I want to insert records in both tables I can first insert a record in the PK table (e.g. INSERT INTO test1 SET testvalue1=100), determine the PK value (e.g. SELECT MAX(pk1) AS LastId FROM test1 or use LAST_INSERT_ID())
and finally use that value to fill the FK column in the second table.
But is it possible to achieve this all in 1 command/query/action? So let's MySQL fill in the PK- and FK-values using the AUTO INCREMENTs?
You should use two INSERT commands; or try to use an INSERT-trigger.
EDIT:
--An example with trigger:
CREATE TABLE dept(
id INT(11) NOT NULL AUTO_INCREMENT,
dept_name VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (id)
)
ENGINE = INNODB;
CREATE TABLE emp(
id INT(11) NOT NULL AUTO_INCREMENT,
emp_name VARCHAR(255) DEFAULT NULL,
dept_id INT(11) DEFAULT NULL,
PRIMARY KEY (id),
CONSTRAINT FK_emp_dept_id FOREIGN KEY (dept_id)
REFERENCES dept (id) ON DELETE RESTRICT ON UPDATE RESTRICT
)
ENGINE = INNODB;
DELIMITER $$
CREATE TRIGGER trigger1
AFTER INSERT
ON dept
FOR EACH ROW
BEGIN
INSERT INTO emp VALUES (NULL, 'Someone', NEW.id);
END
$$
DELIMITER ;
-- Try to add new department.
INSERT INTO dept VALUES(NULL, 'Sales');
-- Is there new default employee?
SELECT * FROM emp;
+----+----------+---------+
| id | emp_name | dept_id |
+----+----------+---------+
| 1 | Someone | 1 |
+----+----------+---------+