简介:《数据库系统课程设计——人事管理》是一份面向高校数据库课程设计场景的doc文档,适合正在完成人事管理选题、需要参考完整设计流程与报告结构的学生。文档以公司多部门人事管理为背景,围绕员工基本信息维护、部门调动、考勤统计、模糊查询及人员调入调出统计等需求展开,依次覆盖需求分析、概念设计、逻辑设计、物理设计、数据库实施、功能实现和总结,并包含数据字典、数据项、ER关系、表结构约束与SQL Server实现思路,能帮助读者梳理从业务需求到数据库落地的完整脉络。压缩包内共1个doc文件,大小约1.23MB,内容集中、便于查阅。已有1110人学习,可作为课程设计报告撰写、方案对照和答辩准备的参考资料。
1. 数据库课程设计选人事管理,难点不在建表而在"人属于部门"这句话
数据库课程设计里,人事管理几乎是出现频率最高的一道题:业务听得懂、部门员工都熟悉、表也就六张,看起来最稳。可每届交上去被打回重做的,恰恰是这一类的多——六张表一字排开挺整齐,一查"某员工某月的出勤情况"就卡住,考勤表只存了 0/1 却没留日期粒度;想统计"某部门某天的迟到人数",发现员工和部门之间隔着一张调动表,没调动过的人直接查不出来。这套人事管理系统后台数据库设计考的不是 SELECT 写得多花哨,而是需求阶段有没有把"一个员工只能属于一个部门"这种口语规则,拆成能被 SQL 追问的字段和约束。下面按课程设计报告的真实推进顺序,从需求分析、概念设计一直走到建表、视图、统计查询和答辩现场,给一套能直接照着改的完整方案。
2. 需求分析与概念设计:从七条业务规则长出的数据字典和六张局部 E-R 图
需求分析这一步最容易被跳过,直接开 SQL Server 建表,然后物理设计阶段返工。我一般会逼自己先把任务书里那段自然语言翻译成一张规则清单,再往下画图。任务书里其实已经把业务说得很清楚了:公司有若干部门,部门有若干职工和一个部门经理,经理管本部门职工,职工只能在一个部门。这句话里藏了三个约束——部门对员工是 1:n、员工对部门是 n:1(不是 m:n)、经理本身也是员工。第 3 条如果不提前想明白,后面部门表里那个"部门经理"字段就会变成一个大坑。
2.1 把业务规则写成清单,每条规则后面挂上它对应的表
我把原始需求拆成七条,每条都标注它最终落在哪张表、哪个字段、哪条约束上:
| 编号 | 业务规则 | 落点 |
|---|---|---|
| R1 | 一个部门有多名职工,一名职工只属于一个部门 | staff_work 以 staff_id 为主键(不是联合主键) |
| R2 | 每个部门有且只有一名部门经理 | department_info.manager_id 外键指向 staff_info |
| R3 | 职工基本信息可录入、修改、删除 | staff_info 的 INSERT/UPDATE/DELETE |
| R4 | 职工可在部门间调动,调动要留痕 | staff_transfer 每次调动一条记录 |
| R5 | 考勤按天记录,含缺勤、迟到、早退 | staff_signin 主键 (staff_id, time) |
| R6 | 工资含底薪、补贴、奖金、扣款、加班费、实发 | staff_salary,实发=底薪+补贴+奖金+加班费-扣款 |
| R7 | 支持模糊查询、按学历/岗位/入职时间统计 | staff_info、staff_work 上的查询与聚合 |
R1 这条规则是整个设计的胜负手。很多同学把 staff_work 的主键写成 (staff_id, department_id) 联合主键,本意是"一个员工在一个部门工作",但联合主键允许同一个 staff_id 出现两次,只是部门不同——这等于默认员工可以同时在两个部门,直接违反了任务书里"职工只能在一个部门"。正确写法是 staff_id 单独做主键,department_id 做外键,数据库层面就把"一人一部"钉死了。
R2 反过来要小心。部门经理是员工,所以 department_info 里存的是 manager_id 而不是经理姓名。但员工又必须挂在某个部门下(R1),于是 staff_info → department_info → staff_info 形成一个引用环。这不是设计错误,而是真实业务里必然存在的循环,处理办法是 manager_id 允许 NULL,先建部门再补经理。
2.2 数据字典不是抄字段:取值范围要能翻译成 CHECK 约束
数据字典是需求分析阶段的产物,作用是给下一步概念设计提供属性清单。任务书里给了一份数据项表,摘几条关键的:
| 数据项 | 类型 | 长度 | 取值范围 | 能否为空 |
|---|---|---|---|---|
| 员工编号 | int | 6 | 100000–999999 | 否 |
| 姓名 | varchar | 10 | 四个汉字以内 | 否 |
| 性别 | varchar | 4 | 男、女 | 否 |
| 年龄 | int | 3 | 18–65 | 是 |
| 身份证号 | varchar | 18 | 18 位数字 | 否 |
| 入职时间 | date | 10 | 1990-01-01 至今 | 否 |
| 缺勤/迟到/早退 | int | 1 | 0 或 1 | 是 |
| 日期 | date | 10 | 考勤当天 | 否 |
| 实发工资 | decimal | 10,2 | 不小于 0 | 否 |
这张表的价值不在好看,而在于每一条"取值范围"后面都要能对应一句 CHECK。任务书原文里年龄写的是 30–199、工龄写的是 1–99,这种明显不合理的范围如果只停留在文档里,建表时不写约束,答辩时老师随手插一条 age = 200 的数据就能把你问住。我在写数据字典时有个习惯:凡是填了取值范围的行,右边加一列"约束表达式",写不出来就说明这个范围是编的,删掉或者重新确认。
提示:数据字典里的"可否为空"和建表脚本里的 NOT NULL 必须一一对应,这是答辩时最容易被抽查的一致性。文档写"否"、脚本写 NULL,属于自己给自己挖坑。
2.3 六张局部 E-R 图怎么合并成一张全局 E-R 图,冗余在哪里消
概念设计的做法是先分后合。按业务模块拆出六个局部 E-R 图,各自只关心自己那摊事:
- 员工基本信息:员工实体,属性为编号、姓名、性别、年龄、身份证号、入职时间、电话
- 员工工作信息:员工与部门的"隶属"联系,属性为职称、工龄
- 部门信息:部门实体,属性为编号、名称、电话、经理
- 工资信息:员工与工资的 1:1 联系(一个员工一条工资标准)
- 考勤信息:员工与日期的 1:n 联系,属性为缺勤、迟到、早退
- 调动信息:员工与部门之间的两个联系,"调出"和"调入",属性为调离时间、调入时间
合并时冲突集中在两处。第一处是"所属部门":员工基本信息图里画了一个"所属部门"属性,员工工作信息图里又用"隶属"联系表达同一件事。这两者是同一个语义,必须消掉一个——保留联系,删掉属性,否则后面转关系模式时 staff_info 和 staff_work 会各存一份部门,改一处忘一处就是数据不一致的源头。第二处是部门经理:部门图里把经理当属性,但经理本身是员工实体,正确画法是部门与员工之间加一条"管理"联系(1:1),与"隶属"联系(1:n)并列。
合并后的全局 E-R 图大致是:员工实体为中心,向左连部门(隶属 1:n、管理 1:1),向右连考勤(1:n)、工资(1:1)、调动(1:n,分调出和调入两条边)。优化阶段主要做两件事:删掉可通过联系推导的冗余属性,以及把同类联系合并(比如"调出"和"调入"本质都是员工与部门的多对多联系,只是角色不同,用两个外键在同一张表里表达比建两张表更省)。
3. 逻辑设计与物理设计:E-R 转六个关系模式,再落到 SQL Server 的建库建表脚本
概念图再好,不能直接建表。逻辑设计的任务是把 E-R 图按规则翻译成关系模式,同时把完整性约束一并定下来;物理设计则是把这些模式落到具体的 DBMS 上,选类型、定文件、建索引。这两步在课程设计报告里通常是两章,但实际动手是连着做的,我合在一起说。
3.1 E-R 转关系模式的三条规则,和六个模式的最终形态
转换规则任务书里写得很标准:一个实体转一个关系模式,实体的码就是关系的码;一个联系转一个关系模式,1:1 联系两边实体的码都可做候选码,1:n 联系的码取 n 端实体的码,m:n 联系的码取两边码的组合;具有相同码的关系模式可以合并。
按这三条,六个局部图落成六个关系模式:
staff_info( staff_id, name, sex, age, id_card, hire_time, phone ) department_info( department_id, department_name, department_phone, manager_id ) staff_work( staff_id, department_id, position, work_age ) staff_signin( staff_id, time, absent, late, leave_early ) staff_salary( staff_id, salary_month, basic_salary, subsidy, bonus, overtime_pay, deduction, real_salary ) staff_transfer( transfer_id, staff_id, old_dep_id, new_dep_id, old_time, new_time )有几处是我在转换时主动做的调整,报告里最好写清楚理由。其一,staff_work 的主码只取 staff_id,因为"隶属"是 1:n 联系,码取 n 端(员工端)实体的码,这样天然保证一人一部。其二,staff_salary 补了一个 salary_month 字段——原始材料里工资表只有员工编号,意味着一个员工一辈子只有一条工资记录,发第二个月工资就得覆盖,历史全丢。按这个项目场景,通常的做法是给工资表加时间维度,主键改成 (staff_id, salary_month)。其三,staff_transfer 补了自增主键 transfer_id,因为同一个员工同一天既可能调出又可能调入,只用 (staff_id, old_time) 做主键在边界情况下会冲突。
3.2 主键、外键与三类完整性约束怎么落
实体完整性靠主键,参照完整性靠外键,用户定义完整性靠 CHECK 和 DEFAULT。这三类在报告里要分节写,在脚本里要真写出来。
-- 员工基本信息表:主键 + 唯一约束 + 域完整性 CREATE TABLE staff_info ( staff_id INT NOT NULL PRIMARY KEY, -- 员工编号,主键 name VARCHAR(10) NOT NULL, -- 姓名 sex VARCHAR(4) NOT NULL CONSTRAINT ck_staff_sex CHECK (sex IN ('男','女')), -- 性别只允许两个值 age INT NULL CONSTRAINT ck_staff_age CHECK (age BETWEEN 18 AND 65), id_card VARCHAR(18) NOT NULL CONSTRAINT uq_staff_idcard UNIQUE, -- 身份证唯一 hire_time DATE NOT NULL, phone VARCHAR(11) NOT NULL ); -- 部门信息表:manager_id 可空,先建部门后补经理,避免循环引用插不进数据 CREATE TABLE department_info ( department_id INT NOT NULL PRIMARY KEY, department_name VARCHAR(20) NOT NULL CONSTRAINT uq_dep_name UNIQUE, department_phone VARCHAR(11) NOT NULL, manager_id INT NULL CONSTRAINT fk_dep_manager REFERENCES staff_info(staff_id) ); -- 员工工作信息表:staff_id 单独做主键,从结构上锁死"一人一部" CREATE TABLE staff_work ( staff_id INT NOT NULL PRIMARY KEY CONSTRAINT fk_work_staff REFERENCES staff_info(staff_id), department_id INT NOT NULL CONSTRAINT fk_work_dep REFERENCES department_info(department_id), position VARCHAR(10) NULL, -- 职称:部门经理 / 员工 work_age INT NULL CONSTRAINT ck_work_age CHECK (work_age BETWEEN 0 AND 60) ); -- 考勤表:主键为员工+日期,同一天不允许两条记录 CREATE TABLE staff_signin ( staff_id INT NOT NULL CONSTRAINT fk_sign_staff REFERENCES staff_info(staff_id), time DATE NOT NULL, absent INT NULL CONSTRAINT ck_sign_absent CHECK (absent IN (0,1)), late INT NULL CONSTRAINT ck_sign_late CHECK (late IN (0,1)), leave_early INT NULL CONSTRAINT ck_sign_early CHECK (leave_early IN (0,1)), CONSTRAINT pk_signin PRIMARY KEY (staff_id, time) );代码里有几个决定值得说明。第一,外键全部没有加 ON DELETE CASCADE。人事数据里删一个部门就把该部门所有员工连带删掉,是事故不是功能,所以默认 NO ACTION,删除前必须先处理下级数据。第二,manager_id 允许 NULL 但同时是外键,这意味着插入部门时可以先不指定经理,等员工入职后再 UPDATE 回填,绕开了循环引用导致的"第一行永远插不进去"问题。第三,薪资相关的字段这里统一用 DECIMAL(10,2) 而不是 FLOAT,原因放在后面避坑那章细讲。
3.3 物理设计:文件组、初始大小与增长方式
物理设计阶段要回答的是"这些表放在哪、怎么长"。SQL Server 的数据库由数据文件和日志文件组成,任务书里提到"一个数据文件、一个次要文件和一个事务日志文件",对应的做法是把主数据文件放主文件组,历史类数据(比如考勤、调动)放次要数据文件所在的文件组,日志单独一个文件。
-- 建库:主文件 + 次要文件(归档文件组)+ 日志文件 CREATE DATABASE hrms ON PRIMARY ( NAME = 'hrms_data', FILENAME = 'D:\db\hrms_data.mdf', SIZE = 100MB, FILEGROWTH = 10% -- 按比例增长,避免频繁扩文件 ), FILEGROUP FG_ARCHIVE ( NAME = 'hrms_archive', FILENAME = 'D:\db\hrms_archive.ndf', SIZE = 50MB, FILEGROWTH = 10MB ) LOG ON ( NAME = 'hrms_log', FILENAME = 'D:\db\hrms_log.ldf', SIZE = 50MB, FILEGROWTH = 10MB );初始大小和增长率这两个参数在课程设计里经常被忽略,直接默认 1MB 按百分比增长。真跑起批量插入考勤数据时,文件会以很小的步长反复扩展,性能抖动很明显。我一般把初始大小设成预估数据量的 1.2 倍左右,增长率用固定 MB 而不是百分比,日志文件同理。归档文件组 FG_ARCHIVE 用来放 staff_signin 和 staff_transfer 这两张只增不改的表,需要时可以把它们单独指定到该文件组:
-- 把考勤表迁到归档文件组上(先建聚集索引指定文件组) CREATE CLUSTERED INDEX ix_signin_pk ON staff_signin(staff_id, time) WITH (DROP_EXISTING = ON) ON FG_ARCHIVE;3.4 索引建在哪几列,为什么不是越多越好
六张表里最需要索引的是考勤表和调动表,因为它们的统计查询最重。索引规划如下:
| 表 | 索引 | 类型 | 服务的查询 |
|---|---|---|---|
| staff_signin | (staff_id, time) | 聚集 | 按员工+年月统计出勤 |
| staff_signin | (time) INCLUDE(late, leave_early) | 非聚集 | 按某日统计部门迟到早退 |
| staff_transfer | (old_dep_id, old_time) | 非聚集 | 按年统计调出人数 |
| staff_transfer | (new_dep_id, new_time) | 非聚集 | 按年统计调入人数 |
| staff_info | (name) | 非聚集 | 精确/前缀查询 |
这里有个常见误解要提前说清楚:给 name 建了索引,不代表WHERE name LIKE '%张%'会变快。前置通配符让优化器无法做索引查找,只能全表扫。真正能吃到索引的是LIKE '张%'。模糊查询的写法在下一章具体展开。另外索引不是免费午餐,每建一个都要维护,六张表建五六个索引足够了,把所有列都加上索引反而拖慢插入。
4. 功能实现:五个视图打底,四类统计查询把需求逐条兑现
建完表进入功能实现,这一步在报告里最容易写成"贴一堆 SQL 截图"。我的做法是先按"用户要看什么"把查询分成四类——明细、模糊、考勤统计、调动统计,每类先建视图简化,再在视图上写聚合。视图在这里不只是语法练习,它把四表连接封起来,后面所有查询都短了一截,权限也好控制。
4.1 五个视图:把多表连接封装成一张宽表
原始报告里给五个视图:员工个人信息、部门员工工作信息、员工工资信息、员工考勤信息、各部门员工调动信息。其中前两个视图的连接写法有一个必须修的问题——它们通过 staff_transfer 去连部门,导致从来没调动过的员工查不出来。修正后的写法:
-- 员工个人信息视图:员工 → 工作信息 → 部门,走 staff_work,不走调动表 IF OBJECT_ID('v_staff_profile', 'V') IS NOT NULL DROP VIEW v_staff_profile; GO CREATE VIEW v_staff_profile AS SELECT s.staff_id AS 员工编号, s.name AS 姓名, s.sex AS 性别, d.department_id AS 部门编号, d.department_name AS 部门名称, w.position AS 职位, w.work_age AS 工龄, s.phone AS 联系电话 FROM staff_info s JOIN staff_work w ON s.staff_id = w.staff_id JOIN department_info d ON w.department_id = d.department_id; GO关键在于 JOIN 的链路:员工的当前部门永远记在 staff_work.department_id 上,调动表只记录历史,不承担"当前归属"的职责。用 staff_work 连接,任何在职员工都查得到,不管他调没调过岗。
考勤视图同理,只连员工表:
-- 员工考勤视图:一行 = 一名员工一天的考勤 CREATE VIEW v_staff_signin AS SELECT s.staff_id AS 员工编号, s.name AS 姓名, g.time AS 日期, g.absent AS 缺勤, g.late AS 迟到, g.leave_early AS 早退 FROM staff_info s JOIN staff_signin g ON s.staff_id = g.staff_id;工资视图和调动视图按同样思路写,工资视图直接连 staff_salary,调动视图需要给 department_info 起两个别名,一个查原部门名称、一个查新部门名称:
-- 调动视图:部门表连两次,必须用别名区分原部门和新部门 CREATE VIEW v_staff_transfer AS SELECT s.staff_id AS 员工编号, s.name AS 姓名, o.department_name AS 原部门名称, t.old_time AS 调离时间, n.department_name AS 新部门名称, t.new_time AS 调入时间 FROM staff_transfer t JOIN staff_info s ON t.staff_id = s.staff_id JOIN department_info o ON t.old_dep_id = o.department_id JOIN department_info n ON t.new_dep_id = n.department_id;原来那版写法里,原部门名称和新部门名称都取的是同一个 department_info.department_name,等于两个字段显示同一个值,答辩时一演示就露馅。同一张表在一条查询里出现两次,必须起别名,这是 SQL 的基本功,也是报告里最容易被挑出来的错误。
4.2 明细查询与模糊查询:条件怎么拼才用得上索引
明细查询是基础功能,按员工编号、姓名、部门、学历等任意组合过滤。课程设计里通常要求"至少支持模糊查询",写法上要注意参数拼接:
-- 模糊查询:按姓名片段、部门、职称组合过滤 SELECT 员工编号, 姓名, 部门名称, 职位, 工龄 FROM v_staff_profile WHERE 姓名 LIKE '张%' -- 前缀匹配,可走索引 AND 部门名称 = '技术部' AND 职位 = '员工' ORDER BY 员工编号;LIKE '张%'和LIKE '%张%'的差别要能讲出来:前者是前缀查找,优化器可以拿索引做范围扫描;后者是包含匹配,只能逐行比对。如果业务确实要"名字里带张就行",那就接受全表扫,但表数据量小的时候影响不大。真正不能接受的是把用户输入直接拼进字符串:
-- 反例:拼接字符串,输入 ' OR 1=1 -- 直接把全表捞出来 SET @sql = 'SELECT * FROM staff_info WHERE name = ''' + @keyword + '''';正确做法是用参数化查询,或者在存储过程里用 sp_executesql 传参。课程设计里数据是自己造的,感受不到风险,但答辩老师问一句"这里如果输入单引号会怎样",答不上来就尴尬了。
4.3 考勤统计:一条 SQL 同时给出"按年月算个人"和"按日算部门"
任务书对考勤提了两个硬指标:按年份月份统计某个职工的出勤情况,按某年某月某日统计某部门的迟到和早退人数。这两个查询的粒度完全不同,要分开写。
先看按员工按月的:
-- 统计某员工 2019 年每个月的出勤明细 SELECT s.staff_id AS 员工编号, s.name AS 姓名, CONVERT(CHAR(7), g.time, 120) AS 月份, -- 取 yyyy-mm COUNT(*) AS 应出勤天数, SUM(g.absent) AS 缺勤天数, COUNT(*) - SUM(g.absent) AS 实际出勤天数, SUM(g.late) AS 迟到次数, SUM(g.leave_early) AS 早退次数 FROM staff_signin g JOIN staff_info s ON g.staff_id = s.staff_id WHERE g.staff_id = 100001 AND g.time >= '2019-01-01' AND g.time < '2020-01-01' -- 半开区间,避免日期时间边界问题 GROUP BY s.staff_id, s.name, CONVERT(CHAR(7), g.time, 120) ORDER BY 月份;CONVERT(CHAR(7), g.time, 120)把日期截成 yyyy-mm 做分组键,比用 MONTH() 和 YEAR() 两个函数再拼更省事,也不会因为跨年把 1 月混到一起。日期范围用>= 起始 AND < 次年起始而不是 BETWEEN,是因为 time 字段虽然是 DATE 类型,但如果哪天有人改成 DATETIME,BETWEEN 的右端点会把 12 月 31 日 00:00 之后的数据漏掉,这种边界问题在统计里是血泪经验。
再看按部门按日的:
-- 统计某部门在 2019-09-19 当天的迟到、早退人数 SELECT d.department_id AS 部门编号, d.department_name AS 部门名称, COUNT(*) AS 当日有记录人数, SUM(CASE WHEN g.late = 1 THEN 1 ELSE 0 END) AS 迟到人数, SUM(CASE WHEN g.leave_early = 1 THEN 1 ELSE 0 END) AS 早退人数, SUM(CASE WHEN g.absent = 1 THEN 1 ELSE 0 END) AS 缺勤人数 FROM staff_work w JOIN department_info d ON w.department_id = d.department_id LEFT JOIN staff_signin g ON g.staff_id = w.staff_id AND g.time = '2019-09-19' -- 日期条件必须写在 ON 里 WHERE d.department_id = 1 GROUP BY d.department_id, d.department_name;这里最值得讲的是日期条件放在 ON 还是 WHERE。放在 WHERE 里,LEFT JOIN 会退化成 INNER JOIN,当天没考勤记录的人直接被过滤掉,"缺勤人数"就统计不到;放在 ON 里,没记录的人保留下来,考勤字段为 NULL,再用 SUM(CASE WHEN ...) 统一处理,才能把"没打卡"和"打了卡但没迟到"区分开。用 COUNT(*) 数总人数也要注意,LEFT JOIN 产生的 NULL 行仍会被计数,所以"当日有记录人数"这个字段名要写准确,不能叫成"当日出勤人数"。
4.4 调动统计:按年统计各部门调入调出人数
调动统计的难点在于一张表里有两个部门字段,调入和调出要分别聚合再贴回部门表。用两个 CTE 各算各的,再按部门和年份做全外连接最稳:
-- 按年统计各部门的调入、调出人数 WITH in_cnt AS ( SELECT new_dep_id AS dep_id, YEAR(new_time) AS y, COUNT(*) AS cnt FROM staff_transfer GROUP BY new_dep_id, YEAR(new_time) ), out_cnt AS ( SELECT old_dep_id AS dep_id, YEAR(old_time) AS y, COUNT(*) AS cnt FROM staff_transfer GROUP BY old_dep_id, YEAR(old_time) ) SELECT d.department_id AS 部门编号, d.department_name AS 部门名称, COALESCE(i.y, o.y) AS 年份, ISNULL(i.cnt, 0) AS 调入人数, ISNULL(o.cnt, 0) AS 调出人数, ISNULL(i.cnt, 0) - ISNULL(o.cnt, 0) AS 净增人数 FROM department_info d FULL OUTER JOIN in_cnt i ON d.department_id = i.dep_id FULL OUTER JOIN out_cnt o ON d.department_id = o.dep_id AND i.y = o.y;用 FULL OUTER JOIN 而不是 LEFT JOIN,是因为有些部门某年只出不进,用 LEFT JOIN 以调入为主表会丢掉这些行。AND i.y = o.y这个连接条件也不能少,否则同一部门不同年份的调入调出会交叉配对,2019 年调入的人会和 2020 年调出的人凑成一行。这段逻辑在报告里最好配一句说明:两个子查询分别以"新部门"和"原部门"为分组维度,主查询再按部门+年份把它们对齐。
如果要把这几类统计做成可重复调用的功能,用存储过程包起来比每次手写 SQL 更像一个"系统":
-- 按员工和年份查月度考勤的存储过程 CREATE PROCEDURE usp_AttendanceByMonth @staff_id INT, @year INT AS BEGIN SET NOCOUNT ON; SELECT CONVERT(CHAR(7), g.time, 120) AS 月份, SUM(g.absent) AS 缺勤天数, SUM(g.late) AS 迟到次数, SUM(g.leave_early) AS 早退次数 FROM staff_signin g WHERE g.staff_id = @staff_id AND g.time >= DATEFROMPARTS(@year, 1, 1) AND g.time < DATEFROMPARTS(@year + 1, 1, 1) GROUP BY CONVERT(CHAR(7), g.time, 120) ORDER BY 月份; END;两个参数分别控制"谁"和"哪一年",调用时EXEC usp_AttendanceByMonth 100001, 2019就能出表,演示环节比手敲一长串 SQL 体面得多。
5. 避坑与排查:这五处是答辩时最容易被追问到的地方
做完上面四步,功能基本能跑通,但报告要拿高分、作品要经得起现场操作,得先把几个高频翻车点自己排查一遍。下面每一条都是我在改这份课设时真实踩过或见到别人踩过的,按"现象、原因、解决"三段写,可以直接当成自查清单。
5.1 现象:新入职员工在个人信息视图里查不到
原因:视图的 FROM 子句里连了 staff_transfer,用 inner join 一路穿过去。逻辑上等于"只有调动过的员工才算存在",没调过岗的人被连接条件过滤掉了。这个错误在数据量小的时候不明显,因为造测试数据时往往人人都有一条调动记录。
解决:视图的部门来源统一改成 staff_work.department_id,staff_transfer 只服务于调动历史和调动统计,两者职责分开。改完可以用SELECT COUNT(*) FROM staff_info和SELECT COUNT(*) FROM v_staff_profile对比,两个数必须相等,这条自检建议写进报告的功能测试小节。
5.2 现象:建表脚本在 SQL Server 里报"关键字 IF 附近有语法错误"
原因:脚本里混用了其他数据库的方言。DROP VIEW IF EXISTS 视图名是 MySQL 的写法,SQL Server 2016 之前都不支持;字段名用反引号包裹name也是 MySQL 习惯,SQL Server 认的是方括号[name]或双引号。课程设计报告里如果同时出现两种方言,老师一眼就能看出脚本没实际跑过。
解决:统一成 T-SQL 写法,删对象先判断存在:
IF OBJECT_ID('v_staff_profile', 'V') IS NOT NULL DROP VIEW v_staff_profile; GO CREATE VIEW v_staff_profile AS ...OBJECT_ID的第二个参数 'V' 表示视图,'U' 表示用户表。字段名如果确实是保留字(name、time、position 都是),要么加方括号,要么改名叫 staff_name、sign_date、job_title,后者更彻底,也省得后面每写一次都要加括号。
5.3 现象:第二个月发工资,上个月的记录没了
原因:工资表的主键只有 staff_id,一个员工只能存一条工资记录。第一次发工资是 INSERT,第二次就变成 UPDATE,历史数据被覆盖,想查"某员工 2019 年全年收入"根本无从查起。
解决:给工资表加时间维度。最小改动是加一列 salary_month CHAR(7),主键改成 (staff_id, salary_month):
ALTER TABLE staff_salary ADD salary_month CHAR(7) NOT NULL CONSTRAINT ck_salary_month CHECK (salary_month LIKE '[0-9][0-9][0-9][0-9]-[0-9][0-9]'); ALTER TABLE staff_salary DROP CONSTRAINT pk_salary; ALTER TABLE staff_salary ADD CONSTRAINT pk_salary PRIMARY KEY (staff_id, salary_month);改完之后,"按年统计某员工总收入"就变成一句 GROUP BY LEFT(salary_month,4) 的事。这类"表设计时少了一个维度"的问题,越往后改代价越大,需求分析阶段就该问一句"工资是按月发还是只发一次"。
5.4 现象:插入第一个部门时报外键冲突,插第一个员工时也报
原因:department_info.manager_id 引用 staff_info,staff_info 的员工又要通过 staff_work 关联到部门,三方互相引用。如果 manager_id 设成 NOT NULL,第一个部门插不进去;如果先插员工,员工的部门外键又找不到部门。这就是循环引用导致的"先有鸡还是先有蛋"。
解决:manager_id 允许 NULL,插入顺序固定为 staff_info → department_info → staff_work,最后再 UPDATE 回填经理。另外部门经理不要在 department_info 里存姓名字符串,姓名是员工实体的属性,存在部门表里意味着员工改名要同步改两处,改漏一处就是数据不一致。如果报告里图省事写了部门经理 VARCHAR(8),答辩时至少要说清楚这是个有意的反规范化,并且说明代价。
5.5 现象:工资合计对不上,实发工资和各项加起来差几分钱
原因:金额字段用了 FLOAT。浮点数在二进制里无法精确表示 0.1、0.2 这类十进制小数,求和几十条记录后误差会累积,出现 3999.9999999 这种值。另外实发工资如果靠手工录入而不是计算得来,本身就和分项脱节,改一个奖金金额实发不变。
解决:金额统一用 DECIMAL(10,2),实发工资用计算列或者触发器保证联动:
-- 用计算列保证实发工资永远等于分项之和,不给手工改的机会 ALTER TABLE staff_salary ADD real_salary AS (basic_salary + subsidy + bonus + overtime_pay - deduction) PERSISTED;PERSISTED 表示把计算结果存下来,可以在上面建索引,查询时不用每次重算。如果担心某些月份有特殊调整,那就保留手工字段,但加一条 CHECK 约束让两者差额不超过 1 元,超了直接插不进去,问题当场暴露。
6. 验收与答辩:把九个字段的演示顺序和八个必问问题提前排好
课程设计的最后一天通常是验收答辩,进程安排里只给了 1 天,但这 1 天的表现决定了前面 8 天的评价。我见过功能做得挺完整、演示时手忙脚乱翻文件的,也见过表只有六张但讲得清清楚楚拿高分的。差别不在代码量,在有没有提前把"演示脚本"和"问答脚本"写出来。
演示顺序建议按数据流向走,一条线串到底,不要东点一下西点一下:
| 步骤 | 演示内容 | 对应报告章节 | 预计耗时 |
|---|---|---|---|
| 1 | 展示建库脚本与文件组配置 | 物理设计 | 1 分钟 |
| 2 | 执行六张表的建表脚本,展示约束生效(故意插一条 sex='未知' 被拒) | 数据库实施 | 2 分钟 |
| 3 | 插入两三个部门、十名员工、几条调动记录 | 功能实现 | 2 分钟 |
| 4 | 打开五个视图,对比视图结果与基表行数 | 功能实现 | 2 分钟 |
| 5 | 演示模糊查询与多条件组合查询 | 功能实现 | 1 分钟 |
| 6 | 执行按员工按月的考勤统计存储过程 | 功能实现 | 2 分钟 |
| 7 | 执行按部门按日的迟到早退统计 | 功能实现 | 1 分钟 |
| 8 | 执行按年统计各部门调入调出人数 | 功能实现 | 2 分钟 |
第 2 步里"故意插一条非法数据被约束拒绝"这个动作,是我每次都会加的。它比展示十条成功插入更能说明你懂约束,而且现场报错是预期内的,心态稳。
答辩问题基本集中在设计决策上,下面这八个几乎每届都问:
| 问题 | 回答要点 |
|---|---|
| 为什么 staff_work 的主键只用 staff_id? | 隶属是 1:n 联系,码取 n 端,从结构上保证一人一部 |
| 部门经理为什么不用姓名字段? | 姓名是员工属性,冗余存储会导致更新异常;用外键指向员工表 |
| 员工调动后,当前部门从哪查? | 从 staff_work 查当前,staff_transfer 只存历史,两者职责分离 |
| 考勤表为什么用 (staff_id, time) 联合主键? | 一人一天一条,天然防重复打卡 |
| 工资为什么用 DECIMAL 不用 FLOAT? | 浮点误差在金额求和时会累积,DECIMAL 精确 |
| 视图能不能更新? | 单表视图通常可更新,多表连接视图一般不可,要通过基表改 |
| 索引建多了有什么问题? | 插入更新要维护索引,降低写性能,占用空间 |
| 如果员工数量涨到十万,哪里会先慢? | 模糊查询的前导通配符和按日全表扫,需要加覆盖索引或改查询条件 |
最后一个问题是拉开差距的地方。大部分同学只会说"加索引",能具体指出"LIKE '%x%' 用不上索引,要么改成前缀匹配,要么加全文索引,要么把统计结果预先算好存到汇总表",说明你真的想过数据量上去之后会发生什么。
脚本执行顺序也要在报告附录里写清楚:建库 → 建表(先 staff_info,再 department_info,再 staff_work 及其余四张)→ 建索引 → 建视图 → 建存储过程 → 插测试数据。顺序错了就是外键冲突,现场重来一遍很掉分。
从那以后我每次带课设,都强制自己先把"演示脚本"写成一份带时间点的清单,再倒推去补功能——演示不出来的功能,等于没做。希望这套结构能帮到你,少走几趟返工的路。
本文还有配套的精品资源,点击获取