2026/10/13 23:08:09

SQL Server超市进销存数据库课设:从建表到存储过程与触发器实战

SQL Server超市进销存数据库课设:从建表到存储过程与触发器实战 简介这份文档是辽宁工业大学软件学院SQL Server数据库技术课程设计报告题目为超市进销存管理系统面向软件工程等专业学生及需要数据库课程设计参考的学习者。报告完整呈现了从需求分析、概念设计、逻辑设计、物理设计到数据库实现与程序设计的全过程包含数据流图、数据字典、E-R图、关系模型等规范内容并在SQL Server 2005上完成数据库建模配合C#实现简单调用帮助读者理解C/S模式下的数据库应用开发流程。资源包共1个doc文件约420KB内容涵盖设计目的与要求、设计内容、设计总结及参考文献等章节结构完整、条理清晰。目前已有418人学习适合作为数据库原理与SQL Server技术综合实践的学习范本也可为撰写课程设计报告、梳理数据库设计步骤提供直接参考。1. 从一份超市进销存文档说起SQL Server 数据库课设到底要落地什么很多人搜「数据库SQL-SERVER数据库技术--超市进销存管理系统.doc」其实是在赶一份数据库课程设计要交文档还要能跑起来。超市进销存这个场景选得聪明——商品、供应商、库存、销售、退货天然是一套多对多关系正好把 SQL Server 的表设计、约束、视图、存储过程、触发器全用上。但真正卡住人的不是「进销存」三个字而是从需求到建库、从建库到能增删改查、从能查到能出报表这条链路。这篇笔记就按这条链路走先讲清数据模型怎么定再给可直接抄的建表与查询脚本最后把课设答辩和实际部署里最容易翻车的地方摊开。适合正在做数据库课设的学生也适合想用 SQL Server 快速搭一套小型进销存原型的开发者。2. 超市进销存的数据模型先把实体和关系钉死再动手2.1 为什么先画 ER 再建表而不是直接写 CREATE TABLE直接开写建表语句是课设里最常见的翻车起点。进销存的核心矛盾是一个商品会出现在多张采购单和销售单里一个供应商会供多种商品库存数量又是采购减销售再加减退货算出来的。如果表关系没理清后面写查询时就会发现要么数据冗余到改一处漏三处要么连「某商品当前库存」都算不出来。我一般先把实体列出来商品Product、供应商Supplier、客户Customer超市场景可简化、采购单PurchaseOrder及其明细PurchaseDetail、销售单SaleOrder及其明细SaleDetail、库存Inventory。关系上采购单和销售单都是「主表 明细表」结构明细表通过外键挂到主表同时挂到商品表。库存表可以独立存当前数量也可以完全由流水算出来——课设里建议独立存一张库存表方便直接查但要用触发器或存储过程保证它和流水一致。这里有个选型判断明细表的主键用自增 ID 还是「单号 商品号」复合主键。复合主键能天然防重同一单同一商品只出现一次但后续关联查询写起来啰嗦自增 ID 简单但需要额外加唯一约束防重。我倾向自增 ID 加唯一约束兼顾查询便利和数据一致性。2.2 建库建表的最小可跑脚本下面这套脚本在 SQL Server 2016 及以上都能直接跑。先建库再按依赖顺序建表供应商、商品、库存在前采购销售主从表在后。-- 建库字符集用默认即可排序规则选中文常用 CREATE DATABASE SupermarketDB; GO USE SupermarketDB; GO -- 供应商表 CREATE TABLE Supplier ( SupplierID INT IDENTITY(1,1) PRIMARY KEY, SupplierName NVARCHAR(100) NOT NULL, Contact NVARCHAR(50), Phone VARCHAR(20), CreateTime DATETIME DEFAULT GETDATE() ); -- 商品表Category 用字符串简化课设够用 CREATE TABLE Product ( ProductID INT IDENTITY(1,1) PRIMARY KEY, ProductName NVARCHAR(100) NOT NULL, Category NVARCHAR(50), Unit NVARCHAR(10) DEFAULT N件, PurchasePrice DECIMAL(10,2) NOT NULL DEFAULT 0, SalePrice DECIMAL(10,2) NOT NULL DEFAULT 0, SupplierID INT FOREIGN KEY REFERENCES Supplier(SupplierID) ); -- 库存表一个商品一条记录 CREATE TABLE Inventory ( ProductID INT PRIMARY KEY FOREIGN KEY REFERENCES Product(ProductID), Quantity INT NOT NULL DEFAULT 0, UpdateTime DATETIME DEFAULT GETDATE(), CONSTRAINT CK_Inventory_Qty CHECK (Quantity 0) ); -- 采购主表 CREATE TABLE PurchaseOrder ( PurchaseID INT IDENTITY(1,1) PRIMARY KEY, SupplierID INT FOREIGN KEY REFERENCES Supplier(SupplierID), PurchaseDate DATETIME DEFAULT GETDATE(), TotalAmount DECIMAL(12,2) DEFAULT 0, Operator NVARCHAR(50) ); -- 采购明细 CREATE TABLE PurchaseDetail ( DetailID INT IDENTITY(1,1) PRIMARY KEY, PurchaseID INT FOREIGN KEY REFERENCES PurchaseOrder(PurchaseID), ProductID INT FOREIGN KEY REFERENCES Product(ProductID), Quantity INT NOT NULL CHECK (Quantity 0), Price DECIMAL(10,2) NOT NULL, CONSTRAINT UQ_Purchase_Product UNIQUE (PurchaseID, ProductID) ); -- 销售主表与明细结构对称 CREATE TABLE SaleOrder ( SaleID INT IDENTITY(1,1) PRIMARY KEY, SaleDate DATETIME DEFAULT GETDATE(), TotalAmount DECIMAL(12,2) DEFAULT 0, Operator NVARCHAR(50) ); CREATE TABLE SaleDetail ( DetailID INT IDENTITY(1,1) PRIMARY KEY, SaleID INT FOREIGN KEY REFERENCES SaleOrder(SaleID), ProductID INT FOREIGN KEY REFERENCES Product(ProductID), Quantity INT NOT NULL CHECK (Quantity 0), Price DECIMAL(10,2) NOT NULL, CONSTRAINT UQ_Sale_Product UNIQUE (SaleID, ProductID) );逻辑说明IDENTITY(1,1)让主键自增省去手工维护外键保证明细不会挂到不存在的单或商品上CHECK (Quantity 0)是库存的最后一道防线防止出现负库存这种业务上说不通的数据明细表的UNIQUE (PurchaseID, ProductID)防止同一张单里同一个商品录两次这是课设答辩常被问的点。参数说明金额统一用DECIMAL(10,2)或DECIMAL(12,2)别用FLOAT浮点算钱会出现0.30000000000000004这种结果对账时是灾难。商品名、供应商名用NVARCHAR支持中文电话用VARCHAR就够。提示建表顺序不能乱被外键引用的表必须先建否则报「引用了无效的表」。如果脚本要反复跑开头加IF DB_ID(SupermarketDB) IS NOT NULL DROP DATABASE SupermarketDB;先清干净。3. 增删改查与库存联动把业务逻辑写进存储过程和触发器3.1 采购入库时库存怎么自动加课设里最容易丢分的地方是采购单录进去了库存表没动。手工再改库存表数据就对不上了。正确做法是把「插入采购明细」和「增加库存」放进一个事务里用存储过程封装。CREATE PROCEDURE AddPurchaseDetail PurchaseID INT, ProductID INT, Quantity INT, Price DECIMAL(10,2) AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; -- 插入明细 INSERT INTO PurchaseDetail (PurchaseID, ProductID, Quantity, Price) VALUES (PurchaseID, ProductID, Quantity, Price); -- 库存存在则累加不存在则初始化 IF EXISTS (SELECT 1 FROM Inventory WHERE ProductID ProductID) UPDATE Inventory SET Quantity Quantity Quantity, UpdateTime GETDATE() WHERE ProductID ProductID; ELSE INSERT INTO Inventory (ProductID, Quantity) VALUES (ProductID, Quantity); -- 更新主单总金额 UPDATE PurchaseOrder SET TotalAmount ISNULL(( SELECT SUM(Quantity * Price) FROM PurchaseDetail WHERE PurchaseID PurchaseID ), 0) WHERE PurchaseID PurchaseID; COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; THROW; END CATCH END;逻辑说明整个操作包在事务里任何一步失败都回滚不会出现「明细插了库存没加」的半截状态。IF EXISTS ... UPDATE ELSE INSERT是库存表的 upsert 写法SQL Server 没有原生ON CONFLICT用MERGE也行但坑多课设里这种写法最直观。主单总金额每次重算避免累加误差。参数说明Quantity必须为正明细表的CHECK会兜底Price用采购价销售明细里换成售价。调用方式EXEC AddPurchaseDetail 1, 3, 50, 12.50;。3.2 销售出库的库存校验与扣减销售比采购多一层校验库存不够不能卖。这个校验必须放在扣减之前且和扣减在同一事务里否则并发下会超卖。CREATE PROCEDURE AddSaleDetail SaleID INT, ProductID INT, Quantity INT, Price DECIMAL(10,2) AS BEGIN SET NOCOUNT ON; DECLARE Stock INT; BEGIN TRY BEGIN TRANSACTION; SELECT Stock Quantity FROM Inventory WITH (UPDLOCK) WHERE ProductID ProductID; IF Stock IS NULL OR Stock Quantity BEGIN RAISERROR (N库存不足当前库存 %d, 16, 1, Stock); ROLLBACK TRANSACTION; RETURN; END INSERT INTO SaleDetail (SaleID, ProductID, Quantity, Price) VALUES (SaleID, ProductID, Quantity, Price); UPDATE Inventory SET Quantity Quantity - Quantity, UpdateTime GETDATE() WHERE ProductID ProductID; UPDATE SaleOrder SET TotalAmount ISNULL(( SELECT SUM(Quantity * Price) FROM SaleDetail WHERE SaleID SaleID ), 0) WHERE SaleID SaleID; COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; THROW; END CATCH END;逻辑说明WITH (UPDLOCK)是关键它在读库存时就加更新锁防止两个销售单同时读到相同库存然后都扣减导致超卖。这是数据库并发锁在进销存里最典型的应用。库存不足时用RAISERROR抛出可读信息前端能直接显示。参数说明UPDLOCK只锁到事务结束粒度是行级有索引时不会锁整表。如果课设并发要求不高去掉也能跑但答辩时能说出这个锁的用途是加分项。3.3 几个高频查询库存预警、销售排行、供应商供货统计建完表、写完过程课设文档里通常要配几张查询截图。下面三条覆盖最常见的报表需求。-- 1. 库存低于 20 的商品预警 SELECT p.ProductName, i.Quantity, p.Unit FROM Inventory i JOIN Product p ON p.ProductID i.ProductID WHERE i.Quantity 20 ORDER BY i.Quantity ASC; -- 2. 商品销售排行按销售数量 SELECT TOP 10 p.ProductName, SUM(sd.Quantity) AS TotalSold, SUM(sd.Quantity * sd.Price) AS TotalAmount FROM SaleDetail sd JOIN Product p ON p.ProductID sd.ProductID GROUP BY p.ProductName ORDER BY TotalSold DESC; -- 3. 各供应商供货金额统计 SELECT s.SupplierName, COUNT(DISTINCT po.PurchaseID) AS OrderCount, SUM(pd.Quantity * pd.Price) AS SupplyAmount FROM Supplier s LEFT JOIN PurchaseOrder po ON po.SupplierID s.SupplierID LEFT JOIN PurchaseDetail pd ON pd.PurchaseID po.PurchaseID GROUP BY s.SupplierName ORDER BY SupplyAmount DESC;逻辑说明第一条用JOIN把库存和商品名拼起来阈值 20 可按业务改第二条TOP 10加GROUP BY做排行SUM里带价格算销售额第三条用LEFT JOIN保证没供过货的供应商也出现金额为NULL需要的话外面套ISNULL。参数说明COUNT(DISTINCT po.PurchaseID)防止明细多行导致单数被重复计数这是统计类查询的常见坑。数据量大时给SaleDetail.ProductID、PurchaseDetail.PurchaseID建非聚集索引。4. 课设文档与部署里的避坑清单这些坑我替你踩过了4.1 现象脚本在同学电脑上跑不通报「无法打开登录所请求的数据库」原因脚本里写了USE SupermarketDB但对方还没建库或者实例名和你不一样有人是localhost有人是.\SQLEXPRESS。解决把建库和建表拆成两段建库段单独跑连接字符串里的实例名在文档里写清楚别默认别人和你一样。4.2 现象插入中文变成问号原因列定义用了VARCHAR而不是NVARCHAR或者插入时字符串前没加N前缀。解决所有存中文的列用NVARCHAR插入语句写成N可乐。这个坑在课设演示时特别显眼一屏问号直接扣印象分。4.3 现象删除商品时报外键冲突原因该商品在采购明细或销售明细里还有记录外键挡住了删除。解决这是外键在保护数据正确做法是软删除——给Product加IsActive BIT DEFAULT 1字段删除时置 0查询时过滤。硬删要先删明细但历史单据就断了不推荐。4.4 现象库存出现负数原因绕过存储过程直接UPDATE Inventory或者销售时没做库存校验。解决库存表加CHECK (Quantity 0)兜底所有库存变动只走存储过程禁止在文档里教用户直接改库存表。4.5 现象总金额和明细对不上原因主单TotalAmount是插入时算一次后续改了明细没重算。解决像前面存储过程那样每次明细变动后重算主单金额或者干脆不存TotalAmount查询时实时SUM用视图封装。课设里存字段方便展示但一定要在明细变动处同步更新。5. 从课设到能演示用视图和备份把系统收尾5.1 用视图把多表查询包装成「一张表」答辩时老师不会等你现场写 JOIN。把常用查询做成视图演示时SELECT * FROM v_StockWarning一行出结果既快又显得设计完整。CREATE VIEW v_ProductFull AS SELECT p.ProductID, p.ProductName, p.Category, p.Unit, p.PurchasePrice, p.SalePrice, s.SupplierName, ISNULL(i.Quantity, 0) AS StockQty FROM Product p LEFT JOIN Supplier s ON s.SupplierID p.SupplierID LEFT JOIN Inventory i ON i.ProductID p.ProductID;逻辑说明LEFT JOIN保证没设供应商或没库存的商品也出现ISNULL把空库存显示成 0。视图不存数据底层表一变查询结果就变适合做展示层。注意视图里别用SELECT *列名写死否则底层加字段视图结果会错位。5.2 备份与还原演示前的后悔药课设演示最怕数据被误删。演示前做一次完整备份出问题几分钟还原。-- 备份到指定路径路径要存在且 SQL Server 服务账号有写权限 BACKUP DATABASE SupermarketDB TO DISK D:\Backup\SupermarketDB.bak WITH INIT, NAME SupermarketDB-Full; -- 还原会覆盖现有库需先断开连接 RESTORE DATABASE SupermarketDB FROM DISK D:\Backup\SupermarketDB.bak WITH REPLACE;逻辑说明WITH INIT覆盖同名备份文件避免追加导致文件膨胀还原时WITH REPLACE允许覆盖现有库。路径权限是高频报错点报「拒绝访问」就换到C:\Program Files\Microsoft SQL Server之外的目录或给服务账号授权。参数说明生产环境还会用WITH DIFFERENTIAL做差异备份课设一次完整备份足够。还原前确保没有活动连接否则报「数据库正在使用」。5.3 一个我自己的习惯每次改完表结构或存储过程我会立刻跑一遍「插入采购 → 插入销售 → 查库存 → 查报表」这条最小链路确认没断。课设文档可以最后写但这条链路必须随时是通的。数据库这东西玄学不多翻车基本都翻在「以为改一处不影响别处」。把事务、外键、约束这三样用足进销存的坑能少踩一大半。希望帮到你。本文还有配套的精品资源点击获取