简介:这份资源是面向高校计算机及相关专业学生的数据库系统课程设计参考文档,以人事管理系统为背景,帮助读者完成从需求分析到数据库实施的全流程设计训练。内容围绕多部门企业场景展开,涵盖员工基本信息管理、部门调动、模糊查询、出勤统计、迟到早退查询及人员调入调出统计等核心功能,并完整呈现需求分析、概念设计、逻辑设计、物理设计、数据库实施、功能实现与总结七个阶段,涉及ER模型、关系模式转换、主外键约束、索引与存储过程等知识点。资源包共1个doc文件,大小约1.23MB,为课程设计报告正文,结构清晰,可直接对照参考。目前已有1108人学习下载,适合需要完成数据库课程设计、撰写设计报告或复习数据库设计流程的读者借鉴使用。
1. 人事管理数据库课设拆包:从需求分析到视图落地,这份文档能省你三天
如果你正在搜“数据库课程设计 人事管理”,大概率是手里攥着一份任务书,知道要做需求分析、E-R 图、建表、写查询,但真打开 SQL Server 或 MySQL 却不知道第一行 DDL 从哪写起。这份《数据库系统课程设计-人事管理》文档,本质是一份完整的课设报告加实现记录,覆盖了从需求分析、概念设计、逻辑设计、物理设计到数据库实施和功能实现的全部环节。它解决的不是“数据库是什么”这种教科书问题,而是“一个公司多部门、员工调动、考勤统计、工资核算这套业务,到底该拆成几张表、字段怎么定、视图怎么建”的落地问题。适合正在做数据库课设的本科生,也适合想拿一个完整案例练手 SQL 的转行者。我翻完这份文档最大的感受是:它的表结构设计不算完美,但业务场景足够真实,尤其是考勤和调动这两块,比那些只有学生表和成绩表的课设强不少。
2. 需求分析怎么落到数据字典:六个模块拆成六张表的依据
2.1 从业务描述里抽实体和属性
文档开头给了一段很典型的业务描述:一个公司有若干部门,每个部门有若干职工和一个部门经理,职工只能属于一个部门。这句话里其实藏着三个实体——部门、员工、经理角色,以及两条关系——部门与员工的一对多、部门与经理的一对一。很多人做课设时直接跳过这段文字,上来就画 E-R 图,结果画到一半发现“调动”这个动作没地方放。文档的处理方式是把调动单独抽成一张表,记录原部门编号、新部门编号、调离时间和调入时间,这样员工当前所属部门仍然存在员工基本信息表里,历史调动记录不丢。这个思路在真实系统里叫“拉链表”的简化版,课设阶段够用了。
需求分析阶段另一个容易被忽略的是数据字典。文档里列了一张数据项表,把员工编号、姓名、性别、年龄、入职时间、所属部门、联系电话、身份证号这些字段的类型、长度、取值范围都标了出来。比如员工编号是 int 类型、长度 1、范围 1000000 到 999999,姓名是 varchar(10)、四个汉字以内。这张表看起来琐碎,但它是后面建表时字段类型和约束的直接依据。我见过太多课设报告,需求分析写了两页纸,到建表时字段类型全靠拍脑袋,varchar 和 char 混用,日期用字符串存,最后查询统计全是坑。
2.2 六个功能模块对应的表结构
文档把系统拆成六个模块:基本信息、工作信息、部门信息、考勤信息、工资信息、调动信息。对应到物理设计阶段就是六张表。这里有一个值得说的设计决策:员工基本信息表和工作信息表是分开的。基本信息表存姓名、性别、年龄、身份证号、入职时间、所属部门、联系电话、基本工资;工作信息表存员工编号、部门编号、职称、工龄。为什么要拆?因为基本信息的变更频率低,工作信息的变更频率高,拆开之后更新职称或工龄时不会锁住整行基本信息。课设阶段不一定需要考虑锁的问题,但这种按变更频率分表的意识是好的。
考勤信息表的设计比较有意思。它没有用“每天一条打卡记录”的方式,而是用缺勤、迟到、早退三个 int 字段,取值 0 或 1,加一个日期字段。这意味着每条记录代表某员工某天的考勤状态。这种设计的好处是统计“某年某月某部门迟到人数”时直接 SUM 迟到字段就行,不用做复杂的条件判断。坏处是如果一天内多次迟到早退,没法记录次数。课设场景下这种简化是合理的,但如果你要拿这个设计去应付更复杂的考勤需求,需要改成事件表结构。
工资信息表把底薪、补贴、奖金、扣款、加班费、实发工资都放在一张表里,实发工资是冗余字段。严格按范式来说,实发工资可以由其他字段计算得出,不应该单独存储。但文档这么设计也有道理:工资一旦发放就是历史事实,计算公式可能变化,存下来反而更安全。这是典型的反范式设计,在数据仓库和报表场景里很常见。
提示:课设报告里的表结构不一定是最优解,但你要能说出每个设计决策的理由。答辩时老师问“为什么工资表要冗余实发工资”,答“方便查询”是及格,答“工资是历史事实,公式可能变”是优秀。
3. 从 E-R 图到建表语句:逻辑设计与物理设计的衔接
3.1 E-R 图转关系模式的四条规则
文档在逻辑设计章节列出了 E-R 图转关系模式的原则:一个实体型转一个关系模式,实体的属性就是关系的属性,实体的码就是关系的码;一个联系转一个关系模式,1:1 联系每个实体的码都是候选码,1:n 联系 n 端实体的码是关系的码,m:n 联系诸实体码的组合是关系的码;自联系按 1:1、1:n、m:n 分别处理;具有相同码的关系模式可合并。这四条规则是教科书标准内容,但文档把它和具体业务结合了起来。
比如员工与部门之间是 1:n 联系,按规则应该把部门编号放到员工端作为外键。文档确实在员工工作信息表里放了部门编号。但员工调动信息表里同时有原部门编号和新部门编号,这两个字段都指向部门表的主键,属于两个不同的外键。建表时如果不加别名,查询时会出现“部门名称”字段歧义。文档在视图部分用了department_info.department_name as 新部门名称和department_info.department_name as 原部门名称两次连接,说明作者意识到了这个问题。
3.2 建表语句的字段类型与约束
文档没有直接贴出完整的 CREATE TABLE 语句,但从物理设计章节的表格可以还原出来。下面是我根据文档描述整理的建表 SQL,以 MySQL 为例,SQL Server 用户把 AUTO_INCREMENT 换成 IDENTITY、把 DATETIME 换成 DATETIME2 即可。
-- 部门信息表:先建,因为员工表要引用它 CREATE TABLE department_info ( department_id INT PRIMARY KEY COMMENT '部门编号', department_name VARCHAR(20) NOT NULL COMMENT '部门名称', department_phone VARCHAR(11) NOT NULL COMMENT '部门电话', department_manager VARCHAR(8) NOT NULL COMMENT '部门经理' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='部门信息表'; -- 员工基本信息表:所属部门用 varchar 存部门名称,不是外键 CREATE TABLE staff_info ( staff_id INT PRIMARY KEY COMMENT '员工编号', name VARCHAR(10) NOT NULL COMMENT '姓名', gender VARCHAR(4) NOT NULL COMMENT '性别', age INT COMMENT '年龄', id_card VARCHAR(18) NOT NULL COMMENT '身份证号', hire_date DATE NOT NULL COMMENT '入职时间', department VARCHAR(20) NOT NULL COMMENT '所属部门', phone VARCHAR(11) NOT NULL COMMENT '联系电话', basic_salary FLOAT NOT NULL COMMENT '基本工资' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工基本信息表'; -- 员工工作信息表:部门编号是外键,关联部门表 CREATE TABLE staff_work ( staff_id INT NOT NULL COMMENT '员工编号', department_id INT NOT NULL COMMENT '部门编号', position VARCHAR(10) COMMENT '职称', work_age INT COMMENT '工龄', PRIMARY KEY (staff_id), FOREIGN KEY (staff_id) REFERENCES staff_info(staff_id), FOREIGN KEY (department_id) REFERENCES department_info(department_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工工作信息表'; -- 考勤信息表:0/1 标记缺勤、迟到、早退 CREATE TABLE staff_signin ( staff_id INT NOT NULL COMMENT '员工编号', absent INT DEFAULT 0 COMMENT '缺勤 0否1是', late INT DEFAULT 0 COMMENT '迟到 0否1是', leave_early INT DEFAULT 0 COMMENT '早退 0否1是', time DATE NOT NULL COMMENT '日期', PRIMARY KEY (staff_id, time), FOREIGN KEY (staff_id) REFERENCES staff_info(staff_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考勤信息表'; -- 工资信息表:实发工资冗余存储 CREATE TABLE staff_salary ( staff_id INT PRIMARY KEY COMMENT '员工编号', basic_salary FLOAT NOT NULL COMMENT '底薪', subsidy FLOAT COMMENT '补贴', bonus FLOAT COMMENT '奖金', deduction FLOAT COMMENT '扣款', overtime_pay FLOAT COMMENT '加班费', real_salary FLOAT NOT NULL COMMENT '实发工资', FOREIGN KEY (staff_id) REFERENCES staff_info(staff_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工资信息表'; -- 员工调动信息表:两个外键都指向部门表 CREATE TABLE staff_transfer ( staff_id INT NOT NULL COMMENT '员工编号', name VARCHAR(10) NOT NULL COMMENT '姓名', old_dep_id INT COMMENT '原部门编号', new_dep_id INT COMMENT '新部门编号', old_time DATE COMMENT '调离时间', new_time DATE COMMENT '调入时间', PRIMARY KEY (staff_id, old_time), FOREIGN KEY (staff_id) REFERENCES staff_info(staff_id), FOREIGN KEY (old_dep_id) REFERENCES department_info(department_id), FOREIGN KEY (new_dep_id) REFERENCES department_info(department_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工调动信息表';这段建表代码有几个参数需要说明。staff_info表里department字段用的是 varchar 而不是外键,这是文档原设计。严格来说这不符合第三范式,因为部门名称变了员工表不会自动更新。但课设阶段这样设计的好处是查询员工信息时不用连表,简单直接。如果你要改成外键,把department换成department_id INT并加外键约束即可。
staff_signin表的主键是(staff_id, time)复合主键,保证一个员工一天只有一条考勤记录。staff_transfer表的主键是(staff_id, old_time),保证一个员工同一调离时间只有一条记录。这两个复合主键的设计是合理的,比用自增 id 更符合业务语义。
staff_salary表的real_salary字段没有用生成列,而是要求插入时手动计算。如果你想让数据库自动算,可以改成real_salary FLOAT GENERATED ALWAYS AS (basic_salary + subsidy + bonus + overtime_pay - deduction) STORED。但注意 MySQL 5.7 才支持生成列,SQL Server 用计算列语法不同。
注意:建表顺序很重要。
staff_work、staff_signin、staff_salary、staff_transfer都引用了staff_info,所以staff_info必须先建。staff_work和staff_transfer引用了department_info,所以department_info要最先建。如果顺序错了,外键约束会报 errno 150。
4. 视图与统计查询:把多表连接封装成可复用的黑匣子
4.1 五个视图的创建逻辑
文档在功能实现章节建了五个视图:员工个人信息图、部门员工工作信息、员工工资信息、员工考勤信息、各部门员工调动信息。视图的作用是把复杂的多表连接封装起来,查询时只写SELECT * FROM 视图名 WHERE 条件就行。对于课设答辩来说,视图是展示你理解“外模式”概念的最好证据。
以员工个人信息图为例,它连接了四张表:department_info、staff_transfer、staff_info、staff_work。连接条件是department_info.department_id = staff_transfer.new_dep_id、staff_transfer.staff_id = staff_info.staff_id、staff_info.staff_id = staff_work.staff_id。这个连接链的逻辑是:通过调动表找到员工当前所在部门,再关联基本信息和岗位信息。但这里有一个潜在问题:如果一个员工没有调动记录,staff_transfer表里没有他的数据,INNER JOIN 会把这个员工过滤掉。文档用的是 INNER JOIN,所以视图里只会出现有调动记录的员工。如果你要包含所有员工,应该把staff_transfer的连接改成 LEFT JOIN,或者直接从staff_info的department字段取部门信息。
-- 员工个人信息视图:包含所有员工,不依赖调动记录 CREATE OR REPLACE VIEW 员工个人信息图 AS SELECT si.staff_id AS 员工编号, si.name AS 姓名, di.department_id AS 部门编号, di.department_name AS 部门名称, sw.position AS 职位, sw.work_age AS 工龄 FROM staff_info si LEFT JOIN staff_work sw ON si.staff_id = sw.staff_id LEFT JOIN department_info di ON sw.department_id = di.department_id;这段代码和文档原版的区别在于:原版通过staff_transfer表绕了一圈,我改成了直接从staff_work取部门编号。这样所有员工都会出现在视图里,不管有没有调动记录。LEFT JOIN保证即使员工没有岗位记录,基本信息也会显示。参数说明:CREATE OR REPLACE VIEW在 MySQL 和 SQL Server 里都支持,但 SQL Server 的语法是CREATE OR ALTER VIEW。
4.2 按年月统计出勤和按部门统计迟到早退
文档任务书里明确要求了三个统计功能:按年份月份统计某个职工的出勤情况、按某年某月某日统计查询某部门的迟到和早退人数、按年统计各部门调入调出人数。这三个查询是课设答辩时老师最爱问的,因为它们涉及日期函数、分组聚合和多表连接。
按年月统计某个职工的出勤情况,核心是把time字段格式化成YYYY-MM再分组。MySQL 用DATE_FORMAT(time, '%Y-%m'),SQL Server 用FORMAT(time, 'yyyy-MM')或CONVERT(VARCHAR(7), time, 120)。
-- 按年月统计指定员工的缺勤、迟到、早退次数 SELECT DATE_FORMAT(time, '%Y-%m') AS 年月, SUM(absent) AS 缺勤次数, SUM(late) AS 迟到次数, SUM(leave_early) AS 早退次数 FROM staff_signin WHERE staff_id = 1000001 GROUP BY DATE_FORMAT(time, '%Y-%m') ORDER BY 年月;这段查询的逻辑是:先按员工编号过滤,再把日期格式化成YYYY-MM,然后按这个格式分组,对 0/1 字段求和得到次数。参数说明:SUM(absent)因为 absent 只有 0 和 1,求和就是缺勤天数。如果 absent 字段允许 NULL,需要用SUM(IFNULL(absent, 0))。
按某年某月某日统计某部门的迟到和早退人数,需要连接考勤表、员工工作信息表和部门表。
-- 统计指定日期指定部门的迟到和早退人数 SELECT di.department_name AS 部门名称, SUM(ss.late) AS 迟到人数, SUM(ss.leave_early) AS 早退人数 FROM staff_signin ss INNER JOIN staff_work sw ON ss.staff_id = sw.staff_id INNER JOIN department_info di ON sw.department_id = di.department_id WHERE ss.time = '2024-06-15' AND di.department_name = '技术部' GROUP BY di.department_name;这里用SUM而不是COUNT,因为每条考勤记录的 late 字段是 0 或 1,求和得到的是迟到人数。如果用COUNT需要加WHERE late = 1条件。参数说明:ss.time = '2024-06-15'是精确日期匹配,如果要查某个月,改成DATE_FORMAT(ss.time, '%Y-%m') = '2024-06'。
按年统计各部门调入调出人数,需要分别统计调入和调出,然后合并。可以用两个子查询加 UNION,也可以用条件聚合。
-- 按年统计各部门调入调出人数 SELECT year_label AS 年份, dep_name AS 部门名称, SUM(CASE WHEN type = '调入' THEN 1 ELSE 0 END) AS 调入人数, SUM(CASE WHEN type = '调出' THEN 1 ELSE 0 END) AS 调出人数 FROM ( SELECT YEAR(new_time) AS year_label, new_dep_id AS dep_id, '调入' AS type FROM staff_transfer WHERE new_time IS NOT NULL UNION ALL SELECT YEAR(old_time) AS year_label, old_dep_id AS dep_id, '调出' AS type FROM staff_transfer WHERE old_time IS NOT NULL ) t INNER JOIN department_info di ON t.dep_id = di.department_id GROUP BY year_label, dep_name ORDER BY 年份, 部门名称;这段查询先用 UNION ALL 把调入和调出记录合并成一张临时表,再用 CASE WHEN 做条件聚合。参数说明:YEAR()函数在 MySQL 和 SQL Server 里都可用。UNION ALL不去重,比UNION快。如果某个部门某年没有调动记录,不会出现在结果里,需要的话用 LEFT JOIN 补全。
提示:课设答辩时,老师大概率会让你现场写一个“查询某个员工在所有部门的调动历史”。提前准备好这个查询,用
staff_transfer表自连接或者直接查staff_transfer加department_info两次连接都行。
5. 课设实施避坑:从建表到查询的五个血泪教训
5.1 外键约束导致插入顺序报错
现象:建完所有表后,往staff_work表插入数据时报错Cannot add or update a child row: a foreign key constraint fails。原因是staff_work的staff_id引用了staff_info,但staff_info里还没有对应的员工记录。解决方法是先插staff_info,再插staff_work。如果数据已经乱了,临时关闭外键检查SET FOREIGN_KEY_CHECKS = 0,插完再打开。但这是后悔药,正确做法是按依赖顺序插入:部门表 → 员工基本信息表 → 工作信息表/考勤表/工资表/调动表。
5.2 日期字段用字符串存导致统计失效
现象:考勤表的time字段用了 varchar 类型,存的是'2024-06-15'这样的字符串。查询WHERE time BETWEEN '2024-06-01' AND '2024-06-30'能出结果,但YEAR(time)报错或者返回 0。原因是字符串没有日期语义,日期函数无法解析。解决方法是在建表时就用 DATE 或 DATETIME 类型。如果已经建了 varchar,用STR_TO_DATE(time, '%Y-%m-%d')转换,但这样查询无法走索引。
5.3 INNER JOIN 丢数据
现象:员工个人信息视图里查不到某些员工。原因是视图用了INNER JOIN staff_transfer,没有调动记录的员工被过滤掉了。这是课设里最常见的翻车点。解决方法是把INNER JOIN改成LEFT JOIN,或者重新审视业务逻辑:员工当前部门到底应该从staff_info.department取,还是从staff_transfer取。如果员工表里已经有部门字段,视图就不应该依赖调动表。
5.4 中文别名在 SQL Server 里需要加方括号
现象:在 SQL Server 里执行SELECT name AS 姓名 FROM staff_info报错Invalid column name '姓名'。原因是 SQL Server 对中文别名支持不好,需要用方括号包起来:SELECT name AS [姓名] FROM staff_info。MySQL 和 PostgreSQL 没有这个问题。如果你用的是 SQL Server,建议所有中文别名都加方括号,或者干脆用英文别名。
5.5 视图嵌套视图导致性能下降
现象:建了视图 A,又建了视图 B 引用视图 A,查询视图 B 时特别慢。原因是视图嵌套会让优化器无法下推谓词,相当于每次查询都先物化整个视图 A。解决方法是课设阶段视图不要超过一层,统计查询直接写多表连接,不要套视图。如果一定要用视图,用EXPLAIN看执行计划,确认有没有全表扫描。
6. 进阶技巧:用存储过程封装统计逻辑并做数据校验
课设报告里通常只要求写查询语句,但如果你想让作品在答辩时高一个档次,可以把统计逻辑封装成存储过程。存储过程的好处是参数化调用,不用每次改 SQL 里的日期和部门名称。下面这个存储过程接收年份和部门编号,返回该部门每个月的迟到和早退人数。
DELIMITER // CREATE PROCEDURE 统计部门月度考勤( IN p_year INT, IN p_dep_id INT ) BEGIN SELECT DATE_FORMAT(ss.time, '%Y-%m') AS 月份, SUM(ss.late) AS 迟到人数, SUM(ss.leave_early) AS 早退人数, SUM(ss.absent) AS 缺勤人数 FROM staff_signin ss INNER JOIN staff_work sw ON ss.staff_id = sw.staff_id WHERE YEAR(ss.time) = p_year AND sw.department_id = p_dep_id GROUP BY DATE_FORMAT(ss.time, '%Y-%m') ORDER BY 月份; END // DELIMITER ; -- 调用:统计 2024 年技术部(部门编号 101)的月度考勤 CALL 统计部门月度考勤(2024, 101);这段存储过程的参数说明:p_year是年份,p_dep_id是部门编号。DELIMITER //是为了让 MySQL 把BEGIN...END里的分号当成存储过程体的一部分,而不是语句结束符。调用时用CALL加参数。SQL Server 的语法不同,用CREATE PROCEDURE加@p_year INT参数,调用时用EXEC。
除了存储过程,数据校验也是课设里容易加分的地方。比如在插入考勤记录前检查该员工当天是否已有记录,可以用触发器或者存储过程里的IF EXISTS判断。
-- 插入考勤前检查重复 DELIMITER // CREATE PROCEDURE 插入考勤( IN p_staff_id INT, IN p_date DATE, IN p_absent INT, IN p_late INT, IN p_leave_early INT ) BEGIN IF EXISTS (SELECT 1 FROM staff_signin WHERE staff_id = p_staff_id AND time = p_date) THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该员工当天考勤记录已存在'; ELSE INSERT INTO staff_signin(staff_id, time, absent, late, leave_early) VALUES (p_staff_id, p_date, p_absent, p_late, p_leave_early); END IF; END // DELIMITER ;这个存储过程用IF EXISTS做前置检查,如果已有记录就抛出自定义错误。SIGNAL SQLSTATE '45000'是 MySQL 抛自定义错误的写法,SQL Server 用RAISERROR或THROW。参数说明:p_absent、p_late、p_leave_early传 0 或 1。
验证存储过程是否生效,可以故意插入两条相同员工相同日期的考勤记录,看第二条是否报错。如果报错信息是该员工当天考勤记录已存在,说明校验逻辑生效。如果没报错,检查staff_signin表的主键是不是(staff_id, time)复合主键——如果是,数据库本身就会拒绝重复插入,存储过程的检查只是提前给出更友好的错误信息。
从那以后我每次做课设或者接类似的小型管理系统,都会先把建表顺序和依赖关系画在一张纸上,插数据之前先跑一遍SELECT确认外键引用的记录都存在,再执行INSERT。这个习惯帮我省了至少三次通宵排查外键报错的时间。希望帮到你。
本文还有配套的精品资源,点击获取