拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从需求PDF到数据库建库:StayHome实体关系映射与SQL实现

从需求PDF到数据库建库:StayHome实体关系映射与SQL实现

简介:这份《用户需求定义》PDF面向数据库课程学习者与系统设计初学者,以StayHome录像租赁连锁为背景,完整梳理分公司视图的数据需求与事务需求,帮助读者理解从业务描述到数据库设计的转化过程。资源包内含1个PDF文件,约28KB,内容涵盖分公司、员工、录像、会员、租借五类核心数据的字段定义,录入、更新、删除与查询等事务操作,以及初始规模、增长速度、网络共享、性能、安全、备份恢复和法律合规等系统定义要点。文中还给出按城市查分公司、按姓名排序员工、按种类统计录像等具体查询示例,并附有数据量与并发访问的量化指标,可作为需求分析、ER建模与数据库课程设计的参考素材。目前已有66人学习,适合需要撰写需求规格说明书或进行数据库课程实践的学生与开发者借鉴。

1. 从一份 2013 年的需求定义 PDF 说起:StayHome 数据库到底要建什么

如果你手上只有一份《用户需求定义[定义].pdf》,却要把它变成能跑的库,第一道坎不是写 SQL,而是把散落在业务描述里的实体、唯一键和基数关系抠出来。StayHome 这份文档就是典型:它用“数据需求 + 事务需求 + 系统定义”三段式,把一家连锁录像租赁公司的分公司视图讲清楚了。核心实体有六个——分公司、员工、录像、拷贝、会员、租借,每个实体都带唯一标识和业务约束。它适合谁?适合正在做数据库课程设计、需要一份真实需求规格来练手的学生,也适合想复盘“需求到表结构”映射逻辑的初级开发。这份 PDF 不是代码包,但它是建库前必须吃透的输入。

2. 把需求定义拆成实体关系:从文字到表结构的映射方法

2.1 六个核心实体的唯一键与基数关系

需求文档里最值钱的信息不是“有哪些字段”,而是“谁唯一标识谁、谁和谁几对几”。StayHome 的原文写得很密,我把它拆成下面这张映射表,方便你直接对照建表。

实体唯一标识关键属性与其他实体的关系
分公司分公司名称(全公司唯一)地址、电话(最多3行)拥有员工、库存、会员
员工员工号码(全公司唯一)姓名、职务、薪水属于一个分公司,含经理、监理
录像目录号(唯一)片名、种类、日租费、购买价、状态、演员、导演有多个拷贝
拷贝录像号(唯一)状态(可租/不可租)属于一个录像、一个分公司
会员会员号(所有分公司唯一)姓名、地址、注册日期、注册员工可在多分公司使用,最多租10部
租借租借号(全公司唯一)会员号、录像号、日租费、租出/归还日期关联会员与拷贝

这张表里有两个容易翻车的点:第一,会员号“对所有分公司唯一”意味着它是全局主键,不是分公司内自增;第二,录像和拷贝是两层概念,目录号标识“片名”,录像号标识“具体那盘带子”,租借业务挂在拷贝上而不是录像上。很多新手一上来就把片名当主键,后面查“某分公司某录像的拷贝情况”时直接卡死。

2.2 从事务需求反推约束与索引

需求文档的第二部分列了 26 条事务(a 到 z),这些不是功能清单,而是约束和索引的线索。我一般会按“写事务”和“读事务”分开处理。

写事务(a 到 l)决定外键和级联规则。比如“录入某分公司的新员工”要求员工必须挂在一个已存在的分公司下;“删除某会员租借某部录像的信息”意味着租借表要有明确的删除路径,不能因为删会员就把租借历史全冲掉。

读事务(m 到 z)决定索引。举几个原文里的高频查询:

  • m) 列出给定城市的分公司情况 → 分公司表按城市建索引
  • n) 按员工名字顺序列出指定分公司的员工 → 员工表按(分公司, 姓名)建复合索引
  • q) 按片名顺序列出某分公司指定演员的录像 → 需要演员-录像关联表,并按片名排序
  • v) 列出每个分公司每种录像的数量 → 拷贝表按(分公司, 录像)建索引,配合分组统计
  • y) 列出每个分公司在某一年注册的会员数量 → 会员表按(分公司, 注册日期)建索引

这些查询在原文里还带了频率:指定录像查询每天 5000 次(周日到周四)、10000 次(周五周六),高峰在下午 6 到 9 点。这意味着索引不是“有就行”,而是要在高峰期扛住每秒几次的并发。我一般会先把高频查询的 WHERE 和 ORDER BY 列出来,再决定复合索引的列顺序。

2.3 建表 SQL 骨架与参数说明

下面这段 SQL 是我按需求文档抠出来的最小可用骨架,用 PostgreSQL 语法写,MySQL 也能改。注意看注释里的约束来源。

-- 分公司表:名称全公司唯一,电话最多3行用单独字段存 CREATE TABLE branch ( branch_name VARCHAR(100) PRIMARY KEY, -- 需求:名称在全公司唯一 street VARCHAR(200) NOT NULL, city VARCHAR(100) NOT NULL, state VARCHAR(50) NOT NULL, zip VARCHAR(20) NOT NULL, phone1 VARCHAR(30), phone2 VARCHAR(30), phone3 VARCHAR(30) ); -- 员工表:员工号全公司唯一,职务区分经理/监理/其他 CREATE TABLE employee ( emp_no VARCHAR(20) PRIMARY KEY, -- 需求:员工号全公司唯一 emp_name VARCHAR(100) NOT NULL, position VARCHAR(50) NOT NULL, -- 经理/监理/助理/采购员等 salary NUMERIC(10,2), branch_name VARCHAR(100) NOT NULL REFERENCES branch(branch_name) ); -- 录像表:目录号唯一,种类限定为五类 CREATE TABLE video ( catalog_no VARCHAR(30) PRIMARY KEY, -- 需求:目录号唯一标识一盘录像 title VARCHAR(200) NOT NULL, category VARCHAR(20) NOT NULL CHECK (category IN ('动作','成人','儿童','恐怖','科幻')), daily_rent NUMERIC(8,2) NOT NULL, purchase_price NUMERIC(10,2), status VARCHAR(20) DEFAULT '可租' ); -- 拷贝表:录像号唯一,属于某个分公司和某个录像 CREATE TABLE copy ( copy_no VARCHAR(30) PRIMARY KEY, -- 需求:录像号唯一标识一份拷贝 catalog_no VARCHAR(30) NOT NULL REFERENCES video(catalog_no), branch_name VARCHAR(100) NOT NULL REFERENCES branch(branch_name), rentable BOOLEAN DEFAULT TRUE -- 需求:状态指出是否可出租 ); -- 会员表:会员号全局唯一,注册员工姓名也要记录 CREATE TABLE member ( member_no VARCHAR(30) PRIMARY KEY, -- 需求:会员号对所有分公司唯一 member_name VARCHAR(100) NOT NULL, address VARCHAR(300), reg_date DATE NOT NULL, reg_emp_name VARCHAR(100) -- 需求:负责注册的员工姓名 ); -- 租借表:租借号全公司唯一,关联会员和拷贝 CREATE TABLE rental ( rental_no VARCHAR(30) PRIMARY KEY, -- 需求:租借号全公司唯一 member_no VARCHAR(30) NOT NULL REFERENCES member(member_no), copy_no VARCHAR(30) NOT NULL REFERENCES copy(copy_no), rent_date DATE NOT NULL, return_date DATE, daily_fee NUMERIC(8,2) NOT NULL );

这段骨架里我故意没加演员和导演表,因为原文对演员的描述是“主要演员名字(以及扮演的角色)”,这是一个多对多关系,需要单独拆表。如果你只是做课程设计,可以先建actor和video_actor两张表;如果要完整复现查询 q 和 x,就必须拆。

参数说明:VARCHAR长度我按业务量估的,分公司名 100 够用,地址 300 能放下完整街道;NUMERIC(10,2)存薪水,NUMERIC(8,2)存日租费,避免浮点误差。CHECK约束把录像种类锁死在原文列的五类里,这是需求文档明确写的,不要漏。

3. 事务需求落地:录入、更新、删除与高频查询的 SQL 实现

3.1 录入与更新事务的 SQL 模板

原文的 a 到 l 是写事务,我挑几个最容易出错的写成模板。注意看每个事务对应的约束检查。

-- a) 录入一个新分公司 INSERT INTO branch (branch_name, street, city, state, zip, phone1) VALUES ('西雅图中心店', '123 Main St', 'Seattle', 'WA', '98101', '206-555-0100'); -- b) 录入某分公司的新员工 INSERT INTO employee (emp_no, emp_name, position, salary, branch_name) VALUES ('E2001', '张三', '监理', 5500.00, '西雅图中心店'); -- d) 录入给定分公司的某部录像拷贝 INSERT INTO copy (copy_no, catalog_no, branch_name, rentable) SELECT 'C0001', 'V100', '西雅图中心店', TRUE WHERE EXISTS (SELECT 1 FROM video WHERE catalog_no = 'V100') AND EXISTS (SELECT 1 FROM branch WHERE branch_name = '西雅图中心店'); -- f) 录入租借协议:先检查会员已租数量是否小于10 INSERT INTO rental (rental_no, member_no, copy_no, rent_date, daily_fee) SELECT 'R0001', 'M500', 'C0001', CURRENT_DATE, v.daily_rent FROM copy c JOIN video v ON c.catalog_no = v.catalog_no WHERE c.copy_no = 'C0001' AND (SELECT COUNT(*) FROM rental r WHERE r.member_no = 'M500' AND r.return_date IS NULL) < 10;

逻辑说明:录入拷贝时用EXISTS双重检查,防止往不存在的录像或分公司下挂拷贝;录入租借时用子查询卡住“一次最多租十部”的约束。这两个检查如果放到应用层做,并发下会翻车,放在 SQL 里至少能保证单条语句的原子性。

更新和删除事务(g 到 l)要注意级联顺序。比如删除某会员,不能直接DELETE FROM member,因为租借表有外键。常见做法是先删该会员未归还的租借记录,再删会员;或者把外键设为ON DELETE CASCADE,但那样会丢掉历史租借数据。我一般会保留历史,用软删除标记会员状态,而不是物理删除。

3.2 高频查询的索引设计与执行计划

原文的 m 到 z 是查询事务,其中 c、d、f 三条每天上万次,是性能大头。我把它们对应的索引写出来。

-- 查询 c:指定录像的情况,按目录号查 CREATE INDEX idx_video_catalog ON video(catalog_no); -- 查询 d:某盘录像的某份拷贝,按录像号查 CREATE INDEX idx_copy_copy_no ON copy(copy_no); -- 查询 f:会员租借录像的详细情况,按会员号查未归还 CREATE INDEX idx_rental_member_return ON rental(member_no, return_date); -- 查询 n:按员工名字顺序列出指定分公司员工 CREATE INDEX idx_employee_branch_name ON employee(branch_name, emp_name); -- 查询 v:每个分公司每种录像的数量 CREATE INDEX idx_copy_branch_catalog ON copy(branch_name, catalog_no);

参数说明:idx_rental_member_return把member_no放前面、return_date放后面,是因为查询 f 先按会员过滤,再判断是否未归还。如果反过来,索引选择性会变差。idx_employee_branch_name的列顺序对应查询 n 的 WHERE 和 ORDER BY,能同时命中过滤和排序。

验证方法:用EXPLAIN ANALYZE跑一遍查询,看是否走 Index Scan 而不是 Seq Scan。如果数据量小的时候优化器不选索引,可以临时SET enable_seqscan = off强制走索引,确认索引本身有效。

3.3 系统定义里的性能与安全参数怎么落

原文的系统定义部分给了很具体的数字:初始 20000 盘录像、400000 盘拷贝、2000 员工、100000 会员;每月新增 100 部新片、每片 20 份拷贝;每天 5000 条租借记录。这些数字直接决定分区和备份策略。

我一般会按租借表的增长速度做分区:每天 5000 条,两年就是 365 万条左右,按rent_date做月度分区,查询时能裁剪掉大部分数据。备份按原文要求“每天半夜 12 点”,用pg_dump加 cron 就能满足,但要注意备份期间不要锁表,用pg_dump -Fc自定义格式,恢复时更灵活。

安全性方面,原文要求“每个员工分配一个到特定用户视图的数据库访问权限”,对应到 PostgreSQL 就是建角色加视图授权。比如给监理建一个只能看本分公司员工和拷贝的视图,再GRANT SELECT ON view_name TO role_name。这一步很多课程设计会跳过,但它是需求文档明确写的,答辩时容易被问。

4. 避坑与排查:需求定义到建库过程中最容易翻车的五件事

4.1 把“录像”和“拷贝”混成一张表

现象:建表时只建了video,用copy_no当主键,结果查“某分公司某录像的拷贝情况”时发现同一个片名在不同分公司有不同拷贝,主键冲突。

原因:原文明确写了“目录号唯一地表示一盘录像”和“每个拷贝由录像号唯一地表示”,这是两层实体。录像描述片名和种类,拷贝描述具体那盘带子在哪家店、能不能租。

解决:拆成video和copy两张表,copy表用catalog_no外键关联video,用branch_name关联分公司。租借业务挂copy_no,不挂catalog_no。

4.2 会员号用分公司内自增

现象:两个分公司各自从 1 开始编会员号,跨店租借时会员号撞车,查租借历史查到别人的记录。

原因:原文写“会员号对所有分公司都是唯一的,而且可以在多个分公司使用同一会员注册号”。这意味着会员号是全局主键,不能按分公司自增。

解决:用全局序列或 UUID 生成会员号,或者在应用层用“分公司代码 + 序列”拼成全局唯一字符串。建表时member_no直接做主键,不要加branch_name做复合主键。

4.3 租借表只记会员号不记每日费用

现象:会员租的时候日租费是 5 元,还的时候录像涨价到 8 元,系统按新价格算租金,会员投诉。

原因:原文写租借业务数据包括“每日费用”,这个费用是租出时锁定的,不是还的时候现查的。

解决:rental表里加daily_fee字段,录入租借时从video.daily_rent复制一份存进去。这样即使录像价格调整,历史租借的计费不受影响。

4.4 删除员工时把租借历史一起冲掉

现象:员工离职后执行DELETE FROM employee,因为外键级联,该员工经手的所有租借记录被删,月底对账少了几百条。

原因:原文写“离开公司一年的员工记录从数据库中删除”,但租借记录要保留两年。这两个删除周期不一致,不能简单级联。

解决:员工表用软删除,加is_active字段标记离职,一年后再物理删除;租借表的外键设为ON DELETE RESTRICT,防止误删。或者把员工姓名冗余到租借表里,删员工不影响租借历史。

4.5 高峰期查询不走索引

现象:下午 6 到 9 点,查询指定录像的响应时间从 0.5 秒涨到 8 秒,超过原文要求的 5 秒上限。

原因:录像表数据量不大时优化器选全表扫描,但高峰期并发上来后全表扫描抢 CPU,拖慢所有查询。

解决:用EXPLAIN ANALYZE确认执行计划,对高频查询强制走索引;同时检查连接池大小,原文要求“每个分公司三名成员同时访问”,100 个分公司就是 300 并发,连接池不够会排队。常见做法是加 PgBouncer 做连接池,把最大连接数控制在数据库能扛的范围内。

5. 进阶技巧:用视图和物化视图把复杂查询压到毫秒级

原文的查询 v 和 z 是典型的聚合查询:v 要列出每个分公司每种录像的数量,z 要列出每个分公司的可能租金收入。这两条如果每次实时算,在 400000 盘拷贝的数据量下会拖垮数据库。我一般会用物化视图加定时刷新来解决。

-- 物化视图:每个分公司每种录像的数量 CREATE MATERIALIZED VIEW mv_branch_category_count AS SELECT c.branch_name, v.category, COUNT(*) AS copy_count FROM copy c JOIN video v ON c.catalog_no = v.catalog_no GROUP BY c.branch_name, v.category; -- 建唯一索引,支持 CONCURRENTLY 刷新 CREATE UNIQUE INDEX idx_mv_branch_cat ON mv_branch_category_count(branch_name, category); -- 定时刷新,比如每小时一次,避开高峰期 REFRESH MATERIALIZED VIEW CONCURRENTLY mv_branch_category_count;

逻辑说明:CONCURRENTLY刷新不锁表,查询可以继续走旧数据,适合高峰期前刷新一次。参数上,刷新频率取决于业务对实时性的要求,原文没有明确说 v 和 z 的查询频率,我一般按每小时一次处理,如果业务要求更实时,可以降到 15 分钟。

另一个技巧是把查询 q 和 r 的演员、导演关联做成覆盖索引。原文要求“按照片名顺序列出某分公司指定演员的录像名称、种类和是否可租借”,这条查询要跨actor、video_actor、video、copy四张表。我一般会建一个video_actor表,然后在(actor_name, title)上建复合索引,让排序在索引里完成,避免额外的 sort 步骤。

验证方法:用EXPLAIN ANALYZE对比物化视图和实时查询的执行时间,在 400000 行数据下,物化视图通常能把聚合查询从秒级压到毫秒级。但要注意物化视图的数据新鲜度,如果业务要求“可租借情况”实时反映,就不能用物化视图,得用普通视图加索引。

从那以后我每次拿到一份需求定义 PDF,都强制先做一遍实体-唯一键-基数映射,再动手写第一行 SQL。这份 StayHome 的需求文档虽然写于 2013 年,但它对唯一键和事务频率的描述,放到今天做任何租赁类系统都不过时。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表