简介:这是一份数据库课程设计文档,主题为小型超市信息管理系统,适合计算机、信息管理相关专业学生学习数据库设计与开发。资源包含完整的课程设计报告,涵盖需求分析、面向对象分析、数据库概念/逻辑/物理结构设计、系统实施与进度计划等环节,并给出具体用例分析、对象建模及关系模式优化示例,可作为课程设计报告撰写和数据库实践的重要参考。压缩包为1个docx文档,大小约282KB,已有321人查看学习。
1. 一套能照着复现的超市信息管理系统:数据库课程设计该交什么
数据库课程设计最怕的不是 SQL 写不出来,而是表结构一开始就没立住。这套小型超市信息管理系统课程设计资料,正好是一份从需求分析到触发器、视图、权限设计都走完的完整报告,原始环境是 SQL Server 2005,放到 SQL Server 2008/2012/2019 上跑也基本兼容。它的核心价值在于:不是给一个孤零零的建库脚本,而是把“超市老板要看盈利、收银员要记销售、仓管要管出入库”这些真实诉求,逐步翻译成十张表、索引、约束和触发器。适合正在做数据库课程设计、准备数据库面试、或者想补一遍进销存数据库设计全流程的人。照着文档把库建出来,等于把数据库增删改查和完整性设计完整练了一遍。
2. 需求分析与用例设计:把五个角色拆开再决定建哪几张表
2.1 五个角色和权限边界:谁该看到进价,谁不该看到
这个超市的设定很关键:面向生活小区的独家经营小型自选超市,老板就是管理人员,员工固定,没有复杂人事、赊账、折扣。业务量是平均每天 1000 个顾客、每人买 3 种商品,每周进一次货。这个规模决定了系统不需要采购审批流、不需要会员积分,但进销存和财务必须闭环。
角色划分是整份设计的起点。资料里明确区分了五类用户:
| 角色 | 核心诉求 | 能碰的数据 |
|---|---|---|
| 管理人员 | 全面掌握经营情况,辅助决策 | 供应商、商品、库存、人事、销售、财务全部数据 |
| 收银员 | 记录销售流水 | 只能录入销售记录和对应财务记录 |
| 仓库管理员 | 管好仓库进出 | 仓库信息、商品入库/出库/库存信息 |
| 采购员 | 管好进货源头 | 查供应商、录入进货信息及进货财务 |
| 顾客 | 查商品公开信息 | 商品基本信息,绝不能看到进价 |
这个权限矩阵直接决定了后面建表的粒度。比如顾客不能看进价,那么“商品基本信息表”里就不能把进价字段放到顾客可见的视图里,而是单独建一个商品视图,只暴露商品名、售价、单位、类别。收银员不需要知道供应商和进货价,所以销售模块单独用销售记录表,和进货表解耦。管理人员要看到所有信息,所以在权限设计里给的是 all。
实际做数据库课程设计时,我一般建议先把这个角色表画出来,再对着每个角色写用例。否则很容易出现“为了凑表而建表”,比如给收银员开一个供应商管理功能,这在业务上根本说不通。这份报告好就好在,每个用例都对应了一个明确的业务动作,没有多余的功能。
2.2 从用例到业务流程闭环:进货、入库、销售、财务一条线
把用例拼起来,会发现整个系统的数据流是一条完整的线:
供应商供货 → 采购员记进货表 → 仓库管理员录入入库信息 → 触发器更新库存表 → 商品上架销售 → 收银员写销售记录 → 每日结算时财务视图汇总销售额和盈利。
这条线里有个容易忽略的设计决策:进货和入库是两步,不是一步。进货表记录的是“从哪个供应商进了什么货”,入库信息表记录的是“货实际放进了哪个仓库”。如果只建一张表,采购员和仓管员的职责就混在一起了。这份资料把两张表分开,触发器在入库时自动维护库存数量,逻辑非常清楚。
再看财务,这是整份设计里最体现水平的地方。资料没有单独建“财务表”,而是明确写了“财务信息中的记录都可从其他基本表导出,所以不另建财务表,财务信息用视图表示”。这个决策非常符合关系数据库设计原则。进货支出可以从进货表和商品表关联算出,销售额和盈利可以从销售记录和商品表聚合得到,工资可以从职工基本表导出。如果单独建财务表,反而要处理数据同步、重复计算和更新异常。
所以拿到这份资料后,第一件事不是急着建表,而是先读第 2 章的角色和用例分析。理解了“谁用什么数据”之后再往后看表结构,每一个字段都能找到业务依据。这也是答辩时老师最喜欢问的点:为什么销售记录表里不存售价?为什么没有财务表?答案都藏在这条业务闭环里。
3. 逻辑结构设计:从用例到十张关系表的映射与优化
3.1 类与对象的提取:十个实体是怎么从用例里冒出来的
面向对象分析部分把用例中的对象抽成了类:职工基本信息、商品基本信息、商品库存信息、仓库基本信息、入库信息、出库信息、进货信息、销售记录、供应商信息。这些对象再转成关系模式,最终形成十张表:
- 商品基本信息表(商品号,商品名,进价,售价,单位,类别,是否销售,说明)
- 商品销售记录表(商品号,销售时间,数量)
- 商品库存信息表(商品号,仓库号,数量)
- 入库信息表(商品号,日期,仓库号,数量)
- 出库信息表(商品号,日期,仓库号,数量)
- 仓库基本信息表(仓库号,管理员职工号,面积)
- 进货表(商品号,供应商号,日期,数量)
- 供应商基本信息表(供应商号,名称,地址,电话,E_mail,联系人)
- 供应商品信息表(供应商号,供应商品号)
- 职工基本信息表(职工号,姓名,职务,性别,生日,电话,居住地址,工资,身份证号)
这里有个容易踩的细节:最初的供应商品信息表设计是“供应商号,供应商名,供应商品号,商品名”,优化后去掉了供应商名和商品名。因为在供应商基本信息表和商品基本信息表里已经有这两个字段了,关联查询时直接 join 就能取到,冗余存储反而要担心数据不一致。同样,商品销售记录表最初也包含了商品名,优化后只保留商品号,理由一样。
实体提取的方法是:先把用例里的名词圈出来,比如进货、仓库、商品、供应商、职工、销售记录,每一个持续存在的名词就是一个潜在实体,然后再看实体之间是一对多还是多对多。供应商和商品是多对多关系,所以需要中间表“供应商品信息表”;仓库和商品在需求约束下是“同种商品都存放在同一个仓库里”,所以商品库存信息表里商品号可以直接做主键,不需要再拆一个关联表。
3.2 关系模式优化:去冗余、定主外键、为什么财务不建表
十张表之间的关系,是整个逻辑结构设计的核心。整理成一张关系表会比较直观:
| 表名 | 主键 | 外键 | 说明 |
|---|---|---|---|
| 商品基本信息表 | 商品号 | 无 | 进价售价单位类别等基础属性 |
| 商品销售记录表 | 商品号 + 销售时间 | 商品号 → 商品基本信息表 | 记录每次销售的数量 |
| 商品库存信息表 | 商品号 | 商品号 → 商品基本信息表,仓库号 → 仓库基本信息表 | 每个商品只放一个仓库 |
| 入库信息表 | 商品号 + 日期 + 仓库号 | 商品号、仓库号 | 入库流水,触发库存更新 |
| 出库信息表 | 商品号 + 日期 + 仓库号 | 商品号、仓库号 | 出库流水,触发库存扣减 |
| 仓库基本信息表 | 仓库号 | 管理员职工号 → 职工基本信息表 | 仓库位置和面积 |
| 进货表 | 商品号 + 供应商号 + 日期 | 商品号、供应商号 | 记录供应商供货流水 |
| 供应商基本信息表 | 供应商号 | 无 | 名称、地址、电话、联系人 |
| 供应商品信息表 | 供应商号 + 供应商品号 | 供应商号、商品号 | 多对多关系的中间表 |
| 职工基本信息表 | 职工号 | 无 | 包括职务、工资、身份证号 |
注意看几个设计取舍。商品库存信息表的主键直接用了商品号,这是基于“同种商品放同一个仓库”的业务约束。如果未来超市扩大规模,一个商品分到多个仓库,这个主键就要改成“商品号 + 仓库号”联合主键。课程设计里这样设计没问题,但答辩时如果老师追问扩展性,要能说清楚这个假设。
商品销售记录表的主键用了“商品号 + 销售时间”,因为同一个商品在同一秒内只可能有一条销售记录,用于课程设计足够了。出库信息表、入库信息表的联合主键也是同样思路。
关键决策还是财务不建表。资料的原文写得很明确:“财务信息中的记录都可其他基本表导出,所以不另建财务表,财务信息用视图表示。”这是符合关系数据库理论的。进货支出、销售额、工资、盈利都是派生数据,单独建表会带来三个问题:一是录入时容易和基础表数据不一致,二是修改基础数据后财务表不同步,三是表数量膨胀增加维护成本。用视图从基础表实时聚合,天然保证一致性。
完整性设计方面,资料对每个表的主键、外键、默认值和检查约束都做了定义。职工性别限定“男”“女”,商品是否销售限定“是”“否”,是否销售默认“是”,性别默认“男”。这些约束在后面的建表语句里都要体现。
4. 物理结构与完整性实现:索引、约束与触发器的落地写法
4.1 建表与约束:十张表的 DDL 应该怎么写
物理结构设计的目标是让查询变快、保证数据完整。先看两张核心表的建表语句,其余表结构同理。
-- 商品基本信息表:商品号是主键,进价售价用 money 类型 CREATE TABLE 商品基本信息表 ( 商品号 nchar(10) NOT NULL PRIMARY KEY, 商品名 nchar(50) NOT NULL, 进价 money NOT NULL, 售价 money NOT NULL, 单位 nchar(10) NOT NULL, 类别 nchar(20) NOT NULL, 是否销售 nchar(2) NOT NULL DEFAULT N'是', 说明 nchar(100) NULL, CONSTRAINT CK_商品_是否销售 CHECK (是否销售 IN (N'是', N'否')) ); GO -- 商品库存信息表:商品号唯一,仓库号引用仓库表 CREATE TABLE 商品库存信息表 ( 商品号 nchar(10) NOT NULL PRIMARY KEY, 仓库号 nchar(10) NOT NULL, 数量 int NOT NULL, CONSTRAINT FK_库存_商品 FOREIGN KEY (商品号) REFERENCES 商品基本信息表(商品号), CONSTRAINT FK_库存_仓库 FOREIGN KEY (仓库号) REFERENCES 仓库基本信息表(仓库号) ); GO注意几个细节。中文列名在 SQL Server 里必须加方括号包围,字符串常量要加 N 前缀,否则中文字符在某些排序规则下会变成乱码。“是否销售”字段用了 nchar(2),配合 CHECK 约束,从数据库层面堵住非法数据。说明字段允许 NULL,因为不是所有商品都需要备注停售原因。
建表顺序有讲究,先建没有外键依赖的表:供应商基本信息表、职工基本信息表、仓库基本信息表、商品基本信息表,然后再建引用它们的外键表。否则 SQL Server 会直接报找不到引用对象的错误。
其余表用同样的写法。出库信息表、入库信息表、进货表都在商品号、仓库号、供应商号上加外键。职工基本信息表里的身份证号用 char(18),因为身份证号长度固定,不会出现前导零被截断的问题。
4.2 索引设计:唯一索引和普通索引怎么取舍
资料里给出的索引方案分两层:唯一索引和普通非唯一索引。
-- 唯一索引:商品号、仓库号、职工号、供应商号 CREATE UNIQUE INDEX IX_商品号 ON 商品基本信息表(商品号); CREATE UNIQUE INDEX IX_仓库号 ON 仓库基本信息表(仓库号); CREATE UNIQUE INDEX IX_职工号 ON 职工基本信息表(职工号); CREATE UNIQUE INDEX IX_供应商号 ON 供应商基本信息表(供应商号); GO -- 普通索引:流水表里的商品号、仓库号、供应商号是高频查询列 CREATE INDEX IX_销售_商品号 ON 商品销售记录表(商品号); CREATE INDEX IX_入库_商品号 ON 入库信息表(商品号); CREATE INDEX IX_入库_仓库号 ON 入库信息表(仓库号); CREATE INDEX IX_出库_商品号 ON 出库信息表(商品号); CREATE INDEX IX_进货_商品号 ON 进货表(商品号); CREATE INDEX IX_进货_供应商号 ON 进货表(供应商号); GO这里有个实际执行时容易犯的错:商品基本信息表里的商品号已经是主键,主键约束在 SQL Server 里会自动创建一个唯一索引,再手动建 IX_商品号 就是重复索引,纯属浪费空间。资料里第 1 条写了两个 create unique index,可能是笔误,实际做的时候主键表和唯一索引表建一个就够。真正需要额外建唯一索引的场景是“业务上唯一但没设主键”的列,比如身份证号如果没做主键,可以建唯一索引保证不重复。
普通索引的列选择一看查询频率,二看更新频率。商品号在销售、入库、出库、进货、库存里都是关联条件,建索引能显著加速 join 和 where 查询。仓库号、供应商号也是同理。但“是否销售”这种低选择性的列就不适合建索引,因为只有“是”“否”两个值,索引对查询优化几乎没帮助。
4.3 触发器设计:入库自动加库存,出库自动扣库存
触发器的设计是这份资料里最体现工程价值的部分。入库触发器的目标是:当某种商品入库时,检查仓库里是否已有该商品,有则累加数量,没有则插入新记录。出库触发器则是扣减库存,同时保证扣减后库存不为负。
-- 入库触发器:存在则累加,不存在则新增 CREATE TRIGGER TR_入库_更新库存 ON 入库信息表 AFTER INSERT AS BEGIN SET NOCOUNT ON; -- 先更新已存在的库存记录 UPDATE 商品库存信息表 SET 数量 = 数量 + i.数量 FROM 商品库存信息表 k INNER JOIN inserted i ON k.商品号 = i.商品号 AND k.仓库号 = i.仓库号; -- 再补插不存在的商品 INSERT INTO 商品库存信息表 (商品号, 仓库号, 数量) SELECT i.商品号, i.仓库号, i.数量 FROM inserted i LEFT JOIN 商品库存信息表 k ON k.商品号 = i.商品号 AND k.仓库号 = i.仓库号 WHERE k.商品号 IS NULL; END GO这个写法避免了“先 IF EXISTS 再 UPDATE 或 INSERT”的经典竞争问题。如果一次批量插入多条入库记录,其中一部分商品已经有库存、另一部分没有,IF EXISTS 配合单值 UPDATE 会漏数据。先更新已存在行,再用 LEFT JOIN 过滤出不存在行做补插,两条语句覆盖所有情况。
-- 出库触发器:先校验库存是否足够,再执行扣减 CREATE TRIGGER TR_出库_扣减库存 ON 出库信息表 AFTER INSERT AS BEGIN SET NOCOUNT ON; -- 有任何一条出库数量大于库存就整体回滚 IF EXISTS ( SELECT 1 FROM inserted i LEFT JOIN 商品库存信息表 k ON k.商品号 = i.商品号 AND k.仓库号 = i.仓库号 WHERE k.数量 IS NULL OR k.数量 < i.数量 ) BEGIN RAISERROR(N'出库数量超过当前库存,事务已回滚', 16, 1); ROLLBACK TRANSACTION; RETURN; END UPDATE 商品库存信息表 SET 数量 = 数量 - i.数量 FROM 商品库存信息表 k INNER JOIN inserted i ON k.商品号 = i.商品号 AND k.仓库号 = i.仓库号; END GO出库触发器里最关键的是先检查再更新。如果把检查放在 UPDATE 之后,已经扣成负数才发现超卖,数据已经写坏了,就算回滚也得依赖事务机制兜底。前置检查的好处是:一旦发现库存不足,立刻 RAISERROR 并回滚,整个触发器和调用它的 INSERT 语句一起撤销,不会留下半截数据。
5. 避坑:进销存建库最容易翻车的四个地方
5.1 触发器重复更新库存:数据翻倍
现象:入库信息表插入一条记录,库存表数量变成了两倍。原因:触发器在 after insert 里同时执行了 UPDATE 和 INSERT,或者发现库里有同名触发器重复创建,一条入库流水被两个触发器各处理一次。解决:先检查库里的触发器是否存在,用 IF OBJECT_ID('TR_入库_更新库存') IS NOT NULL DROP TRIGGER 清理旧触发器,再重新执行创建脚本。
5.2 唯一索引建重:主键和唯一索引重复
现象:按资料建表时,商品基本信息表既有 PRIMARY KEY 约束,又手动建了 UNIQUE INDEX,插入数据时提示重复索引或空间浪费。原因:主键约束在 SQL Server 里已经自动创建了唯一索引,手工再建属于冗余。解决:只保留主键约束,普通列需要唯一性时再单独建唯一索引。
5.3 商品下架删不掉:外键引用挡住了 delete
现象:停售商品时想把商品基本信息表里的记录删掉,结果外键约束报错。原因:销售记录表、库存表、入库表都引用了商品号,直接删主表记录会破坏参照完整性。解决:不要物理删除,用“是否销售”字段做软下架。资料里专门设计了“停售商品”存储过程:先确认库存为 0,删除库存记录,再把商品基本信息表里的“是否销售”改为“否”。这个流程既保住了历史销售记录,又实现了下架语义。
5.4 改售价后历史销售金额变了
现象:今天把某商品售价从 3 元改成 4 元,结果昨天的销售额统计也变了。原因:销售记录表里只存了商品号和数量,没有冗余存当时成交价,财务视图实时关联商品基本信息表的当前售价来计算金额。解决:课程设计的默认前提是“售价不变”,此前提下视图计算没问题。如果要真实落地,建议在商品销售记录表里加一个“售价”快照字段,开单时把当时的售价写进去,历史统计从此不受调价影响。这个点面试里经常被拿出来问,答复思路就是快照 vs 实时关联。
6. 进阶用法:用核对脚本和事务把进销存数据管住
搭建完基础功能后,真正让这套数据库“能用”的,是把它当成一套需要持续维护的进销存系统来验证。课程设计最难的不是建表的瞬间,而是运行一段时间后数据还能对得上。我会反复跑一条核对脚本,把入库累计、出库累计和库存表做一次三方对账:
-- 核对库存表账面数与流水累计数是否一致 SELECT k.商品号, k.数量 AS 库存账面, ISNULL((SELECT SUM(数量) FROM 入库信息表 WHERE 商品号 = k.商品号), 0) AS 累计入库, ISNULL((SELECT SUM(数量) FROM 出库信息表 WHERE 商品号 = k.商品号), 0) AS 累计出库, ISNULL((SELECT SUM(数量) FROM 入库信息表 WHERE 商品号 = k.商品号), 0) - ISNULL((SELECT SUM(数量) FROM 出库信息表 WHERE 商品号 = k.商品号), 0) AS 推算库存 FROM 商品库存信息表 k WHERE k.数量 <> ISNULL((SELECT SUM(数量) FROM 入库信息表 WHERE 商品号 = k.商品号), 0) - ISNULL((SELECT SUM(数量) FROM 出库信息表 WHERE 商品号 = k.商品号), 0);这条 SQL 专门用来找出“账面库存和流水对不上”的商品。只要返回任何一行,就说明触发器漏执行了,或者有人直接手工改过库存表、绕过了触发器。发现问题后,优先检查触发器是否被禁用,其次检查是否有并发操作在同一时间插入了重复的入库记录,导致死锁后部分事务回滚、触发器没跑完。这个核对脚本可以存成视图长期使用,也可以放进数据库 Agent 作业里每天定时跑。
另一个值得养成的习惯是,所有涉及多表写入的操作都包在显式事务里。比如录入一笔进货和一笔入库时:
BEGIN TRANSACTION; INSERT INTO 进货表 (商品号, 供应商号, 日期, 数量) VALUES (N'P001', N'S001', GETDATE(), 100); INSERT INTO 入库信息表 (商品号, 日期, 仓库号, 数量) VALUES (N'P001', GETDATE(), N'W001', 100); COMMIT TRANSACTION;一旦第二个 INSERT 失败,整个事务回滚,不会出现“进货表记了但库存没加”的状态。这也是避免触发器边界问题的最有效手段:让业务操作具备原子性。平时演示给老师看的时候,先执行核对脚本展示数据一致,再演示触发器的自动更新,比直接贴建表语句有说服力得多。
从那以后,我每做一个进销存方向的数据库课程设计,都会在交付前跑一遍核对脚本,把入库累计减出库累计同库存表强制对账,对不上的地方先查触发器再查权限,确认全部通过才敢说这个库是真的立住了。希望帮到你。
本文还有配套的精品资源,点击获取