招聘系统数据库设计:核心表拆分、索引优化与高并发归档方案 简介面向网络招聘场景的数据库设计文档完整覆盖从系统概述、需求分析、系统功能拆解到数据库实体关系、索引设计、安全策略及性能优化的全流程可支撑职位发布、简历管理、应聘流程跟踪等核心功能。资源包内仅含一个doc文档整体约355KB适合直接阅读或作为课程设计报告写作框架参考。目前已有八十二人浏览学习对数据库课程设计或网络招聘系统开发者仍有参考价值。文档结合苏州大学数据库课程设计实际项目详细说明职位实体、求职者实体与申请记录的关系设计对比关系型数据库与非关系型数据库方案并给出缓存、数据库分区和负载均衡等优化手段可帮助读者快速理清设计思路、补充报告章节。1. 网络招聘的数据库设计为什么说它是典型的“表少坑多”如果你接手过一个招聘平台的后端大概率遇到过这类场景求职者投递一份简历企业端隔了十分钟才看到用户简历明明改过了HR 那边看到的还是三个月前的版本校招季投递量一上来投递接口直接超时。这些问题十有八九不是代码逻辑的问题而是网络招聘数据库设计在源头上埋了雷。招聘系统的表数量通常不多三四十张就够用但它有“基础资料表”和“业务流水表”的天然分界又有校招投递这类典型的短时热点设计得糙一点运行半年就开始四处冒烟。这篇方案我把核心实体怎么拆、简历和投递这类热点表怎么做字段与索引、以及数据量上来之后怎么归档分表讲透适合正在做课设、毕设或者要给招聘模块出后端设计文档的人照着改。2. 先拆实体再建表网络招聘库的业务边界与用户信息表设计2.1 梳理业务主线再谈字段数据模型要背靠业务链路很多数据库设计文档上来就画 ER 图但招聘系统里最容易犯的错是忽略了“这个表到底服务于哪条业务链路”。网络招聘的核心链路其实只有两条求职者端是“注册→完善简历→搜索职位→投递→收到面试/offer”企业端是“注册企业→发布职位→筛选简历→约面→发 offer”。这两条链路交叉点就两个职位表和投递记录表。其他像收藏、浏览足迹、订阅推送都是辅助功能不要在核心表上过度设计。从这两条链路倒推实体核心表只需要这些用户、求职者资料、企业、企业成员HR、职位、简历、投递记录、面试、offer。注意这里没有“订单”“支付”这类电商实体招聘系统里弱事务链路多但强状态流转多——一个投递记录要经历“已投递→被查看→邀面试→不合适/通过”这种状态流转必须用状态字段加时间戳来建模而不是用多张表去拼。我一般建议把表分成两类看待一类是“资料表”记录用户、企业、职位的静态信息更新频率低重在完整另一类是“流水表”记录投递、浏览、状态变更只增不改或改状态重在追加和查询性能。这个区分贯穿整个设计后面简历快照、归档分表都靠这个分类做决策。资料表别想去省字段流水表别想去加太多冗余列各司其职。2.2 用户与角色的两种做法统一账号表加拆分配档用户角色是招聘系统设计的第一个分叉路口。常见做法有两种一种是一张 user 表通过 role 字段区分求职者和 HR另一种是 user 表只存账号求职者资料和 HR 的企业关联分别拆成独立表。我明确推荐第二种原因有两个一是求职者资料字段和企业 HR 字段几乎没有交集硬塞一张表里会产生大量 NULL二是权限模型需要“HR 属于某企业”这个关联如果 role 字段只存一个角色就无法表达同一个人在不同企业当 HR 的情况。用户信息表账号表的核心字段这样定CREATE TABLE t_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, mobile VARCHAR(20) NOT NULL COMMENT 登录手机号留足国际区号位, pass_hash VARCHAR(64) NOT NULL COMMENT BCrypt/Argon2 哈希串禁止明文, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 2冻结 3注销, register_ip VARCHAR(45) NOT NULL DEFAULT COMMENT 兼容 IPv6 长度, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_mobile (mobile) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci;这里两个细节容易被新手忽略。手机号一定要用 VARCHAR(20) 而不是 BIGINT否则未来要支持国际号段、带前导 0 的号码时直接翻车register_ip 定成 VARCHAR(45) 是为了让 IPv6 地址也能完整落库如果只按 IPv4 的长度写 VARCHAR(15)后面审计时只能看到截断的脏数据。注册时间这类审计字段建议每张表都带上尤其是流水表做数据归档时直接按 created_at 切分。求职者资料表和 HR 关联表再单拆CREATE TABLE t_seeker_profile ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL COMMENT 关联 t_user.id, real_name VARCHAR(50) NOT NULL DEFAULT , gender TINYINT NOT NULL DEFAULT 0 COMMENT 0未知 1男 2女, expect_city VARCHAR(64) NOT NULL DEFAULT COMMENT 期望城市编码存编码便于等值检索, expect_salary_min INT NOT NULL DEFAULT 0 COMMENT 月薪下限单位元, expect_salary_max INT NOT NULL DEFAULT 0 COMMENT 月薪上限单位元, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_company_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, company_id BIGINT UNSIGNED NOT NULL, role TINYINT NOT NULL DEFAULT 2 COMMENT 1超级管理员 2HR 3面试官, UNIQUE KEY uk_user_company (user_id, company_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;期望薪资不要存成字符串“10k-15k”拆成两个 INT 字段才能做薪资区间检索和排序。t_company_user 的唯一键放在 (user_id, company_id) 上顺便就解决了“同一个人不可能在同一家公司挂两次身份”的约束。性别、学历这类枚举值用 TINYINT 存编码前端展示做映射不要直接存中文——否则以后改选项时需要 UPDATE 全表数据成本极高。2.3 企业、职位与 HR 的关系权限模型就是三张表的事企业相关信息同样拆开设计t_company 存企业营业执照、规模、融资阶段等静态资料t_company_user 关联 HRt_job 存职位。这里 HR 与职位的关系不需要额外建表在 t_job 里直接存 company_id 和 creator_id 就行。原因是职位永远归属单个企业不太会出现一个职位由多个企业共用的场景而 HR 的权限判断在登录态里先查出该用户关联的 company_id 列表再在 SQL 里带 company_id 过滤。一个经常被追问的设计点是企业表要不要冗余存“最新发布的职位数”“活跃度”这类统计字段我的建议是像“职位数”这种低频统计可以统计时现算“今日新增投递数”这种放在企业工作台首页的指标单独建一张 t_company_stat 日汇总表每天凌晨跑批更新不要实时 count 流水表。这是招聘系统数据库设计里最常见的性能杀手——企业列表页一打开就 count 全表投递记录库再大也扛不住。字段选型和索引可以在后续章节展开但实体边界这层想清楚表结构才不会东倒西歪。3. 三大热点表简历、职位、投递记录的字段与索引设计3.1 简历表主表加段落块别把简历压成一个 JSON简历是招聘系统里最特殊的表。它的特点是结构不稳定有的人写三份工作经历有的人写五份教育背景、项目经历、技能标签的条数都不同但它的展示需求非常明确——企业端看简历时要按“基本信息→教育经历→工作经历→项目经历”的顺序展示。如果做成一张横表要么预留几十个空字段要么后续改需求时天天 ALTER TABLE。常见做法是拆成主表和段落块表两张CREATE TABLE t_resume ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT UNSIGNED NOT NULL, title VARCHAR(100) NOT NULL DEFAULT COMMENT 简历名称允许用户建多份简历, is_default TINYINT NOT NULL DEFAULT 0 COMMENT 1为默认投递简历, status TINYINT NOT NULL DEFAULT 1 COMMENT 1完整 2草稿 3审核不通过, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_resume_item ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, resume_id BIGINT UNSIGNED NOT NULL, section_type TINYINT NOT NULL COMMENT 1基本信息 2教育经历 3工作经历 4项目经历 5技能标签, content JSON NOT NULL COMMENT 结构化内容由应用层约束字段, sort_no SMALLINT NOT NULL DEFAULT 0 COMMENT 段落排序号, is_valid TINYINT NOT NULL DEFAULT 1 COMMENT 软删除位1有效 0删除, KEY idx_resume (resume_id, sort_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;简历正文存段落、段落内容用 JSON这是目前兼顾灵活性和可维护性的折中。需要在列表页展示的字段期望薪资、城市、姓名抽出来放到 t_seeker_profile 或主表的独立列上方便检索。不要试图对 JSON 字段做 SQL 条件过滤MySQL 的 JSON 函数即使能查也走不了普通索引性能完全不可控——这一点放到后面的避坑章节再说。“允许用户建多份简历”是招聘产品的常见需求所以 t_resume 里特意留了 title 和 is_default 字段。投递时选哪份简历就把对应的 resume_id 传到投递记录里。但注意这里只传 resume_id 是不够的因为用户随时可能修改简历。企业端应该看到的是“投递那一刻的简历内容”而不是用户改完之后的版本。解决方式是简历快照下一节写。3.2 投递记录与简历快照状态机与历史留痕投递记录是整个库的流水核心它承担的责任是“谁在哪一刻投了哪个职位的哪个版本的简历”。字段设计上除了最基本的 seeker_id、job_id还有三个关键设计点。CREATE TABLE t_delivery ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, seeker_id BIGINT UNSIGNED NOT NULL COMMENT 求职者 user_id, job_id BIGINT UNSIGNED NOT NULL, company_id BIGINT UNSIGNED NOT NULL COMMENT 冗余企业id按企业筛选时避免二次join, status TINYINT NOT NULL DEFAULT 1 COMMENT 1已投递 2HR已查看 3邀面试 4不合适 5待沟通 6已撤销, resume_snapshot JSON NOT NULL COMMENT 投递时的简历快照JSON序列化, resume_version INT NOT NULL DEFAULT 1 COMMENT 投递时简历主表自增版本号, source TINYINT NOT NULL DEFAULT 1 COMMENT 1网站 2小程序 3APP, cancel_reason VARCHAR(255) NOT NULL DEFAULT COMMENT 撤销原因状态为6时填写, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_seeker_job (seeker_id, job_id), KEY idx_company_status (company_id, status, created_at), KEY idx_job (job_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第一个关键设计是唯一键 uk_seeker_job。同一个求职者不能重复投递同一个职位这个约束必须放在数据库层面而不是靠应用先查再插——并发下查和插之间会有空窗期重复数据就这么进去的。第二个是 resume_snapshot 字段。投递动作发生时应用层把简历主表和段落内容序列化成一个 JSON存入这条投递记录。这样用户第二天改了简历之前已投递的企业看到的还是投递时刻的内容。简历快照会牺牲一部分存储空间但换来了业务上的合规和体验确定性——在做“企业端职位投递列表”查询时直接读这条 JSON 渲染即可不需要再 join 简历表反而省了查询开销。第三个是公司 id 冗余。投递记录里冗余存 company_id是为了支撑企业端后台常见的“按企业查全部投递”“按企业状态筛选”场景。不冗余的话每次都要通过 job 表去关联 company_id多一次 join在高频筛选页面上代价不小。这类冗余在流水表上是推荐的它属于“数据一致性可以被接受、查询性能显著提升”的典型取舍。投递动作本身用事务包住-- 投递服务里的事务 START TRANSACTION; INSERT INTO t_delivery (seeker_id, job_id, company_id, status, resume_snapshot, resume_version, source) VALUES (12345, 67890, 111, 1, {...}, 5, 1); UPDATE t_job SET delivery_count delivery_count 1 WHERE id 67890; COMMIT;投递计数和投递主记录放在同一事务里保证计数不丢。如果对计数实时性要求没那么高也可以直接用一个定时任务汇总 t_delivery 表但校招期间“职位热度”会被运营拿来实时看所以在线更新计数的方案更稳妥。事务本身很小只插一条、更新一条锁开销可以接受。3.3 职位表高频查询场景下的字段与组合索引职位表是整个系统读压力最大的表求职者在职位列表页的每一次搜索、筛选、排序都打在这张表上。它的字段设计要围绕“列表页展示”和“搜索过滤”来定。CREATE TABLE t_job ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, company_id BIGINT UNSIGNED NOT NULL, company_name VARCHAR(100) NOT NULL DEFAULT COMMENT 冗余企业名列表页免join, title VARCHAR(100) NOT NULL COMMENT 职位名称, category_code INT NOT NULL COMMENT 职位类别编码如1001技术类, city_code INT NOT NULL COMMENT 城市编码用行政区划编码, salary_min INT NOT NULL DEFAULT 0 COMMENT 月薪下限单位元, salary_max INT NOT NULL DEFAULT 0 COMMENT 月薪上限单位元, tags JSON NOT NULL COMMENT 职位标签如[五险一金,弹性工作], description MEDIUMTEXT NOT NULL COMMENT 职位描述正文, status TINYINT NOT NULL DEFAULT 1 COMMENT 1发布中 2下架 3草稿 4审核拒绝, refresh_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 刷新时间支撑最新发布排序, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_city_category_status (city_code, category_code, status, refresh_at), KEY idx_company (company_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 职位列表页的典型查询 SELECT id, company_name, title, salary_min, salary_max FROM t_job WHERE city_code 1001 AND category_code 1002 AND status 1 ORDER BY refresh_at DESC LIMIT 20;查询条件和索引的匹配顺序是city_code 等值category_code 等值status 等值最后才用 refresh_at 做排序。所以组合索引应该把等值列放前面把排序列放最后也就是 idx_city_category_status 这个顺序。如果反过来把 refresh_at 放前面这个查询就走不了索引的最左前缀等于白建。职位表还需要注意两个点。一是 description 这种大字段不要放进列表页查询里查询时显式列出需要的列避免 SELECT * 把 MEDIUMTEXT 也拖出来白费 IO二是公司名冗余存储换来的是列表页不 join但代价是企业改名字时要 UPDATE 这张表。企业改名的频率极低这个代价完全可以接受。如果想让“最新发布”更实时可以定时把新职位和新刷新职位在 Redis 里维护一份有序集合列表页读缓存详情页读 MySQL这是后面缓存章节的事。4. 网络招聘库的五条踩坑记录现象、原因与解决4.1 简历表用一个大 JSON 字段装全部内容检索和后续分析全废现象开发时为了图省事把简历所有字段塞进一个 content JSON投递列表页展示倒是方便但运营要统计“本科学历且三年以上经验的简历数量”时SQL 写不出来只能把 JSON 拉到应用层过滤10 万条简历直接内存爆掉。更糟的是简历结构频繁调整老数据的 JSON 字段对新代码不兼容解析时频繁报错。原因JSON 字段在 MySQL 里本质上还是一个大的 TEXT它没有列的概念传统 B 树索引完全帮不上忙。一旦把核心业务字段压进 JSON等于主动放弃了数据库的查询能力把所有过滤逻辑推给应用层做。解决把需要在列表页展示、筛选、统计的字段上升为独立列例如学历、工作年限、期望城市、期望薪资。简历正文段落继续用 JSON 存但只服务于“详情展示”不承担任何检索职责。如果后续有简历全文搜索需求单独接 ES通过 canal 之类的同步工具把简历表变更同步过去应用层直接查 ES。4.2 投递表没有唯一约束同一岗位被反复投递现象用户快速点击两次“投递”按钮后台出现两条完全相同的投递记录企业端看到同一个候选人投了同一个职位两次运营统计“总投递数”也翻倍数据完全不可信。线上没做防重测试环境因为单线程跑一直没暴露。原因应用层先查再插的逻辑在高并发下失效。两个请求同时查到“未投递”然后同时 insert数据库没有约束就产生了重复数据。这属于典型的“并发查插竞态”。解决给 t_delivery 加 (seeker_id, job_id) 唯一索引数据库层面兜底。插入时用 INSERT ... ON DUPLICATE KEY UPDATE或者先插入后捕获重复键异常返回“请勿重复投递”。唯一索引同时还能加速按用户查投递记录的查询。另外要注意如果业务允许“企业对同一职位重复发布多个 job_id”那么唯一索引拦不住这种情况需要在产品规则上约定或对职位内容做哈希指纹。4.3 时间字段选型混乱约定面试时间变成玄学现象面试时间存了 TIMESTAMP夏令时或者跨时区使用时显示的时间总是差 8 个小时还有状态变更时间用 VARCHAR 存了字符串“2025-3-1 10:30”结果做时间范围筛选时排序和比较全部错乱甚至出现“2025-1-5”排在“2024-12-15”前面这种离谱结果。原因对 MySQL 的日期时间类型理解不到位。TIMESTAMP 类型存储的是 UTC 时间受数据库时区参数影响展示时转换如果没配对就会出现时区错位。而字符串比较是字典序格式稍微不统一就不可能正确排序。解决所有业务时间字段统一用 DATETIME 存储应用层连接串指定时区比如 Asia/Shanghai后端在代码里统一用 UTC 转换。DATETIME 不受时区参数影响存什么就返回什么行为最可控。排序和筛选直接走索引不用担心字符串格式。面试、offer 这类未来时间也可以统一 DATETIME到应用层做时区展示。设计文档里应该明确定下这一条不准用字符串存时间。4.4 职位表的状态字段不带索引后台筛选全表扫描现象企业后台“查看已下架职位”列表接口响应从 100ms 慢慢变成 3 秒数据库 CPU 飙升。打开慢查询日志一看SQL 一直在扫 t_job 全表数据量涨到几十万行后问题开始明显。原因status 2 这种过滤条件区分度很低如果单独给 status 建索引优化器可能还是选全表扫描。但是如果查询还带了 company_id组合索引就能显著缩小扫描范围。常见错误是只给 status 建单列索引然后抱怨“建了索引也没用”。解决按实际查询模式建组合索引。后台按企业查职位就走 idx_company (company_id, status)前台按城市类别筛选就走 idx_city_category_status (city_code, category_code, status, refresh_at)。建完索引后用 EXPLAIN 验证走的是哪个索引、扫描多少行。要记住组合索引的列顺序必须跟查询条件的最左前缀匹配否则索引就是摆设。4.5 投递事务里跑了简历全文匹配锁膨胀直接打崩投递接口现象校招季某天 10 点投递接口突然大量超时数据库监控显示行锁等待超过 5 秒连接数被占满。排查发现投递事务里不仅 Insert 了投递记录还做了简历全文匹配并顺手更新了好几张统计表。原因事务时间被拉长锁持有的时间也跟着变长。投递事务里做简历匹配等于在锁的临界区内做重 IO 操作外面的请求全部排队等锁连接池被占满整个投递入口直接不可用。解决在线事务里只做必要操作——插投递记录、更新职位计数。简历全文匹配、推荐算法、统计报表更新全部拆出去用消息队列异步处理或者定时任务跑批。记住一个原则事务越短越好锁范围越小越好。涉及多张统计表的更新先算好结果再 UPDATE不要在一个事务里做多轮 SELECT 和计算。这条我在实际项目中踩过一次之后所有写接口都强制套这个约束。5. 数据增长与校招洪峰拆分、缓存与归档的落地方式5.1 招聘业务的数据特征读多写少、冷热不均先做容量估算不要等数据库慢到不可用了才考虑分库分表。网络招聘的数据模型有个典型特征读多写少但写入有短时洪峰数据总量增长快但活跃数据占比很低。一个注册用户可能三年前投过简历之后再没登录过那份简历还是占着在线库的空间却几乎不会被读取。这就是冷热不均。做容量规划时可以用一个简单的公式估算假设注册用户 100 万其中 30% 完善了简历即 30 万份简历平均每份简历段落块 10 条那么 t_resume_item 就是 300 万行。投递记录如果一年 500 万条三年就是 1500 万条。这个量级在 MySQL 单表下还能撑但查询开始明显变慢。这时候不应该直接上分库分表而是先做冷热分离和归档。我的建议顺序是先归档冷数据再上缓存最后才考虑分表。大多数招聘系统跑到需要分表阶段的不到两成更多是被冷数据拖垮的。归档方案比分库分表简单一个量级运维成本也低得多。5.2 投递流水按用户分表分表键与跨页查询的取舍如果投递记录表确实到了千万级以上分表是最直接的出路。分表键选 seeker_id这是按用户维度取数据的核心路径。具体做法是把 t_delivery 拆成 64 张表路由规则是 seeker_id % 64-- 分表路由示例在应用层或中间层计算 -- 表名t_delivery_0 ~ t_delivery_63 SELECT * FROM t_delivery_${seeker_id % 64} WHERE seeker_id 12345 ORDER BY created_at DESC LIMIT 20;用 seeker_id 做分表键单个求职者查自己的投递历史永远落在一张表里走唯一索引性能没有问题。但要注意两个代价。第一个代价是企业端按公司查候选人投递列表的查询会变成跨 64 张表的聚合扫描需要并发查所有分表再合并结果实现复杂度上来了。第二个代价是“统计全站投递总量”这种后台报表没法直接 count 单表只能每天跑批汇总到一张统计表里或者引入 ClickHouse 这类 OLAP 存储。所以在分表方案设计时要提前把“按企业维度查投递”的场景单独存一份冗余表用企业 id 作为分表键或者用 ES 承载这个查询。分表解决的是写入和单用户查询的瓶颈代价是牺牲了全局视角的查询这个取舍必须跟业务方说清楚。5.3 缓存层设计职位列表热点与简历状态延迟写职位列表页是招聘系统读压力最大的场景校招季尤其明显。缓存方案的核心思路是列表页读缓存详情页读数据库写操作主动失效缓存。职位列表的缓存键建议按查询维度设计例如 city_code、category_code、page 三个维度组合job:list:{city_code}:{category_code}:{page} TTL 300 秒当职位上下架、刷新时应用层主动删除对应城市和分类下的列表缓存。这里有个细节如果分页到第 20 页每次更新时把 20 页缓存全删掉成本偏高且很多页没人看。一个更实用的方案是只缓存前 5 页热点数据后面的页直接查库因为绝大部分用户投递行为集中在前几页——这是读过线上数据后的直观结论。投递记录的状态更新也可以做延迟写用户点投递时先把状态写入 Redis返回“投递成功”再异步落 MySQL。这样投递接口的响应时间不被数据库写瓶颈拖住校招洪峰时接口能扛住更高的 QPS。但要注意异步落库失败的数据补偿定时扫描 Redis 里的待落库集合超过一定时间没落库的重新投递到 MySQL。状态不一致的窗口期大约几秒对招聘业务来说完全可以接受。5.4 冷数据归档别让历史简历拖垮在线库归档是招聘系统性价比最高的优化手段。操作不复杂把超过 18 个月未登录、未投递、未更新的简历和关联的投递记录迁移到归档库在线库只保留活跃数据。归档库可以用同一套 MySQL 实例也可以放到成本更低的机器上。-- 归档脚本思路先查出符合条件的简历 id再分批迁移 -- 示例每批 1000 条 INSERT INTO archive_db.t_resume (id, user_id, title, is_default, status, created_at, updated_at) SELECT id, user_id, title, is_default, status, created_at, updated_at FROM main_db.t_resume WHERE updated_at DATE_SUB(NOW(), INTERVAL 18 MONTH) AND id NOT IN (SELECT DISTINCT seeker_id FROM t_delivery WHERE created_at DATE_SUB(NOW(), INTERVAL 3 MONTH)) LIMIT 1000;迁移注意三点第一先迁从表简历段落块再迁主表避免主表数据过去但从表没过去第二迁移完成后在在线库执行 DELETE但不要一次删太多按主键范围分批删除避免产生大事务锁第三归档不是删除如果用户重新活跃、登录后要能看到自己的历史简历需要在用户登录时做“归档数据回迁”或者通过一个 view 跨库查询具体取决于产品要求。我之前做过的项目是“归档后用户只能看到摘要编辑需要触发回迁”这样既控制了在线库的数据量又保住了用户体验。6. 从零落地这份方案先动十张核心表再用 EXPLAIN 验收如果你正打算按这份方案给网络招聘系统建库我的建议是不要一次建完所有表。先建十张核心表把主流程跑通再补辅助表。这十张是t_user、t_seeker_profile、t_company、t_company_user、t_job、t_resume、t_resume_item、t_delivery、t_interview、t_offer。面试和 offer 表可以后补因为投递流程不依赖它们。表建完后用 EXPLAIN 验收核心查询。拿出一条职位列表页的真实 SQL 和一条投递记录查询 SQL看执行计划里的 type 是否达到 ref 或 rangekey 是否命中了预期索引。比如核心查询期望命中的索引检查点职位列表筛选idx_city_category_statustype 为 refrows 远小于全表按用户查投递uk_seeker_jobtype 为 const/refExtra 无 Using filesort按企业查投递idx_company_status覆盖 city/status 等值条件另外可以做一轮简单的数据量摸底把简历段落块和投递记录的行数查出来对照上面 5.1 的数据特征判断当前方案是否需要归档或分表。建完表之后拿测试数据写一个模拟投递事务跑一遍观察接口耗时和锁等待情况。这个动作每次数据结构调整后都值得重复一次。我在自己的项目里吃过亏表设计得很全但没人验证过核心查询的索引是否合理结果上线第一周就出现慢查询。后来我养成了一个习惯凡是表结构变动必须顺手跑一遍设计文档里列出的核心查询的 EXPLAIN确认没有全表扫描才合代码。这份网络招聘的数据库设计方案如果你能照着这个顺序落地——实体拆清楚、热点表索引验证过、冷数据归档机制到位校招洪峰来了也不会太慌。希望帮到你。本文还有配套的精品资源点击获取