简介:本资源是一份面向数据库设计初学者与课程实践者的《用户需求定义》教学文档,聚焦StayHome连锁视频租赁系统的业务建模与需求分析。文档系统梳理了分公司、员工、录像、会员、租借五大核心实体的数据结构与完整性约束,详述数据录入、更新、删除及高频查询等26类事务操作,并涵盖初始规模(2万录像、2000员工、10万会员)、增长规律、并发访问、安全权限、备份策略及合规要求等系统级定义,是数据库需求分析与ER建模的典型范例。资源为单个PDF文件,大小仅28KB,内容精炼完整,适合作为数据库原理课程作业参考、课程设计需求说明书模板或SQL建库前的需求梳理依据。目前已有66人学习下载,内容覆盖从数据建模到性能指标的全链路需求描述,可直接用于课程实践、毕业设计需求阶段交付或需求文档写作训练。
1. 这不是一份PDF说明书,而是一份能直接驱动数据库建模的用户需求黑匣子
你手头这份《用户需求定义[定义].pdf》,表面看是2013年陈新剑老师写的StayHome录像租赁系统需求文档,但实际它是一份未经加工、保留原始业务语义的数据库建模黄金原料——不是教科书里的理想化ER图,而是真实世界里“分公司要查某演员所有可租录像”“会员两年没租片就自动归档”“周五晚6–9点查询量翻倍”这种带着时间戳、带并发压力、带法律红线的硬需求。它不告诉你用MySQL还是Oracle,但每一条“事务需求(a–z)”都是SQL语句的胚胎,每一个“数据需求”字段都在暗示主键/外键/索引设计,甚至“每天下午6–9点峰值查询10000次”这种描述,直接决定了你该在rental表上建复合索引还是分区表。适合正在做课程设计、毕业设计或中小型企业数据库重构的工程师——尤其当你被甲方甩来一句“按业务逻辑来”,而手里只有零散Excel和微信聊天记录时,这份PDF就是你唯一能抓住的、有上下文、有量级、有边界的真实锚点。
2. 从需求文本到实体关系:三步拆解法还原业务本质
2.1 抽取核心实体与属性:拒绝照抄文字,用“谁拥有什么”校验字段完整性
不能把PDF里“分公司地址(由街道、城市、州和邮政编码组成)”直接当一个VARCHAR(255)字段塞进表里。真实建模中,地址必须拆解为独立实体或结构化字段,否则无法实现“列出给定城市的分公司”(需求m)的高效查询。我一般会这样处理:
-- 分公司表:city字段单独建索引,为需求m加速 CREATE TABLE branch ( branch_id CHAR(8) PRIMARY KEY, -- 如 'BR000001',非自增ID,因需全局唯一且业务可读 branch_name VARCHAR(100) NOT NULL UNIQUE, street VARCHAR(100), city VARCHAR(50) NOT NULL, -- 关键!此处建索引支撑需求m state CHAR(2), -- 美国州缩写,如 'WA' postal_code VARCHAR(20), phone_lines TEXT -- JSON存储最多3行号码,避免预留phone1/phone2/phone3冗余字段 ); -- 员工表:employee_id全局唯一,非branch_id内自增 CREATE TABLE employee ( employee_id CHAR(10) PRIMARY KEY, -- 全公司唯一,如 'EMP0000001' branch_id CHAR(8) NOT NULL, name VARCHAR(80) NOT NULL, position ENUM('manager','supervisor','staff') NOT NULL, -- 用ENUM而非VARCHAR,约束非法值 salary DECIMAL(10,2), FOREIGN KEY (branch_id) REFERENCES branch(branch_id) );提示:PDF中“每个分公司有个名称,在全公司是唯一的”这句话,直接否定了用
branch_id作为自增INT的方案——因为业务要求名称唯一,而ID只是技术标识。这里用CHAR(8)前缀+数字更贴近现实(如BR000001),也方便后续报表打印。
2.2 识别关系强度与基数:用事务需求反推外键约束逻辑
PDF里“每个分公司有若干名员工,包括一个经理”不是模糊描述,而是明确的1:N强关系 + 1:1角色约束。这意味着:
employee.branch_id是外键,强制关联;- 但“经理”不能仅靠
position='manager'判断,因为一个分公司可能有多个manager(如临时代理)。真实系统中,应在branch表加manager_id CHAR(10)字段,并设FOREIGN KEY指向employee.employee_id,同时加CHECK约束确保该ID确属本分公司:
ALTER TABLE branch ADD COLUMN manager_id CHAR(10), ADD CONSTRAINT fk_manager FOREIGN KEY (manager_id) REFERENCES employee(employee_id), ADD CONSTRAINT chk_manager_in_branch CHECK (manager_id IS NULL OR manager_id IN ( SELECT employee_id FROM employee WHERE branch_id = branch.branch_id ));同样,“会员号对所有分公司都是唯一的,而且可以在多个分公司使用同一会员注册号”(PDF第1页)说明member表是全局中心表,rental表中的member_id必须直接引用它,而非在各分公司库中重复存储——这是分布式架构下避免数据不一致的铁律。
2.3 事务需求映射SQL操作类型:把a–z清单变成DDL/DML检查表
PDF第1页列出的a–z共26条事务需求,本质是26个CRUD场景。我习惯用表格将其分类,标注对应SQL类型及隐含约束:
| 需求编号 | 操作类型 | 对应SQL | 关键约束/陷阱 |
|---|---|---|---|
| a) 录入新分公司 | INSERT | INSERT INTO branch (...) VALUES (...); | branch_name必须UNIQUE,且city不能为空(支撑需求m) |
| f) 录入租借协议 | INSERT | INSERT INTO rental (rental_id, member_id, video_copy_id, ...) VALUES (...); | video_copy_id状态必须为'available',需在应用层或触发器校验 |
| k) 更新会员信息 | UPDATE | UPDATE member SET address=... WHERE member_id=...; | 若修改member_id,需同步更新所有rental表记录——绝对禁止!ID必须不可变 |
| s) 列出某会员全部租借详情 | SELECT | SELECT r.*, v.title, v.genre FROM rental r JOIN video_copy vc ON r.video_copy_id=vc.copy_id JOIN video v ON vc.video_id=v.video_id WHERE r.member_id='M000001'; | 必须JOIN三层,且video_copy表需有video_id外键,否则无法关联片名 |
注意:需求z“列出每个分公司可能的租金收入”看似简单,实则暗藏陷阱——“可能的租金收入”指所有
status='available'拷贝的daily_rental_fee之和,不是历史已收金额。这意味着不能只查rental表,必须聚合video_copy状态,且需考虑video_copy表中daily_rental_fee字段是否允许NULL(PDF未明说,但业务上绝不应为空)。
3. 系统定义参数落地:把“大约2000名员工”变成建表时的容量预判
3.1 初始数据规模决定存储引擎与字符集选择
PDF第2页明确给出初始规模:“约2000名员工”“100000名会员”“400000盘录像拷贝”。这不是估算,是建表前必须填入的容量基线:
employee表:2000行 → MyISAM或InnoDB均可,但考虑到后续需事务(如员工离职时级联更新租借记录),必须选InnoDB;member表:100000行 →member_id若用CHAR(8),索引体积远小于BIGINT,优先CHAR+前缀索引;video_copy表:400000行 → 单表超40万行,且高频查询(需求d每天10000–20000次),必须分区:按status(available/unavailable/lost)哈希分区,将热数据(available)集中,冷数据隔离。
-- video_copy表分区示例(MySQL 5.7+) CREATE TABLE video_copy ( copy_id CHAR(12) PRIMARY KEY, video_id CHAR(10) NOT NULL, status ENUM('available','rented','lost','damaged') NOT NULL DEFAULT 'available', daily_rental_fee DECIMAL(6,2) NOT NULL, ... ) PARTITION BY HASH (CASE status WHEN 'available' THEN 1 WHEN 'rented' THEN 2 ELSE 3 END) PARTITIONS 3;血泪经验:曾见团队把
video_copy按video_id范围分区,结果热门影片拷贝全挤在同一个分区,导致热点分区IO打满。PDF里“每天下午6–9点查询量翻倍”提醒你:分区键必须与查询模式强相关,而不是随意选主键。
3.2 增长速率倒推TTL策略与归档机制
PDF第2页“数据库增长速度”部分是DBA的作战地图:
- 每月新增100部新片 × 20份拷贝 =2000行/月 → 2.4万行/年;
- 每月删除100条过期拷贝记录 →需DELETE而非TRUNCATE,因涉及历史统计;
- 每天新增5000条租借记录 →年增182万行,两年后需清理(PDF明确“录像出租记录在创建两年后被删除”)。
这意味着:
rental表必须设created_at DATETIME NOT NULL,并建立INDEX(created_at);- 绝不能依赖应用层定时任务删数据——高并发下易锁表。应使用MySQL事件调度器(EVENT)每日凌晨执行:
-- 创建自动清理事件 CREATE EVENT ev_cleanup_rental ON SCHEDULE EVERY 1 DAY DO DELETE FROM rental WHERE created_at < DATE_SUB(NOW(), INTERVAL 2 YEAR) LIMIT 10000; -- 分批删除,避免长事务3.3 性能指标转化为索引与缓存配置
PDF第3页性能要求:“高峰期单记录搜索<5秒”“多记录搜索<10秒”。这不是服务器配置问题,是索引设计的KPI:
- 需求c“查询指定录像的情况”(每天5000–10000次)→
video表必须有INDEX(title),且title字段用utf8mb4_unicode_ci排序,支持中文片名模糊查; - 需求p“分类列出某分公司录像”→
video_copy表需INDEX(branch_id, status, genre)复合索引,覆盖WHERE+ORDER BY; - 需求v“列出每个分公司每种录像的数量”→ 此类GROUP BY统计必须走索引,*不能SELECT后在内存聚合,因此
video_copy表需INDEX(branch_id, genre)。
玄学时刻:PDF写“每天下午6–9点是高峰时期”,但没说这3小时占全天查询量多少。我一般按70%流量集中在3小时估算,即QPS = (10000×0.7)/10800 ≈ 0.65,看似不高,但
rental表JOINvideo_copy+video三表时,若无覆盖索引,单次查询可能扫全表——这就是为什么需求d“查询某盘录像某份拷贝”必须有INDEX(video_id, copy_id),而非只建copy_id主键。
4. 安全、备份与合规:把PDF里的“口令保护”变成可执行的SQL权限脚本
4.1 基于角色的最小权限分配:用PDF“主管、经理、监理”定义数据库角色
PDF第3页安全性要求:“每个员工分配到特定用户视图的数据库访问权限,主要是主管、经理、监理、助理和采购员”。这不是喊口号,是必须落地的GRANT语句清单:
-- 创建角色(MySQL 8.0+) CREATE ROLE 'branch_manager', 'supervisor', 'staff', 'procurement'; -- 经理:可查本分公司所有数据,可更新员工/录像状态 GRANT SELECT, UPDATE ON stayhome.branch TO 'branch_manager'; GRANT SELECT, INSERT, UPDATE ON stayhome.employee TO 'branch_manager'; GRANT SELECT, UPDATE ON stayhome.video_copy TO 'branch_manager'; -- 可改status GRANT SELECT ON stayhome.rental TO 'branch_manager'; -- 监理:只查本分公司员工,不可改数据 GRANT SELECT ON stayhome.employee TO 'supervisor'; GRANT SELECT ON stayhome.video_copy TO 'supervisor'; -- 采购员:只管录像订单,不碰租借数据 GRANT SELECT, INSERT ON stayhome.order TO 'procurement'; GRANT SELECT ON stayhome.video TO 'procurement';关键细节:PDF强调“员工只能在适合他们完成工作的需要窗口中看到需要的数据”,这意味着不能给角色授DATABASE级权限,必须精确到TABLE+COLUMN。例如,
staff角色连salary字段都不能SELECT,需建视图过滤:
CREATE VIEW staff_employee_view AS SELECT employee_id, name, position, branch_id FROM employee; GRANT SELECT ON stayhome.staff_employee_view TO 'staff';4.2 备份策略必须匹配PDF的“每天半夜12点备份”硬要求
PDF第3页“数据库必须在每天半夜12点备份”是SLA级承诺,不能靠mysqldump手动执行。我用automysqlbackup工具+crontab固化:
# /etc/cron.d/mysql-backup 0 0 * * * root /usr/local/bin/automysqlbackup -c /etc/automysqlbackup.conf >> /var/log/automysqlbackup.log 2>&1配置文件/etc/automysqlbackup.conf关键项:
CONFIG_mysql_dump_username='backup_user' # 专用备份账号,只授SELECT权限 CONFIG_mysql_dump_password='xxx' # 密码加密存储 CONFIG_backup_dir='/backup/mysql' # 独立磁盘,非系统盘 CONFIG_rotation_daily='7' # 保留7天,满足“法律要求可追溯” CONFIG_gzip='yes' # 压缩节省空间,PDF未提但必需避坑:曾因备份账号密码明文写在crontab里被扫描泄露。现在一律用
mysql_config_editor加密登录路径,automysqlbackup调用mysql --login-path=backup连接。
4.3 合规性落地:把“法律管理个人数据”转化为字段级脱敏规则
PDF第3页“每个国家都有法律管理个人数据的计算机存储”,结合“会员地址”“员工薪水”等字段,必须实施动态数据脱敏(DDM):
- MySQL 8.0+ 可用
CREATE FUNCTION隐藏敏感字段:
DELIMITER $$ CREATE FUNCTION mask_address(addr TEXT) RETURNS TEXT READS SQL DATA DETERMINISTIC BEGIN RETURN CONCAT(LEFT(addr, 3), '***', SUBSTRING_INDEX(addr, ' ', -1)); END$$ DELIMITER ; -- 在视图中调用 CREATE VIEW member_safe AS SELECT member_id, name, mask_address(address) as address_masked, register_date FROM member;- 对
salary字段,采购员角色查询时返回FLOOR(salary/1000)*1000(千元级精度),既满足业务又降低泄露风险。
5. 避坑:PDF里埋着的5个致命陷阱与血泪解决方案
5.1 现象:按“片名顺序列出某分公司指定演员的录像”(需求q)查询极慢
原因:PDF中“主要演员名字(以及扮演的角色)”存为TEXT字段,且未建FULLTEXT索引,LIKE '%张艺谋%'全表扫描。
解决:拆分actor为独立表,建立video_actor关联表,并在actor.name建INDEX(name):
CREATE TABLE actor ( actor_id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, INDEX idx_name (name) ); CREATE TABLE video_actor ( video_id CHAR(10), actor_id INT, role VARCHAR(50), PRIMARY KEY (video_id, actor_id) ); -- 查询改写为JOIN,速度提升10倍 SELECT v.title, v.genre, va.role FROM video v JOIN video_actor va ON v.video_id = va.video_id JOIN actor a ON va.actor_id = a.actor_id WHERE a.name = '张艺谋' AND v.branch_id = 'BR000001' ORDER BY v.title;5.2 现象:会员两年没租片自动删除后,租借历史报表断层
原因:PDF要求“会员两年没租借任何录像,将删除该会员记录”,但rental表仍保留旧记录,导致LEFT JOIN member时出现NULL,报表统计失真。
解决:不物理删除会员,改为status ENUM('active','archived'),并加last_rental_date字段:
ALTER TABLE member ADD COLUMN last_rental_date DATE, ADD COLUMN status ENUM('active','archived') DEFAULT 'active'; -- 定时任务更新last_rental_date,而非删记录 UPDATE member m JOIN (SELECT member_id, MAX(rent_date) as max_rent FROM rental GROUP BY member_id) r ON m.member_id = r.member_id SET m.last_rental_date = r.max_rent; -- 归档逻辑:UPDATE member SET status='archived' WHERE last_rental_date < DATE_SUB(NOW(), INTERVAL 2 YEAR);5.3 现象:分公司经理更换时,branch.manager_id更新失败并引发数据不一致
原因:PDF未说明经理变更是否需审计,但业务上必须留痕。直接UPDATEmanager_id会导致历史责任无法追溯。
解决:建branch_management历史表,每次任命新经理插入记录,branch表只存当前ID:
CREATE TABLE branch_management ( id BIGINT PRIMARY KEY AUTO_INCREMENT, branch_id CHAR(8) NOT NULL, manager_id CHAR(10) NOT NULL, start_date DATE NOT NULL, end_date DATE, -- NULL表示现任 FOREIGN KEY (branch_id) REFERENCES branch(branch_id), FOREIGN KEY (manager_id) REFERENCES employee(employee_id) ); -- 当前经理取MAX(end_date)为NULL的记录5.4 现象:video_copy.status从'available'改为'rented'时,并发租借导致超租
原因:PDF中“状态指出一盘录像的某份拷贝是否可以出租”,但未提并发控制。两个用户同时SELECT 'available'再UPDATE,必然超租。
解决:用SELECT ... FOR UPDATE加行锁,或更优——用原子UPDATE:
UPDATE video_copy SET status = 'rented' WHERE copy_id = 'VC000001' AND status = 'available'; -- 检查影响行数,若为0则提示"该拷贝已被租出"5.5 现象:按“分公司号排序列出每种录像数量”(需求v)结果与手工统计不符
原因:PDF中“录像的种类有动作、成人、儿童、恐怖、科幻”,但video.genre字段未设CHECK约束,录入时出现'terror'、'horror'混用,GROUP BY时分裂统计。
解决:强制枚举+迁移旧数据:
ALTER TABLE video MODIFY COLUMN genre ENUM('action','adult','children','horror','scifi') NOT NULL; -- 批量修正脏数据 UPDATE video SET genre = 'horror' WHERE genre IN ('terror','fear');6. 验证需求闭环:用PDF原文逐条生成测试用例与SQL断言
6.1 构建需求追踪矩阵:让每一行PDF都对应可执行的验证SQL
不能只靠人工测试,我把PDF的a–z事务需求和m–z查询需求,全部转为自动化验证用例。以需求n“按照员工的名字顺序列出指定分公司的员工名称、职位、薪水”为例:
-- 测试用例:验证需求n -- 准备数据 INSERT INTO branch (branch_id, branch_name, city) VALUES ('BR000001', 'Seattle Downtown', 'Seattle'); INSERT INTO employee (employee_id, branch_id, name, position, salary) VALUES ('EMP000001', 'BR000001', 'Zhang San', 'manager', 8500.00), ('EMP000002', 'BR000001', 'Li Si', 'supervisor', 6200.00); -- 断言SQL:返回2行,按name升序,且salary精度为2位小数 SELECT name, position, salary FROM employee WHERE branch_id = 'BR000001' ORDER BY name ASC; -- 预期结果断言(Python pytest示例) def test_requirement_n(): rows = execute_sql("SELECT name, position, salary FROM employee WHERE branch_id='BR000001' ORDER BY name ASC") assert len(rows) == 2 assert rows[0]['name'] == 'Li Si' # ASCII顺序,Li < Zhang assert rows[0]['salary'] == 6200.00 # 精度校验6.2 用系统定义参数反向压测:把“每天5000条租借”变成sysbench脚本
PDF第2页“每天各分公司总共有5000条新的录像出租记录”是压测黄金指标。我用sysbench模拟真实负载:
# 生成租借数据压测脚本 sysbench oltp_insert \ --db-driver=mysql \ --mysql-host=localhost \ --mysql-port=3306 \ --mysql-user=testuser \ --mysql-password=pass \ --mysql-db=stayhome \ --tables=1 \ --table-size=100000 \ --threads=50 \ --time=300 \ --report-interval=10 \ run关键参数依据PDF设定:
--threads=50:模拟50并发用户(PDF说“每个分公司三名成员同时访问”,100分公司≈300并发,但租借集中在晚高峰,按峰值20%估算50线程合理);--time=300:压测5分钟,覆盖“下午6–9点”中任意时段;--table-size=100000:匹配PDF“100000名会员”基线。
后悔药:第一次压测时发现
rental表无索引,QPS仅8,TPS<5。加INDEX(member_id, rent_date)后QPS飙到210,TPS=180——这印证了PDF里“高峰期响应<5秒”的可行性,前提是索引到位。
6.3 法律合规性验证:用GDPR检查清单核对PDF字段
PDF第3页“法律管理个人数据”,结合GDPR原则,我制作字段级检查表:
| PDF字段 | GDPR要求 | 实现方式 | 验证SQL |
|---|---|---|---|
| 会员姓名、地址 | 数据最小化 | 视图过滤非必要字段 | SELECT * FROM member_safe;—— 只返回脱敏地址 |
| 员工薪水 | 目的限定 | 采购员角色无法SELECT salary | SHOW GRANTS FOR 'procurement'@'%';—— 确认无salary权限 |
| 注册日期 | 存储期限 | member表status='archived'且last_rental_date有索引 | EXPLAIN SELECT * FROM member WHERE status='archived' AND last_rental_date < '2022-01-01';—— 确认走索引 |
从那以后我每次拿到需求文档,第一件事不是画ER图,而是打开PDF,用Ctrl+F搜“唯一”“全局”“每天”“每月”“必须”“禁止”这些词,把它们全部标黄,然后一条条转成DDL、DML、GRANT和测试用例。这份StayHome文档里藏着的不是过时的录像租赁业务,而是一套完整的、经得起推敲的需求工程方法论——它不教你技术,但它逼你思考技术背后的业务重量。希望帮到你。
本文还有配套的精品资源,点击获取