简介:职业介绍信息管理系统的数据库课程设计资源,面向数据库相关专业学生,以SQL Server为环境,完整演示了需求分析、数据库设计与SQL编程等课程设计核心环节,适合作为课程设计或数据库实践的参考。压缩包共8个文件,包括6个SQL脚本、1个课程设计报告doc和1个数据库备份bak,整体仅283KB。SQL脚本分别定义职业分类、介绍人员、求职者信息、用人单位、费用管理、职业信息等6张业务表,doc报告覆盖课程设计目的、系统分析和数据库设计等章节,bak备份可直接附加还原,便于查看完整的表结构和示例数据。目前已有1538人学习下载,系高分课设成果,既能帮助理解数据库设计方法,也能用于课程设计报告撰写和SQL Server建表练习,配合源码与报告可快速复现项目流程,实用性强。
1. 职业介绍信息管理系统:数据库课程设计里性价比最高的一类选题
数据库课程设计最怕的其实不是写代码,是选题。学生管理系统、图书管理系统做完交上去,答辩老师顺着业务问两句就冷场了,因为那些系统的表关系太单薄。职业介绍信息管理系统不一样,它天然存在求职者、招聘企业、管理员三类角色,职位与求职者之间是典型的多对多关系,拆出一张应聘记录表之后,增删改查、连表统计、事务、索引、备份恢复这些数据库课程设计要覆盖的点全都用得上。这个题目门槛不高,一个 MySQL 加一个 Java 控制台或 Swing 界面就能跑通,往上做空间也大,可以加登录鉴权、报表和可视化。适合正在做数据库课程设计、需要交可运行系统和设计报告的同学,也适合想把它扩展成毕业设计前站的人。
2. 先把表结构立住:职业介绍系统的关系模型与建表 SQL
2.1 五张表怎么连:求职者、企业、职位和应聘记录的关系
设计关系模型之前,先把业务语言翻译成实体。管理员负责维护基础数据、审核企业入驻,这是第一个实体;求职者填简历、投职位,是第二个;企业发布职位、查看候选人,是第三个;职位本身是第四个;求职者投递职位这个动作产生应聘记录,是第五个。课程设计里通常还会想到加行业表、学历表这类字典表,但做职业介绍系统时行业直接作为企业表的一个字段就够用,单独成表反而让报告显得为了建模而建模。
关键的建模决策在“求职者—职位”这一段。一个求职者可以投多个职位,一个职位可以被多个求职者投递,这是教科书上标准的多对多关系。多对多不能直接用两张表表达,必须拆成“求职者 1—N 应聘记录 N—1 职位”,t_application 就是那张中间表。拆完之后,应聘记录表里存 seeker_id、position_id、apply_time、result 四个字段,既能回答“某某投了哪些职位”,也能回答“某个职位收到了多少简历”,统计查询都压在这张表上。另一个决策是字段冗余的控制:企业名称只在 t_company 里存一份,职位表通过 company_id 关联,不把公司名冗余进职位表,这是第三范式的基本要求,答辩时老师一定会问。
设计时我一般还会加两个“看起来多余”的字段。一是 create_time 默认当前时间,方便演示“最近新增了哪些数据”;二是 status 软删除标记,企业注销、职位下线、求职者退库都不应该物理删行,因为应聘记录还要留作历史统计。这两个字段在后面的统计 SQL 里处处要用,现在定了,后面少改一次表结构。
2.2 建表 SQL 一次到位:主键、外键、索引和字符集怎么配
表结构定了就落 SQL。下面的建表脚本按照“先主表、后子表”的顺序执行,因为外键要求被引用的表先存在。字符集统一用 utf8mb4,别用默认的 latin1,否则插入中文直接变问号,这个坑后面第四章会细说。
-- 创建数据库,字符集统一 utf8mb4 CREATE DATABASE job_intro_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE job_intro_db; -- 管理员表 CREATE TABLE t_admin ( admin_id INT AUTO_INCREMENT PRIMARY KEY, admin_name VARCHAR(20) NOT NULL UNIQUE, admin_pwd VARCHAR(64) NOT NULL, -- 存 SHA-256 摘要,别存明文 real_name VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 求职者表 CREATE TABLE t_job_seeker ( seeker_id INT AUTO_INCREMENT PRIMARY KEY, seeker_name VARCHAR(20) NOT NULL, gender CHAR(1) DEFAULT '男', age TINYINT UNSIGNED, education VARCHAR(30), -- 学历,统计报表按它分组 major VARCHAR(50), phone VARCHAR(20), expected_salary DECIMAL(10,2), -- 期望薪资,金额一律用定点数 status TINYINT DEFAULT 1, -- 1 正常 0 注销 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 招聘企业表 CREATE TABLE t_company ( company_id INT AUTO_INCREMENT PRIMARY KEY, company_name VARCHAR(100) NOT NULL, industry VARCHAR(50), contact_person VARCHAR(20), contact_phone VARCHAR(20), address VARCHAR(200), status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 职位表,企业一对多职位 CREATE TABLE t_position ( position_id INT AUTO_INCREMENT PRIMARY KEY, company_id INT NOT NULL, position_name VARCHAR(100) NOT NULL, salary_min DECIMAL(10,2), salary_max DECIMAL(10,2), requirement TEXT, status TINYINT DEFAULT 1, -- 1 招聘中 0 已下线 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_position_company FOREIGN KEY (company_id) REFERENCES t_company (company_id) ) ENGINE=InnoDB; -- 应聘记录表,多对多关系的中间表 CREATE TABLE t_application ( application_id INT AUTO_INCREMENT PRIMARY KEY, seeker_id INT NOT NULL, position_id INT NOT NULL, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, result TINYINT DEFAULT 0, -- 0 待处理 1 通过 2 未通过 UNIQUE KEY uk_seeker_position (seeker_id, position_id), CONSTRAINT fk_app_seeker FOREIGN KEY (seeker_id) REFERENCES t_job_seeker (seeker_id), CONSTRAINT fk_app_position FOREIGN KEY (position_id) REFERENCES t_position (position_id) ) ENGINE=InnoDB;几个参数说明,这些也是设计报告里值得写的点。所有表都用 InnoDB,只有它支持外键约束和行级锁,这是后面并发和事务的前提。年龄用 TINYINT UNSIGNED 而不是 INT,人的年龄不会超过 255,用无符号小整型省空间,也防止写入负数这种脏数据。薪资用 DECIMAL(10,2) 不用 FLOAT,浮点数存金额会有 0.1 加 0.2 不等于 0.3 的精度问题,定点数才能保证报表里薪资求和正确。t_application 上加了 (seeker_id, position_id) 的唯一索引,作用是防止同一求职者对同一职位重复投递,这是业务规则在数据库层的兜底。外键约束名用 fk_ 前缀,报错时一眼能看出是哪张表和哪张表的关系出了问题。
索引这一步要顺带说清楚。InnoDB 会在外键列上自动建索引,所以 t_position.company_id、t_application.seeker_id、t_application.position_id 这三处不需要手动再建普通索引。唯一索引 uk_seeker_position 本身也是一个联合索引,查询“某求职者投了哪些职位”时,MySQL 会直接走这个索引定位 seeker_id,回表次数少。答辩时老师问索引怎么设计的,把这四点说全:主键聚簇索引、外键自动索引、唯一索引兜底重复投递、统计查询的 GROUP BY 字段走索引,基本就不会再追问了。
3. 跑通数据库增删改查:连接池、标准 DAO 与统计查询落地
3.1 为什么选连接池:HikariCP 替代 DriverManager 的参数说明
控制台或 Swing 版的课程设计,最常见的初始写法是每次操作都 DriverManager.getConnection,用完再 close。单机演示时没问题,但只要连续点几十次增删改查,或者开两个窗口同时操作,立刻能感觉到界面卡顿。原因是一个物理连接的建立要走 TCP 握手、MySQL 认证、分配线程,平均几十毫秒,高频操作时大量时间都耗在“开连接”而不是“执行 SQL”上。更糟的是漏写 close 时连接不释放,MySQL 端连接数涨满,后续操作全部报错。
解决方法是引入数据库连接池,课程设计里用 HikariCP 最省事,一个 jar 包加几行配置就能替换原来的获取连接方式。连接池的原理是启动时先创建几个空闲连接放着,业务要连接时从池里借,用完归还而不是销毁,连接的创建成本只承担一次。我一般把连接池封装成一个工具类,启动时初始化一次,后面所有 DAO 都从这里拿连接。
import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; public class DbPool { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/job_intro_db" + "?useUnicode=true&characterEncoding=utf8mb4" + "&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("你的密码"); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(3000); config.setMaxLifetime(1800000); config.setPoolName("jobIntroPool"); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }参数按课程设计的机器和业务量来调,别直接抄网上大厂配置。maximumPoolSize 设 10 就够,理论依据是 CPU 核心数乘 2 加磁盘数,课设机器一般 4 核,10 已经是富裕值,设成 50 反而会让 MySQL 连接数被这个程序占满。connectionTimeout 设 3000 毫秒,作用是池里没空闲连接时快速失败,而不是让界面无限期等下去,演示时看到报错能马上定位是连接泄漏。maxLifetime 设 30 分钟,必须小于 MySQL 的 wait_timeout 默认值 8 小时,否则池里的连接被 MySQL 服务端断掉后,程序还在用“死连接”查询,会出现偶发的通信链路异常。poolName 设上,日志里能区分是这个应用占用的连接。
3.2 求职者信息增删改查:PreparedStatement 标准 DAO 写法
有了连接池,DAO 层就是标准四件套。这里用 PreparedStatement,目的是让 SQL 结构和参数分离,既避免字符串拼接带来的注入风险,也让 MySQL 可以复用执行计划。增删改查里,新增和修改用 executeUpdate,查询用 executeQuery,删除我建议做成软删除,把 status 置为 0,保住应聘记录的历史数据。
public class SeekerDao { public int addSeeker(JobSeeker s) { String sql = "INSERT INTO t_job_seeker" + "(seeker_name, gender, age, education, major, phone, expected_salary) " + "VALUES(?,?,?,?,?,?,?)"; try (Connection conn = DbPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getSeekerName()); ps.setString(2, s.getGender()); ps.setInt(3, s.getAge()); ps.setString(4, s.getEducation()); ps.setString(5, s.getMajor()); ps.setString(6, s.getPhone()); ps.setBigDecimal(7, s.getExpectedSalary()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("新增求职者失败", e); } } public int updateSeeker(JobSeeker s) { String sql = "UPDATE t_job_seeker SET seeker_name=?, gender=?, age=?, " + "education=?, major=?, phone=?, expected_salary=? " + "WHERE seeker_id=? AND status=1"; try (Connection conn = DbPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getSeekerName()); ps.setString(2, s.getGender()); ps.setInt(3, s.getAge()); ps.setString(4, s.getEducation()); ps.setString(5, s.getMajor()); ps.setString(6, s.getPhone()); ps.setBigDecimal(7, s.getExpectedSalary()); ps.setInt(8, s.getSeekerId()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("修改求职者失败", e); } } public int softDeleteById(int seekerId) { String sql = "UPDATE t_job_seeker SET status=0 WHERE seeker_id=?"; try (Connection conn = DbPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, seekerId); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("删除求职者失败", e); } } public JobSeeker findById(int seekerId) { String sql = "SELECT * FROM t_job_seeker WHERE seeker_id=? AND status=1"; try (Connection conn = DbPool.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, seekerId); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { JobSeeker s = new JobSeeker(); s.setSeekerId(rs.getInt("seeker_id")); s.setSeekerName(rs.getString("seeker_name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setEducation(rs.getString("education")); s.setMajor(rs.getString("major")); s.setPhone(rs.getString("phone")); s.setExpectedSalary(rs.getBigDecimal("expected_salary")); return s; } } return null; } catch (SQLException e) { throw new RuntimeException("查询求职者失败", e); } } }逻辑说明:每个方法都依赖 try-with-resources 语法,方法结束时 ResultSet、Statement、Connection 自动关闭,连接不是真的销毁而是归还给连接池,这是防止连接泄漏的关键,也是和旧写法最本质的区别。所有查询都带 status=1 条件,保证软删除的数据不会出现在业务界面里。setBigDecimal 对应表里的 DECIMAL 字段,不要用 setFloat 或 setDouble,精度对不上。返回值用 int,是受影响行数,调用方可以根据返回值判断 0 表示没改到数据,给用户提示“记录不存在或已注销”,比只抛异常友好得多。
3.3 查询数据库做统计:职位热度、学历分布和企业用人报表
增删改查能跑通只是及格,课程设计想拿高分,统计查询是必须有的。职业介绍系统最自然的三个统计是:哪些职位被投得多、求职者的学历分布、每个企业有多少在招职位。这三个查询分别考察连表、分组聚合和 LEFT JOIN 的用法,也是答辩时最能讲出东西的部分。
-- 热门职位 TOP5:职位表左连接应聘记录表,投递数为 0 的职位也要保留 SELECT p.position_name, c.company_name, COUNT(a.application_id) AS apply_count FROM t_position p JOIN t_company c ON p.company_id = c.company_id LEFT JOIN t_application a ON p.position_id = a.position_id WHERE p.status = 1 GROUP BY p.position_id, p.position_name, c.company_name ORDER BY apply_count DESC LIMIT 5; -- 学历分布:求职者表按 education 分组计数 SELECT education, COUNT(*) AS cnt FROM t_job_seeker WHERE status = 1 GROUP BY education ORDER BY cnt DESC; -- 企业用人需求:每个企业在招职位数和平均薪资上限 SELECT c.company_id, c.company_name, COUNT(p.position_id) AS position_cnt, AVG(p.salary_max) AS avg_salary_max FROM t_company c LEFT JOIN t_position p ON c.company_id = p.company_id AND p.status = 1 WHERE c.status = 1 GROUP BY c.company_id, c.company_name HAVING COUNT(p.position_id) > 0 ORDER BY position_cnt DESC;三个查询各有一个值得讲的设计点。热门职位用了 LEFT JOIN 而不是 INNER JOIN,一个刚发布还没人投的职位也应该出现在统计结果里,只是投递数为 0,如果用 INNER JOIN 这种职位会被过滤掉。企业用人报表把 p.status = 1 条件放在 JOIN 的 ON 里而不是 WHERE 里,这是 LEFT JOIN 的经典规则:过滤右表条件放 ON,过滤主表条件放 WHERE,放错位置结果就不对,第四章会再讲一次这个翻车现场。HAVING 和 WHERE 的分工也要说清,WHERE 在分组前过滤行,HAVING 在分组后过滤组,这里要求“只保留在招职位数大于 0 的企业”,是分组后的条件,必须用 HAVING。
这三个查询跑出来的数据可以直接喂给界面层的表格组件,如果想加可视化,把 COUNT 结果导成柱状图数据源也不会额外增加数据库压力,因为计算已经在数据库完成。
4. 数据库课程设计避坑实录:死锁、乱码、外键约束和连接耗尽
4.1 运行半小时后界面假死:Too many connections
现象:连续操作一段时间后,界面上任何按钮点下去都卡住,控制台或日志里出现 Connection is not available, request timed out after 3000ms,MySQL 端则报 Too many connections。
原因:最常见的是每个方法里 new 了一个 Connection 但没 close,异常路径上连接直接泄漏,池里的连接被借光,后续请求全部超时。另一种是连接池最大连接数配得过大,多个客户端加起来超过 MySQL 的 max_connections 默认值 151。
解决:先看 MySQL 当前连接数确认问题根源,执行 SHOW PROCESSLIST,重点看 Sleep 状态连接的数量。然后检查代码里所有 getConnection 是否都在 try-with-resources 里,用连接池后禁止手动 new Connection。连接池参数按 3.1 节的方法收敛到 10 以内,程序端限制住之后,MySQL 端不会被打满。
4.2 中文存进去全是问号
现象:界面里输入的“张三”,用 Navicat 打开表看到的是“???”,英文和数字正常。
原因:字符集在三个地方不一致,通常是 JDBC URL 没带 characterEncoding,或者建库建表时用了默认 latin1,或者控制台与界面的输入流编码不是 UTF-8。任何一层不对,中文在写入时就已经被转成了问号,改界面代码是救不回来的。
解决:先查看 MySQL 实际生效的字符集,执行 SHOW VARIABLES LIKE 'character_set%'。然后按三处统一:建库建表用 utf8mb4,JDBC URL 带 useUnicode=true 和 characterEncoding=utf8mb4,紧急场景可以执行 SET NAMES utf8mb4 让当前会话立即生效。原则是数据库、连接、客户端三层全用 utf8mb4,只改一处没用。
4.3 删除企业报外键约束错误
现象:执行 DELETE FROM t_company WHERE company_id=2,MySQL 报 Cannot delete or update a parent row: a foreign key constraint fails,指到 fk_position_company。
原因:t_position 里有职位还在引用这个企业,外键约束不允许直接删主表行,这是 InnoDB 外键的默认行为,不是错误配置。
解决:业务上有两条路。如果确实要物理删除,必须在一个事务里先删子表再删主表,手动控制提交或回滚。更推荐的是走软删除,把 t_company.status 置为 0,职位也在界面上隐藏,既保留历史应聘数据,也避免级联删除带来的误删风险。答辩时如果老师问能不能用 ON DELETE CASCADE,我的回答是能但不敢用:删一个企业会连带删掉所有职位和应聘记录,课程演示看不出问题,真实业务里这种操作没有后悔药。
Connection conn = DbPool.getConnection(); try { conn.setAutoCommit(false); // 先删子表,再删主表,顺序不能反 try (PreparedStatement ps = conn.prepareStatement( "DELETE FROM t_application WHERE position_id IN " + "(SELECT position_id FROM t_position WHERE company_id=?)")) { ps.setInt(1, companyId); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement( "DELETE FROM t_position WHERE company_id=?")) { ps.setInt(1, companyId); ps.executeUpdate(); } try (PreparedStatement ps = conn.prepareStatement( "DELETE FROM t_company WHERE company_id=?")) { ps.setInt(1, companyId); ps.executeUpdate(); } conn.commit(); // 全部成功才提交 } catch (SQLException e) { conn.rollback(); throw new RuntimeException("删除企业失败,已回滚", e); } finally { conn.setAutoCommit(true); conn.close(); }这段事务代码有两点必须讲:setAutoCommit(false) 之后所有语句共享一个事务,任何一步失败都 rollback,数据库回到删除前的状态;finally 里把自动提交恢复并关闭连接,归还给连接池,否则事务没结束会把连接长时间占住,极易引发下一节的死锁。
4.4 并发窗口操作报死锁:数据库并发锁的典型翻车
现象:开两个管理员窗口,一个在维护企业资料,一个在处理应聘结果,操作到一半某个窗口报 Deadlock found when trying to get lock; try restarting transaction。
原因:两个事务以不同的顺序去锁同一批行。事务 A 先锁 t_company 再锁 t_application,事务 B 先锁 t_application 再锁 t_company,两边都在等对方释放锁,InnoDB 检测到循环等待后主动让其中一个事务回滚。另一个常见原因是两个事务同时 UPDATE 同一行,一个还没 commit,另一个等锁等到锁等待超时。
解决:核心是让所有事务固定加锁顺序,都按“先职位表、后应聘记录表”的顺序访问,避免循环等待。事务体尽量短,查询和业务计算放在事务外,事务里只放必要的增删改。出问题后用 SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 段,里面会明确列出两个事务各持有什么锁、在等什么锁,按这个日志调整代码里的访问顺序即可,不用靠猜。
4.5 软删除数据混在报表里:status 过滤条件不统一
现象:职位已经下线的企业,热门职位统计里还在;status=0 的求职者出现在学历分布报表里,数量还是错的。
原因:列表页查询带了 status=1,但统计 SQL 是后写的,漏掉了同一个过滤条件,造成“删除没生效”的错觉。另一个隐蔽版本是 LEFT JOIN 的右表过滤条件放错了位置,比如应聘记录表的 result 条件写进 WHERE,把投递数为 0 的职位全过滤掉,统计结果比实际少。
解决:把 status=1 这类公共过滤条件当成每个查询的默认开头来写,新增统计 SQL 时先复制列表页的 WHERE 条件再扩展。LEFT JOIN 的过滤规则记一条死规矩:过滤右表条件放 ON,过滤主表条件放 WHERE。错误写法是 LEFT JOIN t_application a ON p.position_id = a.position_id WHERE a.result = 0,这会先把左连接结果过滤掉所有没有应聘记录的职位,正确写法是把 a.result 的条件挪进 ON 的括号里。这属于那种报错不明显、数据悄悄错的坑,最花时间,设计报告里写进去反而能成为亮点。
5. 答辩前补两课:用 EXPLAIN 证明查询优化,用 mysqldump 做恢复演练
5.1 用 EXPLAIN 看执行计划:怎么跟老师说查询走对了索引
课程设计的数据库优化,不需要调什么高端参数,最值的动作是给统计查询做一次 EXPLAIN,用执行计划证明索引设计有效。在查询前加上 EXPLAIN 关键字,MySQL 会返回一行表格,不用真的执行查询,重点看三个字段:type 表示访问类型,key 表示实际用的索引,rows 表示预估扫描的行数。
EXPLAIN SELECT p.position_name, c.company_name, COUNT(a.application_id) AS apply_count FROM t_position p JOIN t_company c ON p.company_id = c.company_id LEFT JOIN t_application a ON p.position_id = a.position_id WHERE p.status = 1 GROUP BY p.position_id, p.position_name, c.company_name ORDER BY apply_count DESC LIMIT 5;我讲执行计划时的固定话术是:连接列 company_id 和 position_id 都是外键,InnoDB 自动建了索引,所以 c 和 a 两张被驱动表的 type 都是 ref 级别,不是全表扫描的 ALL,rows 列显示扫描行数远小于表总行数;p 作为驱动表要过滤 status,可能是 ALL,但演示数据量在几百到几千行时完全可接受。只要被驱动表里没出现 ALL,这个查询就够快。如果看到 ALL,就回头检查关联列有没有索引,这是数据库优化最直接的入口。
5.2 mysqldump 备份与恢复:现场演示不怕断电
备份恢复是课程设计报告里要求写、但很多人从来不真的跑一遍的环节。答辩前花十分钟把备份和恢复完整走一次,老师问“数据丢了怎么办”时,你可以当场演示而不是背概念。
# 备份整个库,InnoDB 下加 --single-transaction 避免锁表 mysqldump -uroot -p --single-transaction \ --default-character-set=utf8mb4 job_intro_db > job_intro_db_backup.sql # 验证备份文件是文本,直接看建表语句和 INSERT 数据 head -50 job_intro_db_backup.sql # 恢复演练:新建一个测试库,把备份导进去 mysql -uroot -p -e "CREATE DATABASE job_restore_test DEFAULT CHARACTER SET utf8mb4;" mysql -uroot -p job_restore_test < job_intro_db_backup.sql--single-transaction 的作用是让备份基于 InnoDB 的一致性快照,备份期间其他连接还能正常读写,不会把业务锁死;如果表是 MyISAM,这个参数不生效,会退化回锁表备份,所以建表时统一 InnoDB 在这里又占了一次便宜。恢复后做两件验证:对比两库行数,执行 SELECT COUNT(*) FROM job_intro_db.t_job_seeker 和 job_restore_test.t_job_seeker,结果一致说明备份完整;然后随机查一条刚补录的数据,确认它也在恢复库里。这套动作在答辩现场演示完,比口头说“我做了备份”有说服力得多。
我自己的习惯是把建表脚本和备份脚本都放进项目根目录的 sql/ 文件夹,每改一次表结构就重新导出一次备份,别等到检查前一天才发现备份文件是两周前的旧数据。数据库课程设计这东西,大部分翻车都发生在细节上,表结构立稳、连接池配好、统计查询讲得清、备份真跑一遍,这个题目基本就到优秀档了。希望帮到你。
本文还有配套的精品资源,点击获取