生選課系統(tǒng)數(shù)據(jù)庫設(shè)計:從ER模型到MySQL事務(wù)與并發(fā)控制)
簡介面向數(shù)據(jù)庫課程設(shè)計學(xué)習(xí)者這份某高校學(xué)生選課系統(tǒng)的設(shè)計資料包以學(xué)生選課場景為載體完整呈現(xiàn)數(shù)據(jù)庫課程設(shè)計從需求分析、概念結(jié)構(gòu)設(shè)計到邏輯結(jié)構(gòu)設(shè)計與物理實現(xiàn)的典型過程適合需要完成相似課設(shè)或鞏固數(shù)據(jù)庫原理的本科及高職學(xué)生參考。壓縮包內(nèi)共3個文件其中Word版課程設(shè)計報告doc詳述系統(tǒng)分析、ER圖、關(guān)系模式及設(shè)計思路SQL腳本sql提供建庫建表與基礎(chǔ)數(shù)據(jù)數(shù)據(jù)庫備份bak便于直接還原查看運行效果整體僅802KB輕量易用。資源已有998人學(xué)習(xí)下載口碑較好屬于高分?jǐn)?shù)課設(shè)。通過該資源讀者可學(xué)到選課系統(tǒng)涉及的學(xué)生、課程、成績等核心實體建模方法掌握數(shù)據(jù)庫定義、完整性約束設(shè)置與SQL編程技巧并借鑒規(guī)范化報告寫作框架為獨立完成課程設(shè)計提供有力支撐。1. 學(xué)生選課系統(tǒng)課程設(shè)計從一張成績單反推表結(jié)構(gòu)拿到“某高校學(xué)生選課系統(tǒng)的設(shè)計”這個數(shù)據(jù)庫課程設(shè)計題目大多數(shù)人第一反應(yīng)是建三張表、寫幾個增刪改查頁面。但真正拉開差距的不在“能不能跑”而在“跑起來之后還能不能守住業(yè)務(wù)規(guī)則”同一門課只剩一個名額時并發(fā)選課會不會超員、退課后成績記錄能不能追溯、學(xué)生能不能查到同一學(xué)期的課程沖突。這篇博文按做課程設(shè)計最常見的 MySQL 方案從 ER 模型、三張核心表、存儲過程、觸發(fā)器一路走到事務(wù)、權(quán)限和答辯演示驗證把一套可以完整復(fù)現(xiàn)的路線講清楚。適合拿了這題想認(rèn)真做完而不是臨交差前復(fù)制粘貼的同學(xué)。2. 學(xué)生選課系統(tǒng)的數(shù)據(jù)庫設(shè)計ER模型、范式與核心表結(jié)構(gòu)2.1 從需求描述到實體-聯(lián)系圖選課業(yè)務(wù)里必須畫清楚的三個實體寫這個課程設(shè)計我一般會先讓學(xué)生在紙上畫 ER 圖而不是直接打開 MySQL。學(xué)生、課程、選課記錄三個實體是骨架可選實體還有院系、教師、教室。最容易畫錯的地方是選課記錄它不是學(xué)生和課程之間的純粹連接表它自己就是實體承擔(dān)成績、選課時間、退課狀態(tài)這些屬性。評分看數(shù)據(jù)模型的人第一眼就看你有沒有把選課記錄當(dāng)作實體對待。畫完實體還要標(biāo)函數(shù)依賴。學(xué)號決定姓名、性別、學(xué)院課程號決定課程名、學(xué)分、教師、容量(學(xué)號, 課程號)決定選課時間和成績。凡是不完全依賴主鍵、或者存在傳遞依賴的字段都要拆出去。比如教師職稱和教師所屬院系如果在課程表里就會產(chǎn)生傳遞依賴要拆成教師表課程表只保留教師 ID。這也是數(shù)據(jù)庫面試題里反復(fù)問的“范式”在這個題目里最實在的落點。2.2 第三范式下的核心表結(jié)構(gòu)學(xué)生、課程、選課三大表的字段取舍下面這套結(jié)構(gòu)是我做這個課程設(shè)計時最常用的一版以 MySQL 8.0 為基準(zhǔn)語法上也兼容很多其他數(shù)據(jù)庫。學(xué)生表studentstu_id CHAR(10) PK學(xué)號用定長字符而不是 INT學(xué)號是業(yè)務(wù)編號不是數(shù)值不參與數(shù)學(xué)運算stu_name VARCHAR(20) NOT NULLgender ENUM(M,F)major VARCHAR(50)專業(yè)grade SMALLINT UNSIGNED年級比如 2024enroll_date DATE入學(xué)時間課程表coursecourse_id CHAR(6) PKcourse_name VARCHAR(50) NOT NULLcredit DECIMAL(2,1)學(xué)分支持 3.5 這種小數(shù)teacher_id CHAR(6)教師編號關(guān)聯(lián)教師表capacity SMALLINT UNSIGNED DEFAULT 30課程容量selected_count SMALLINT UNSIGNED DEFAULT 0已選人數(shù)selected_count是冗余字段它能避免每次選課都去COUNT(*)全表掃一遍但冗余字段必須在寫入時同步維護(hù)否則會出現(xiàn)不一致。這個矛盾后面用觸發(fā)器兜底。選課表enrollment的字段設(shè)計字段類型說明stu_idCHAR(10)學(xué)號復(fù)合主鍵之一course_idCHAR(6)課程號復(fù)合主鍵之一enroll_timeDATETIME選課時間默認(rèn)當(dāng)前時間gradeDECIMAL(4,1)成績允許為空statusENUM(selected,dropped)選課狀態(tài)退課后保留記錄status字段是很多人會漏掉的設(shè)計退課不應(yīng)該物理刪除選課記錄否則成績數(shù)據(jù)、選課歷史全沒了。用狀態(tài)標(biāo)記才能回答“這學(xué)生退過哪些課”這類問題。2.3 建庫建表 SQL 完整腳本課程設(shè)計第一版可執(zhí)行代碼CREATE DATABASE IF NOT EXISTS course_db DEFAULT CHARSET utf8mb4; USE course_db; CREATE TABLE student ( stu_id CHAR(10) NOT NULL, stu_name VARCHAR(20) NOT NULL, gender ENUM(M, F) DEFAULT M, major VARCHAR(50) NOT NULL, grade SMALLINT UNSIGNED, enroll_date DATE, PRIMARY KEY (stu_id) ); CREATE TABLE course ( course_id CHAR(6) NOT NULL, course_name VARCHAR(50) NOT NULL, credit DECIMAL(2,1) DEFAULT 2.0, teacher_id CHAR(6), capacity SMALLINT UNSIGNED DEFAULT 30, selected_count SMALLINT UNSIGNED DEFAULT 0, PRIMARY KEY (course_id) ); CREATE TABLE enrollment ( stu_id CHAR(10) NOT NULL, course_id CHAR(6) NOT NULL, enroll_time DATETIME DEFAULT CURRENT_TIMESTAMP, grade DECIMAL(4,1) DEFAULT NULL, status ENUM(selected,dropped) DEFAULT selected, PRIMARY KEY (stu_id, course_id), CONSTRAINT fk_enr_stu FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, CONSTRAINT fk_enr_cou FOREIGN KEY (course_id) REFERENCES course(course_id) );邏輯說明刪除學(xué)生時級聯(lián)刪除其選課記錄刪除課程時不做級聯(lián)用約束默認(rèn)的RESTRICT行為阻止刪除已經(jīng)被選過的課程保護(hù)歷史數(shù)據(jù)。字符集統(tǒng)一utf8mb4避免中文亂碼和 emoji 導(dǎo)致的存儲問題。capacity和selected_count用SMALLINT UNSIGNED就足夠課程容量不可能超過 65535沒必要給INT。3. 用存儲過程與觸發(fā)器實現(xiàn)學(xué)生選課系統(tǒng)的核心業(yè)務(wù)選課、退課、查余量3.1 選課存儲過程把“查余量、判重復(fù)、寫選課記錄、扣名額”裝進(jìn)一個事務(wù)選課的核心邏輯是四個動作的串行組合。如果讓應(yīng)用層分四步執(zhí)行任何一步失敗都會留下半個業(yè)務(wù)狀態(tài)。常見做法是寫一個存儲過程把所有動作放進(jìn)一個事務(wù)邊界。DELIMITER $$ CREATE PROCEDURE sp_enroll_course( IN p_stu_id CHAR(10), IN p_course_id CHAR(6) ) BEGIN DECLARE v_capacity INT; DECLARE v_selected INT; START TRANSACTION; SELECT capacity, selected_count INTO v_capacity, v_selected FROM course WHERE course_id p_course_id FOR UPDATE; IF v_selected v_capacity THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 課程人數(shù)已滿; END IF; IF EXISTS (SELECT 1 FROM enrollment WHERE stu_id p_stu_id AND course_id p_course_id AND status selected) THEN ROLLBACK; SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 不能重復(fù)選課; END IF; INSERT INTO enrollment(stu_id, course_id) VALUES (p_stu_id, p_course_id); UPDATE course SET selected_count selected_count 1 WHERE course_id p_course_id; COMMIT; END$$ DELIMITER ;邏輯說明FOR UPDATE是行級排他鎖鎖住課程表的這一行讓兩個并發(fā)會話不能同時讀到同一個“剩余名額”。SIGNAL語句主動拋出異常并攜帶中文錯誤信息應(yīng)用層直接捕獲異常消息就能知道失敗原因比返回一個錯誤碼讓前端猜更穩(wěn)。參數(shù)說明p_stu_id和p_course_id是輸入?yún)?shù)分別對應(yīng)學(xué)生學(xué)號和課程號。這個存儲過程沒有輸出參數(shù)業(yè)務(wù)是否成功通過異常判斷調(diào)用方需要把調(diào)用包在try-catch里處理異常消息。3.2 觸發(fā)器兜底直接 INSERT 也超不了選的容量保護(hù)存儲過程能攔住按規(guī)矩走接口的人攔不住繞過存儲過程直接執(zhí)行INSERT INTO enrollment的操作。課程設(shè)計里經(jīng)常出現(xiàn)這樣的場景管理系統(tǒng)后臺有一個“手動補錄選課”的功能開發(fā)時圖省事直接寫了一條 INSERT結(jié)果容量限制完全失效。觸發(fā)器能把這道防線沉到數(shù)據(jù)庫引擎層。CREATE TRIGGER trg_enroll_before_insert BEFORE INSERT ON enrollment FOR EACH ROW BEGIN DECLARE v_selected INT; DECLARE v_capacity INT; SELECT selected_count, capacity INTO v_selected, v_capacity FROM course WHERE course_id NEW.course_id FOR UPDATE; IF v_selected v_capacity THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 選課人數(shù)已滿觸發(fā)器攔截; END IF; UPDATE course SET selected_count selected_count 1 WHERE course_id NEW.course_id; END邏輯說明NEW.course_id引用的是即將插入選課表的課程號。觸發(fā)器先讀課程人數(shù)超員就拋異常阻止插入未超員就自動維護(hù)selected_count。這樣手動 INSERT 也被納入容量控制。要注意的是普通 MySQL 觸發(fā)器默認(rèn)基于BEFORE或AFTER操作在BEFORE里拋異常對應(yīng)的 INSERT 會整體失敗。這里沒有寫FOR EACH ROW之外的分區(qū)條件FOR EACH ROW本身是 MySQL 觸發(fā)器的固定語法每行操作都會執(zhí)行。3.3 視圖與索引三條高頻查詢路徑的數(shù)據(jù)庫 SQL 優(yōu)化課程設(shè)計答辯時老師最喜歡問“你做了哪些查詢優(yōu)化”。至少有三條高頻 SQL 需要保證性能學(xué)生查自己已選課程及成績、學(xué)生查課程余量、教師查某門課選課名單。對應(yīng)關(guān)系可以用一張表說清查詢場景涉及表應(yīng)命中的索引學(xué)生查成績單enrollment course studentenrollment 復(fù)合主鍵(stu_id, course_id)查課程余量coursecourse 主鍵course_id教師查選課名單enrollmentenrollment 的course_id前綴索引enrollment的復(fù)合主鍵是(stu_id, course_id)按學(xué)生查成績單時走的是聯(lián)合索引最左前綴按課程查名單則不行需要額外建一個索引ALTER TABLE enrollment ADD INDEX idx_enr_course (course_id); CREATE VIEW v_student_score AS SELECT s.stu_id, s.stu_name, c.course_name, e.grade FROM enrollment e JOIN student s ON e.stu_id s.stu_id JOIN course c ON e.course_id c.course_id WHERE e.status selected; CREATE VIEW v_course_remain AS SELECT course_id, course_name, capacity - selected_count AS remain FROM course;邏輯說明視圖不存儲數(shù)據(jù)每次查詢都會展開成底層 SQL 執(zhí)行 JOIN。v_student_score把成績單查詢封裝成語義清晰的接口應(yīng)用層不必拼復(fù)雜的 JOIN。v_course_remain里capacity - selected_count是計算列直接暴露余量應(yīng)用層把remain 0作為可選的判斷條件。3.4 課程設(shè)計演示數(shù)據(jù)的最小增刪改查集造數(shù)據(jù)不要手寫上百條 INSERT先準(zhǔn)備一個最小集合夠展示功能就行INSERT INTO student VALUES (20240001, 趙一, M, 計算機學(xué)院, 2024, 2024-09-01), (20240002, 錢二, F, 軟件學(xué)院, 2024, 2024-09-01), (20240003, 孫三, M, 計算機學(xué)院, 2024, 2024-09-01); INSERT INTO course VALUES (CS101, 數(shù)據(jù)庫原理, 3.0, T001, 2, 0), (CS102, 操作系統(tǒng), 3.5, T002, 30, 0); CALL sp_enroll_course(20240001, CS101); CALL sp_enroll_course(20240002, CS101); CALL sp_enroll_course(20240003, CS101);這里把CS101的容量設(shè)成 2第三個學(xué)生選課會觸發(fā)存儲過程里的滿員異常演示效果最直觀。三個學(xué)生兩門課足夠覆蓋查成績、查余量、滿員選課失敗、退課四個必演示場景。4. 從課程設(shè)計到答辯演示事務(wù)、權(quán)限與數(shù)據(jù)一致性驗證4.1 并發(fā)超選的經(jīng)典翻車現(xiàn)場為什么不能只寫一條 INSERT如果課程設(shè)計里選課功能只有一句INSERT INTO enrollment演示時瀏覽器開兩個窗口同時點選課就能復(fù)現(xiàn)數(shù)據(jù)不一致。MySQL 客戶端開兩個會話模擬兩個學(xué)生選同一門只剩一個名額的課-- 會話 A START TRANSACTION; UPDATE course SET selected_count selected_count 1 WHERE course_id CS101; -- 會話 B在 A 提交前執(zhí)行 UPDATE course SET selected_count selected_count 1 WHERE course_id CS101;會話 B 的 UPDATE 會阻塞等 A 提交后 B 才繼續(xù)。這個過程中B 在不知道 A 已占名額的情況下可能已經(jīng)向應(yīng)用層返回了“選課中”的中間狀態(tài)寫入選課記錄時又產(chǎn)生重復(fù)。正確做法是把“查余量、插選課記錄、更新人數(shù)”放進(jìn)同一個事務(wù)并且在一開始就SELECT ... FOR UPDATE鎖住課程行。第三章的存儲過程已經(jīng)把這一步做好了這里重點是用兩個會話把效果跑出來給答辯老師看。4.2 MySQL 用戶與權(quán)限設(shè)計一套庫同時服務(wù)學(xué)生、教師、管理員課程設(shè)計規(guī)范里要求“不同角色不同權(quán)限”這句話要落到數(shù)據(jù)庫用戶層面才有說服力。MySQL 里可以建兩個用戶對應(yīng)學(xué)生和管理員兩個角色CREATE USER stu_userlocalhost IDENTIFIED BY Stu2024; CREATE USER admin_userlocalhost IDENTIFIED BY Admin2024; GRANT SELECT, INSERT, UPDATE ON course_db.enrollment TO stu_userlocalhost; GRANT SELECT ON course_db.course TO stu_userlocalhost; GRANT ALL PRIVILEGES ON course_db.* TO admin_userlocalhost; FLUSH PRIVILEGES;權(quán)限矩陣可以整理成表格寫進(jìn)設(shè)計報告角色可操作表權(quán)限范圍學(xué)生enrollmentSELECT / INSERT / UPDATE僅自己的記錄學(xué)生courseSELECT管理員全部表所有權(quán)限教師enrollment / courseSELECT擴展場景“學(xué)生只能改自己的記錄”在 MySQL 單靠表級授權(quán)不夠GRANT無法指定行級條件。實際項目中通常通過視圖加WHERE stu_id 當(dāng)前登錄用戶實現(xiàn)行級過濾或者在應(yīng)用層校驗。課程設(shè)計里把賬權(quán)限矩陣寫清楚比硬編碼實現(xiàn)更專業(yè)。4.3 事務(wù)回滾驗證腳本演示用一段異常 SQL 證明原子性答辯現(xiàn)場只展示“正常選課成功”沒有沖擊力要主動表演一次回滾START TRANSACTION; INSERT INTO enrollment(stu_id, course_id) VALUES (20240001, CS102); UPDATE course SET selected_count selected_count 1 WHERE course_id CS102; -- 故意制造一個錯誤不存在的課程 INSERT INTO enrollment(stu_id, course_id) VALUES (20240001, XX999); ROLLBACK;執(zhí)行到這個故意寫錯的課程XX999時外鍵約束會拒絕插入事務(wù)進(jìn)入異常狀態(tài)。如果不做ROLLBACK前面的選課記錄雖然已經(jīng)寫入內(nèi)存中的事務(wù)日志但不會真正落盤。執(zhí)行完 ROLLBACK 后SELECT * FROM enrollment看不到CS102那條記錄selected_count也沒有變化這就是原子性的直觀演示。需要補充的是MySQL 客戶端默認(rèn)autocommit1上面用顯式START TRANSACTION手動關(guān)閉了自動提交所以 ROLLBACK 才有意義。如果不開事務(wù)直接跑三條 INSERT失敗的那條會擋住后續(xù)語句但前面成功的語句已經(jīng)永久提交清理起來更麻煩。5. 數(shù)據(jù)庫課程設(shè)計答辯前的 5 個驗證動作5.1 用 EXPI 證明索引真實生效不要只說“我建了索引”現(xiàn)場跑EXPLAIN SELECT * FROM enrollment WHERE course_id CS101;看執(zhí)行計劃里的key字段是不是idx_enr_coursetype是不是ref。如果key是 NULL說明索引沒建上或者查詢沒走索引先ALTER TABLE enrollment ADD INDEX idx_enr_course(course_id);再跑一遍。5.2 用異?;貪L證明事務(wù)不是擺設(shè)按 4.3 的腳本執(zhí)行一次把回滾前后的SELECT COUNT(*) FROM enrollment結(jié)果截圖對比。評審老師看到“異常后數(shù)據(jù)恢復(fù)原狀”比什么口頭說明都有效。5.3 用 mysqldump 命令準(zhǔn)備一鍵恢復(fù)mysqldump -u root -p course_db course_db_backup.sql mysql -u root -p course_db course_db_backup.sql演示時先把庫里的數(shù)據(jù)刪兩條再執(zhí)行導(dǎo)入命令還原。注意恢復(fù)前要確認(rèn)目標(biāo)庫存在否則執(zhí)行導(dǎo)入會直接報錯Unknown database。5.4 留一個擴展點選課日志表與連接池參數(shù)有余力的話加一張enrollment_log表記錄每次選課/退課操作的客戶端 IP 和操作時間用觸發(fā)器寫入。這是課程設(shè)計評分里很加分的擴展功能同時也是軟件工程課程設(shè)計里審計模塊的雛形。如果項目對接 Java 后端把連接池參數(shù)寫進(jìn)application.yml的 HikariCP 配置里maximum-pool-size調(diào)成 5 到 10演示時不會因為連接數(shù)爆炸卡死。答辯前把這五個動作依次過一遍每個結(jié)論都有 SQL 輸出撐腰本數(shù)據(jù)庫課程設(shè)計的數(shù)據(jù)側(cè)支撐就完整了。本文還有配套的精品資源點擊獲取