The idea
Imagine education center which has branches. Courses of this education center are common for all branches.
Branches
CREATE TABLE `Branch` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
CREATE TABLE `Course` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`active` tinyint(1) DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
Rooms in every branch for each course generated by administrators. For example, administrator enters count of rooms for Math course. System generates 3 rooms. In other words they are limited by count.
CREATE TABLE `Room` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`branch_id` int(10) unsigned DEFAULT NULL,
`course_id` int(10) unsigned DEFAULT NULL,
`occupied_hours` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
Every room has 5 available teaching hours per day. In other words, Math-1 will have 1 different student group in each teaching hour (of 5).
Students -also grouped by branches. Every student has preffered weekly plan (week_day_mode) to come to secondary school.
- for 1, 3, 5th days of the week
- for 2, 4, 6th days of the week
class field is grade in school (main school),
CREATE TABLE `Student` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fullname` varchar(255) NOT NULL,
`class` tinyint(2) DEFAULT NULL,
`branchID` int(10) unsigned DEFAULT NULL,
`week_day_mode` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `branchID` (`branchID`)
) ENGINE=InnoDB AUTO_INCREMENT=246 DEFAULT CHARSET=utf8;
When administrator registers student for the first time, he selects all the courses that student wants to participate. For example, if 5 courses selected StudentCourseAssoc will be filled with 5 rows for this student. After testing student for basic knowledge level for each course, administrator evaluates student as "clever" (+1) or "dumb" (-1) on particular course. So knowledge_level is the value for student-course connection.
CREATE TABLE `StudentCourseAssoc` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`studentID` int(10) unsigned DEFAULT NULL,
`courseID` int(10) unsigned DEFAULT NULL,
`knowledge_level` tinyint(1) DEFAULT NULL,
`group_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1144 DEFAULT CHARSET=utf8;
The application must:
Automatically group (it can create new group or add student to existing group) students of each branch with following conditions
- Clever and dumb students must be in separate groups
- Group may consist of some grade mixes. So, it's okay to mix 9th grade with 10th. And 11th with graduated (12th class means graduated in sql). But not 10th-11th. (There will be 2 mode: 9-10, 11-12)
- Group may consist of 8 students max.
- Course rooms are limited. So every room might host only 5 groups during day
- Every student must seat on every chosen (by himself) course in 1 day
After searching for group that meets conditions above, if not found, app must create and then assign the student to the group. Then :
CREATE TABLE `StudentGroupAssoc` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`group_id` int(10) unsigned DEFAULT NULL,
`student_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
CREATE TABLE `Schedule` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`group_id` int(10) unsigned DEFAULT NULL,
`week_day_mode` tinyint(1) DEFAULT NULL,
`hour` tinyint(1) DEFAULT NULL,
`room_id` int(4) unsigned DEFAULT NULL,
`teacher_id` int(10) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `Unique Room for exact time` (`week_day_mode`,`hour`,`room_id`) USING BTREE,
UNIQUE KEY `Unique Group for exact time` (`group_id`,`week_day_mode`) USING BTREE,
KEY `Unique Teacher for exact time` (`week_day_mode`,`hour`,`teacher_id`),
KEY `room_id` (`room_id`),
KEY `teacher_id` (`teacher_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
And here is fiddle to play with.
What I've done
I'm trying to place student to the group (either existing or to create new one) during knowledge evaluation. Like, if student choose Math as one of courses, when administrator evaluates his knowledge of Math and marks positive the procedure starts to pick right group for this student:
- Function marks students knwowledge level
- Checks available hours of student (say, 1th hour is already taken, then he has 4 available hours)
- Adds class coverage condition to search (like 9-10th grades or 11-12 grades)
- Checks schedule table if has any group in available hours in student's weekly plans
If there is no one, then attempts to create.
So PHP representation looks like this
//sets knowledge level of student
$studentCourse->knowledge_level = intval($_POST["mark"]);
//check hours of student, and keep only available hours
$availableHours = array_combine(range(1, 5), range(1, 5));
//Unsets students unavailable hours from possible hours
if ($student->GroupRels)
foreach ($student->GroupRels as $groupRel)
unset($availableHours[$groupRel->hour]);
//Checks available groups based on class coverage
if (in_array($student->class, ['11', 'G']))
$classCoverage = "11-m";
else if (in_array($student->class, ['9', '10']))
$classCoverage = "9-10";
$availableGroups = Group::find()
->with("schedule")
->where([
"Group.class_coverage" => $classCoverage,
"Group.knowledge_level" => $studentCourse->knowledge_level,
"Group.participiant_count<8",
"Schedule.hour" => $availableHours,
'Schedule.week_day_mode' => $student->week_day_mode
]
)->all();
if (count($availableGroups) > 0) {
//Selecting one of groups
//adding row to StudentGroupAssoc
//adding row to Schedule
} else {
$group = new Group();
$group->branch_id = $student->branchID;
$group->class_coverage = $classCoverage;
$group->course_id=$studentCourse->courseID;
$group->knowledge_level=$studentCourse->knowledge_level;
$group->save();
...
//adding row to StudentGroupAssoc
//adding row to Schedule
}
The question is
Theoretically, the way that I'm doing is like buying ticket for airplane. Is errorless, and must work but it's not efficient and optimal. All of conditions of grouping must be met in a most efficient way: minimal group count and meeting limited room count policy. This methid soon will make plenty of groups which will not fit to available hours of rooms.
As I take hours of student one by one, (during evaluation process) it gets harder and harder to get really efficient results. Chance of not finding groups and not being to able to create new groups because of room limits is going up and up by taking hours of student.
What do you suggest to use to make use of every hour in every room?
UPDATE
Based on @norbert_van_nobelen 's answer I created 'dummy' hours table and following view to get all possible hour-room-course combinations list for each student.
hoursthe real to be planned hour
hours_available is the binary switch.
So in the real code we add a where clause: WHERE hours_available=0 to get only the hours we want to plan against:
SELECT
`s`.`id` AS `student_id`,
IF ((ifnull(`sch`.`hour`, 0) > 0), 1, 0) AS `hour_available`,
`d`.`hours` AS `hours`,
`sca`.`courseID` AS `courseID`,
`sch`.`room_id` AS `room_id`,
`sca`.`knowledge_level` AS `knowledge_level`,
(
CASE
WHEN (
(`s`.`class` = 9)
OR (`s`.`class` = 10)
) THEN
'9-10'
WHEN (
(`s`.`class` = 11)
OR (`s`.`class` = 12)
) THEN
'11-12'
ELSE
'??'
END
) AS `class_variant`
FROM
(
(
(
(
`dummy_hours` `d`
JOIN `Student` `s`
)
LEFT JOIN `StudentCourseAssoc` `sca` ON ((`s`.`id` = `sca`.`studentID`))
)
LEFT JOIN `StudentGroupAssoc` `b` ON ((`s`.`id` = `b`.`student_id`))
)
LEFT JOIN `Schedule` `sch` ON (
(
(
`sch`.`group_id` = `b`.`group_id`
)
AND (`d`.`hours` = `sch`.`hour`)
)
)
)
Using this view gives full scene of current situation. But I still can't figure out the algorithm to
- place students in groups
- to place groups in rooms
in a most efficient, optimal way with minimal group count creation.
Any suggestions?