简介:一套基于Java与SQL Server/MySQL的餐厅点餐系统课程设计项目,适合数据库及软件工程方向学生作为课程设计、毕业设计参考。系统围绕菜品信息、消费信息、包厢信息、员工信息等模块展开,前端界面由Eclipse开发,后台结合JDBC完成数据交互,数据库脚本与Java源码完整,可帮助读者理解表结构设计、数据持久化以及桌面端功能联动。资源包共298个文件,包括120个java源码、98个class编译文件、24张jpg界面截图、3个SQL脚本、8个xml配置、5个doc设计文档、mdf/ldf数据库文件及jar依赖等,压缩包约24.29MB,目录层次清晰。已有14237人学习下载,适合需要快速搭建餐饮管理类数据库项目的学习者,借助源码和文档可梳理点餐流程、复用订单与菜品管理模块,并高效完成课程设计报告。
1. 为什么“SQL Server + 餐厅点餐系统”是数据库课程设计里最值得做透的题目
如果你的数据库课程设计选型是 SQL Server,题目又落在餐厅点餐系统上,那你等于拿到一个能把主外键、约束、事务、存储过程、触发器、索引验证全部串起来的完整场景。每年答辩都有大量同题作品,但多数停在“表建了十几张、能增删改查”,老师追问一句“两个收银员同时给同一桌结账会发生什么”就答不上来。
真正拉开差距的地方在于:订单与桌台状态如何用事务保持一致、结账如何防止重复处理、销售统计能不能不靠每次临时拼 SQL。下面这套方案按可以直接交付的标准来写,从建库、七张核心表、存储过程、触发器,到索引体检和造数验证,照着做一遍一个下午能跑通。适合正在写课程设计的同学,也适合想用这个题目把 SQL Server 基础操作练扎实的开发者。
2. 把“开台到结账”拆成表结构:七张核心表与完整建库脚本
2.1 状态机先于建表:一张订单从开台到结账要经过哪几个状态
很多同学一上来就建表,建到一半发现订单和桌台状态分不清。我习惯先列状态机,再回来设计表结构,这样每张表需要哪些字段、哪些约束会清晰很多。
点餐业务的核心状态有三组:
| 表 | 状态字段 | 取值 | 含义 |
|---|---|---|---|
| Orders | StatusCode | 0 / 1 / 2 / 3 / 4 | 0 已开台、1 点餐中、2 已下单、3 已结账、4 已取消 |
| DineTable | StatusCode | 0 / 1 / 2 | 0 空闲、1 已开台、2 待清洁 |
| OrderItems | StatusCode | 0 / 1 / 2 | 0 正常、1 已退、2 已上菜 |
订单状态和桌台状态必须分开,这一点经常被忽略。坐下点菜时,桌台已经算“已开台”,但订单还处于“点餐中”;结账完成后,订单进入“已结账”,桌台则进入“待清洁”,两个状态的切换时机完全不同。如果把状态都塞在一张表里,后面写存储过程时会出现大量互相矛盾的更新逻辑。
状态设计完成后再看实体关系:服务员开台、顾客点菜、后厨出菜、收银结账,中间还有会员积分和支付流水。核心关系是 DineTable 一对多 Orders,Orders 一对多 OrderItems,Dish 一对多 OrderItems,Orders 一对多 Payment,Member 一对多 Orders。除了订单和菜品通过明细表间接关联外,没有真正意义上的多对多关系,所以不需要额外建关联表,ER 图画起来也干净。
2.2 建库与核心表脚本:主外键、CHECK约束与命名规范
建库时顺手把排序规则定成中文常用规则,避免后续字符串比较和中文显示出问题。脚本如下:
CREATE DATABASE RestaurantDb COLLATE Chinese_PRC_CI_AS; GO USE RestaurantDb; GOCOLLATE 指定为 Chinese_PRC_CI_AS 后,字符串比较默认不区分大小写,中文排序按拼音,最直观的好处是“abc”和“ABC”不会被认为是两个不同的桌号前缀。这一步后面省掉很多乱码和匹配问题。
然后是员工、桌台、菜品、会员四张基础表。这四张表是业务的地基,先建它们,后面的订单表才能引用:
CREATE TABLE dbo.Employee ( EmpId INT IDENTITY(1,1) PRIMARY KEY, EmpNo VARCHAR(20) NOT NULL UNIQUE, EmpName NVARCHAR(20) NOT NULL, RoleCode TINYINT NOT NULL DEFAULT 0, PassHash VARBINARY(64) NOT NULL, IsActive BIT NOT NULL DEFAULT 1, CreatedAt DATETIME2(0) NOT NULL DEFAULT SYSDATETIME() ); CREATE TABLE dbo.DineTable ( TableId INT IDENTITY(1,1) PRIMARY KEY, TableNo VARCHAR(10) NOT NULL UNIQUE, SeatCount TINYINT NOT NULL CHECK (SeatCount BETWEEN 1 AND 20), Area NVARCHAR(20) NOT NULL DEFAULT N'大厅', StatusCode TINYINT NOT NULL DEFAULT 0, EmpId INT NULL, CONSTRAINT FK_DineTable_Employee FOREIGN KEY (EmpId) REFERENCES dbo.Employee(EmpId) ); CREATE TABLE dbo.Dish ( DishId INT IDENTITY(1,1) PRIMARY KEY, DishName NVARCHAR(50) NOT NULL, Category NVARCHAR(20) NOT NULL DEFAULT N'热菜', Price DECIMAL(10,2) NOT NULL CHECK (Price > 0), Cost DECIMAL(10,2) NOT NULL DEFAULT 0 CHECK (Cost >= 0), IsOnShelf BIT NOT NULL DEFAULT 1, SortNo INT NOT NULL DEFAULT 0 ); CREATE TABLE dbo.Member ( MemberPhone VARCHAR(20) PRIMARY KEY, MemberName NVARCHAR(20) NOT NULL, Points INT NOT NULL DEFAULT 0, TotalAmount DECIMAL(10,2) NOT NULL DEFAULT 0, CreatedAt DATETIME2(0) NOT NULL DEFAULT SYSDATETIME() );提示:凡是能存中文字段的列,一律用 NVARCHAR,字符串字面量统一写成 N'大厅' 这种带 N 前缀的形式。这是避免中文变问号最有效的一步。
这里解释几个设计点,答辩时老师大概率会问到。EmpId 用 IDENTITY 代理主键而不是员工工号,因为工号属于业务编号,可能会调整格式,代理主键与业务解耦;EmpNo 单独加 UNIQUE 约束保证业务唯一即可。DineTable 的 SeatCount 用 CHECK 限制在 1 到 20 之间,防止录入 0 座或 99 座这种脏数据。EmpId 允许 NULL,表示当前桌台没有绑定服务员。Dish.Cost 是成本字段,用于后面算毛利报表,单独加 CHECK 不小于 0。
接着建订单、订单明细、支付流水三张核心交易表:
CREATE TABLE dbo.Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL UNIQUE, TableId INT NOT NULL, EmpId INT NOT NULL, MemberPhone VARCHAR(20) NULL, StatusCode TINYINT NOT NULL DEFAULT 0, OpenTime DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), CloseTime DATETIME2(0) NULL, Remark NVARCHAR(200) NULL, CONSTRAINT FK_Orders_DineTable FOREIGN KEY (TableId) REFERENCES dbo.DineTable(TableId), CONSTRAINT FK_Orders_Employee FOREIGN KEY (EmpId) REFERENCES dbo.Employee(EmpId) ); CREATE TABLE dbo.OrderItems ( OrderId INT NOT NULL, ItemNo INT NOT NULL, DishId INT NOT NULL, Quantity INT NOT NULL CHECK (Quantity > 0), UnitPrice DECIMAL(10,2) NOT NULL, Discount DECIMAL(4,3) NOT NULL DEFAULT 1, StatusCode TINYINT NOT NULL DEFAULT 0, CONSTRAINT PK_OrderItems PRIMARY KEY (OrderId, ItemNo), CONSTRAINT FK_OrderItems_Orders FOREIGN KEY (OrderId) REFERENCES dbo.Orders(OrderId), CONSTRAINT FK_OrderItems_Dish FOREIGN KEY (DishId) REFERENCES dbo.Dish(DishId) ); CREATE TABLE dbo.Payment ( PaymentId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL, PayMethod CHAR(2) NOT NULL DEFAULT '01', PaidAmount DECIMAL(10,2) NOT NULL CHECK (PaidAmount > 0), PaidAt DATETIME2(0) NOT NULL DEFAULT SYSDATETIME(), EmpId INT NOT NULL, CONSTRAINT FK_Payment_Orders FOREIGN KEY (OrderNo) REFERENCES dbo.Orders(OrderNo) );OrderItems 用 OrderId + ItemNo 做复合主键,ItemNo 是同一次点单内部的行号,由程序或存储过程生成。这么设计是为了避免单独加一个无意义的明细流水号,同时保证同一个订单内不会出现重复行。
OrderItems.UnitPrice 是下单时的价格快照,不是实时 JOIN Dish 表拿当前价格。这是账单系统的关键设计:菜品后来改价了,历史订单的金额不能被顶掉。计算订单金额时一律用 OrderItem.UnitPrice 乘以 Quantity,这个意识要从课程设计阶段就养好。
Payment 表直接引用 OrderNo 而不是 OrderId,原因是支付流水面向订单号展示更直观,而 Orders.OrderNo 有 UNIQUE 约束,可以被外键引用。PayMethod 用 CHAR(2) 存两位代码,01 现金、02 电子支付、03 会员卡,避免存中文导致统计口径不统一。
2.3 用 Schema 把业务表、统计表、归档表分开
表都建在 dbo 下也能跑,但课程设计要体现“设计感”,我建议从第一天就拆 Schema。最常见做法是建三个 Schema:dbo 放业务表、rpt 放统计报表表、arc 放历史归档表。
CREATE SCHEMA rpt; CREATE SCHEMA arc; GO CREATE TABLE rpt.DishSalesDaily ( StatDate DATE NOT NULL, DishId INT NOT NULL, SoldQty INT NOT NULL DEFAULT 0, SoldAmount DECIMAL(10,2) NOT NULL DEFAULT 0, CONSTRAINT PK_DishSalesDaily PRIMARY KEY (StatDate, DishId) );如果表已经建在 dbo 里,也可以用 ALTER SCHEMA 转移:
ALTER SCHEMA rpt TRANSFER dbo.DishSalesDaily;Schema 拆分的实际收益在权限管理上体现。比如给收银员账号只授业务表的 SELECT、EXECUTE 权限,给店长账号授 rpt 的 SELECT 权限,语句可以精确到 Schema 粒度。课程设计汇报时,权限设计是很容易出彩的一页,而 Schema 就是它的支撑结构。后面第 3 章写触发器时,统计表直接落在 rpt 下,语义上比混在业务表里清晰得多。
3. 存储过程、事务与触发器:让“点单、结账、统计”自动化
3.1 开台点餐的存储过程:一个事务同时更新三张业务表
点餐动作表面上只是插入订单和明细,实际上还牵动桌台状态。常见做法是把“开台 + 点餐”合并成一个存储过程,减少客户端往返。先定义表值参数类型,用于一次传入多道菜:
CREATE TYPE dbo.OrderItemList AS TABLE ( DishId INT NOT NULL, Quantity INT NOT NULL );表值参数的作用是让应用程序把“这桌点了哪几道菜、各几份”作为一个集合传给存储过程,避免用逗号拼接字符串再拆分的土办法。接着写主流程:
CREATE PROCEDURE dbo.usp_CreateOrder @TableId INT, @EmpId INT, @Items dbo.OrderItemList READONLY, @OrderId INT OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @TableStatus TINYINT; SELECT @TableStatus = StatusCode FROM dbo.DineTable WITH (UPDLOCK, ROWLOCK) WHERE TableId = @TableId; IF @TableStatus IS NULL THROW 50001, N'桌台不存在', 1; IF @TableStatus <> 0 THROW 50002, N'该桌已开台,不能重复下单', 1; BEGIN TRAN; BEGIN TRY INSERT INTO dbo.Orders (OrderNo, TableId, EmpId, StatusCode, OpenTime) VALUES ( CONVERT(VARCHAR(32), GETDATE(), 112) + RIGHT('0000' + CAST(@TableId AS VARCHAR(4)), 4), @TableId, @EmpId, 0, SYSDATETIME() ); SET @OrderId = SCOPE_IDENTITY(); INSERT INTO dbo.OrderItems (OrderId, ItemNo, DishId, Quantity, UnitPrice, Discount) SELECT @OrderId, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)), i.DishId, i.Quantity, d.Price, 1 FROM @Items i JOIN dbo.Dish d ON d.DishId = i.DishId; UPDATE dbo.DineTable SET StatusCode = 1 WHERE TableId = @TableId; COMMIT; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; THROW; END CATCH; END;这个存储过程的核心思想是:点单、明细、桌台状态三件事要么全部成功,要么全部回滚。最容易被忽略的是开头的WITH (UPDLOCK, ROWLOCK),它先把桌台那一行锁住,再检查状态。如果没有这把锁,两个服务员在同一毫秒内对同一空桌下单,两边都读到“空闲”,然后都插入订单,产生两张订单占用一张桌的逻辑错误。加了 UPDLOCK 后,第二个会话会等待第一个事务结束,再读到状态变成“已开台”,直接触发 50002 错误。
参数说明:@TableId 传桌台 ID;@EmpId 传当前服务员 ID;@Items 传餐品明细集合;@OrderId 是 OUTPUT 参数,返回新生成的订单 ID。OrderNo 用日期加桌号拼接,格式类似 20250107120001,虽然简单,但在单店场景足够唯一,也方便人工识别。实际调用示例:
DECLARE @items dbo.OrderItemList; INSERT INTO @items (DishId, Quantity) VALUES (1, 2), (2, 1); DECLARE @newOrderId INT; EXEC dbo.usp_CreateOrder @TableId = 1, @EmpId = 2, @Items = @items, @OrderId = @newOrderId OUTPUT; SELECT @newOrderId;注意:THROW 语句是 SQL Server 2012 及以上版本支持的写法,课程设计通常用的都是这个版本以上,可以放心用。它比旧版 RAISERROR 更简洁,错误号可以自定义成 50001 这种业务错误段。
3.2 结账存储过程:并发重复买单时,UPDLOCK 和状态检查缺一不可
结账是点餐系统里最容易出并发问题的动作。两个收银员同时点“结账”,如果流程只是先 SELECT 状态再 UPDATE,两边都可能读到“未结账”,产生两笔支付流水。解决思路和开台一样,先锁行再判断:
CREATE PROCEDURE dbo.usp_Checkout @OrderId INT, @PayMethod CHAR(2), @EmpId INT, @MemberPhone VARCHAR(20) = NULL, @OrderNo VARCHAR(32) OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @CurStatus TINYINT; DECLARE @TableId INT; DECLARE @RealAmount DECIMAL(10,2); SELECT @CurStatus = StatusCode, @TableId = TableId, @OrderNo = OrderNo FROM dbo.Orders WITH (UPDLOCK, ROWLOCK) WHERE OrderId = @OrderId; IF @CurStatus IS NULL THROW 50003, N'订单不存在', 1; IF @CurStatus = 3 THROW 50004, N'订单已结账,请勿重复操作', 1; IF @CurStatus = 4 THROW 50005, N'订单已取消,不能结账', 1; SELECT @RealAmount = SUM(UnitPrice * Quantity * Discount) FROM dbo.OrderItems WHERE OrderId = @OrderId AND StatusCode = 0; BEGIN TRAN; BEGIN TRY INSERT INTO dbo.Payment (OrderNo, PayMethod, PaidAmount, PaidAt, EmpId) VALUES (@OrderNo, @PayMethod, @RealAmount, SYSDATETIME(), @EmpId); UPDATE dbo.Orders SET StatusCode = 3, CloseTime = SYSDATETIME() WHERE OrderId = @OrderId; UPDATE dbo.DineTable SET StatusCode = 2 WHERE TableId = @TableId; IF @MemberPhone IS NOT NULL UPDATE dbo.Member SET Points = Points + CAST(@RealAmount AS INT), TotalAmount = TotalAmount + @RealAmount WHERE MemberPhone = @MemberPhone; COMMIT; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK; THROW; END CATCH; END;订单金额不是从 Orders 表里的某个冗余字段读取,而是现场 SUM OrderItems 的实收金额,这样永远和明细一致,不会出现“明细改了、总账忘了改”的脏数据。会员积分、累计消费和支付流水放在同一个事务里,任何一步失败整体回滚,避免用户付了款但积分没加上。
参数说明:@OrderId 是订单主键;@PayMethod 是支付方式代码;@EmpId 是收银员;@MemberPhone 可选,传入则同步更新会员积分;@OrderNo 是 OUTPUT 参数,返回单号,供前端打印小票。调用示例:
DECLARE @orderNo VARCHAR(32); EXEC dbo.usp_Checkout @OrderId = 1001, @PayMethod = '02', @EmpId = 3, @MemberPhone = N'13800138000', @OrderNo = @orderNo OUTPUT; SELECT @orderNo;3.3 触发器自动累计日销量:MERGE 写法与双写成本
统计报表是课程设计的加分项。与其让业务系统每次临时去 SUM OrderItems,不如用一张日维度统计表,把当天的销量累计起来。这里用触发器+MERGE 的常见做法:
CREATE TRIGGER dbo.trg_DishSalesDaily_AfterInsert ON dbo.OrderItems AFTER INSERT AS BEGIN SET NOCOUNT ON; MERGE rpt.DishSalesDaily AS t USING ( SELECT CAST(SYSDATETIME() AS DATE) AS StatDate, DishId, SUM(Quantity) AS Qty, SUM(Quantity * UnitPrice) AS Amt FROM inserted GROUP BY CAST(SYSDATETIME() AS DATE), DishId ) AS s ON (t.StatDate = s.StatDate AND t.DishId = s.DishId) WHEN MATCHED THEN UPDATE SET SoldQty = t.SoldQty + s.Qty, SoldAmount = t.SoldAmount + s.Amt WHEN NOT MATCHED THEN INSERT (StatDate, DishId, SoldQty, SoldAmount) VALUES (s.StatDate, s.DishId, s.Qty, s.Amt); END;这段触发器只响应 AFTER INSERT,也就是每次点菜写入明细后,自动把当天该菜品的销量和销售额累加到统计表。MERGE 的好处是同一道菜在同一天已经存在记录就做累加,不存在就插入新记录,一条语句解决两条分支。
这里有一个经典问题:为什么不用视图直接SELECT DishId, SUM(...) FROM OrderItems GROUP BY ...?因为明细表会持续膨胀,报表每次查询都全表扫描,数据量上来后性能很难看。日统计表牺牲了一点写入开销,换来报表查询恒定读取一行数据,是典型的空间换时间。课程设计里能讲清楚这个取舍,老师印象分会明显不一样。
但触发器也要付出“双写”的维护成本:统计表的数据是冗余出来的,一旦触发逻辑和业务逻辑不一致,两边数据就会对不上。比如退菜时,明细状态改成 1,这套触发器不会自动把统计扣回去,所以退菜存储过程里必须同步对 rpt.DishSalesDaily 做反向 UPDATE。如果嫌麻烦,也可以退菜时直接重算当日统计,用一条 UPDATE 语句覆盖,保证最终一致。设计文档里把这一点写明白,比单纯贴一段触发器更有说服力。
4. SQL Server 点餐系统避坑清单:5 个翻车现场与排查方法
4.1 建表报错“order 附近有语法错误”:保留字与表名规范
现象:执行CREATE TABLE order (...)直接报Incorrect syntax near the keyword 'order',查资料才发现 ORDER 是 SQL Server 的保留字。
原因:ORDER 在 SQL 里用于 ORDER BY 排序从句,直接做表名会被解析器当成语法关键字,脚本根本走不到建表那一步。
解决:表名统一用复数或者加业务前缀,比如 Orders、OrderItems、DishOrder,这是最省事的方案。如果实在想叫 [Order],可以用方括号把标识符包起来,但这种写法每次查询都要带括号,容易漏,而且不利于代码规范。点餐系统里 Orders、OrderItems 这种命名既符合英语习惯,又天然避开保留字,建议直接采用。
4.2 中文全部变成问号:排序规则不一致和漏写 N 前缀
现象:插入“宫保鸡丁”后查询显示“???”,或者从程序写入的数据在 SSMS 里看是乱码,从 SSMS 插入的中文在程序里又显示异常。
原因:中文字符不能正确存入,通常有两个来源。一是建库时排序规则用了默认的 SQL_Latin1_General_CP1_CI_AS,不认中文字符;二是应用程序发 SQL 时用 VARCHAR 传中文,连接层没走 Unicode 编码,数据到数据库之前已经被转坏。
解决:建库时明确指定COLLATE Chinese_PRC_CI_AS,存中文的字段全部用 NVARCHAR,SQL 里的中文字面量统一带 N 前缀,比如N'宫保鸡丁'。程序端的连接参数也要检查字符编码设置,JDBC 连接串在 URL 后面加 characterEncoding=utf-8。这个坑最气人的地方在于它通常不是一上来就报错,而是等到报报表时才偶尔翻车。
4.3 IDENTITY 断号看着像表坏了:自增值不会回退
现象:删掉一批测试数据后,新插入的订单编号从 10001 开始,而不是从 1 继续;用 DELETE 清空表后依然从上次的值往后走。
原因:IDENTITY 是自增列,它的当前值只增不减。DELETE 删掉的是数据行,不是自增种子,所以新插入行为高编号是正常行为,不是表坏了。
解决:如果希望演示环境干净,有两种做法。没有外键引用的表可以直接TRUNCATE TABLE,它会重置自增种子;有外键引用的表不能 TRUNCATE,只能 DELETE 后执行DBCC CHECKIDENT('dbo.Orders', RESEED, 0)手动重置。课程设计阶段通常没必要重置,答辩时能解释清楚“IDENTITY 不回收”反而显得底子扎实。
4.4 金额用 float 算出一串小数:精确数值必须用 DECIMAL
现象:计算订单总价时,SUM(UnitPrice * Quantity)的结果里出现11.00000004这种一长串小数,打印小票怎么都对不上账。
原因:float 是近似数值类型,底层按二进制科学计数法存储,0.1 这种十进制小数在二进制里是无限循环,运算结果自然带误差。金额计算对精度极其敏感,不能用近似类型。
解决:所有价格、成本、实收金额字段统一用DECIMAL(10,2)或DECIMAL(12,2),折扣字段用DECIMAL(4,3)存 0.85 这种值,不要用 float,也不要用 real。记住一个原则:凡是和“元”“角”“分”有关的字段,一律不进 float 家族。
4.5 两个窗口同时结账:卡死、报错还是重复支付
现象:打开两个查询窗口模拟两个收银台,同时对同一个订单执行结账存储过程。结果要么一个窗口一直等在那边,要么第二个窗口报“订单已结账,请勿重复操作”,看起来像是程序卡死。
原因:如果结账存储过程先 SELECT 状态再 UPDATE,两个连接可能同时读到“未结账”,随后都在等对方的行锁,形成锁等待甚至死锁。SQL Server 检测到死锁后会自动选择牺牲一个事务,报错 1205,应用层如果不处理,用户看到的就是“卡了一下然后报错”。
解决:在查询订单状态时就用WITH (UPDLOCK, ROWLOCK)先把目标行锁住,第二个会话只能等第一个事务提交后才能读到新状态,随即触发业务错误。事务里加上SET XACT_ABORT ON,出现错误时自动回滚。死锁发生时,SQL Server 会牺牲其中一个事务并抛 1205,客户端捕获这个错误号后重试一次即可,课程设计里把重试逻辑写在调用端就够了。
5. 让数据库“证明自己”:索引体检、造数对比与一个收尾习惯
5.1 一条 SQL 做索引体检:哪些索引在吃白饭
建完表、写完存储过程后,还要让索引和性能优化有依据。用动态管理视图查看索引实际使用次数:
SELECT OBJECT_NAME(i.object_id) AS TableName, i.name AS IndexName, i.type_desc, s.user_seeks, s.user_scans, s.user_lookups, s.user_updates FROM sys.indexes i LEFT JOIN sys.dm_db_index_usage_stats s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() WHERE i.object_id > 100 ORDER BY OBJECT_NAME(i.object_id), i.index_id;user_seeks 是索引查找次数,user_scans 是索引或表扫描次数,user_updates 是写操作对索引的维护次数。如果一个索引 user_scans 很高、user_seeks 很低,说明查询经常走全量扫描,这个索引价值不大;如果 user_updates 也很高,说明它每次写入都在付维护成本,基本可以判定为冗余索引,该删就删。
注意:这个 DMV 的统计是从 SQL Server 实例启动后才开始累计的,所以要先跑几遍真实的点单、结账、报表查询,再来看数据,否则结果没有参考意义。
5.2 造 3000 条订单并对比报表:缺索引的代价一目了然
索引体检是静态观察,要动态证明索引的价值,最好造一批真实分布的数据。下面这段脚本造 3000 条历史订单,桌号和员工循环分布,时间往前推 3000 小时:
;WITH Nums AS ( SELECT TOP 3000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM sys.all_columns a CROSS JOIN sys.all_columns b ) INSERT dbo.Orders (OrderNo, TableId, EmpId, StatusCode, OpenTime, CloseTime) SELECT N'T2025' + RIGHT('000000' + CAST(n AS VARCHAR(10)), 6), (n % 12) + 1, (n % 3) + 1, 3, DATEADD(HOUR, -n, GETDATE()), DATEADD(HOUR, -n, GETDATE()) FROM Nums;造数完成后,打开 SET STATISTICS IO ON,跑一版日报统计:
SET STATISTICS IO ON; SELECT CONVERT(DATE, o.OpenTime) AS BusinessDate, COUNT(DISTINCT o.OrderId) AS OrderCount, SUM(oi.Quantity * oi.UnitPrice) AS SalesAmount FROM dbo.Orders o JOIN dbo.OrderItems oi ON oi.OrderId = o.OrderId WHERE o.StatusCode = 3 GROUP BY CONVERT(DATE, o.OpenTime); SET STATISTICS IO OFF;消息栏会输出类似“表 Orders。扫描计数 1,逻辑读取 128 次”的信息。这时候再执行一条CREATE INDEX IX_Orders_Status_OpenTime ON dbo.Orders(StatusCode, OpenTime),重新跑报表,逻辑读取大幅下降,就是答辩现场最直观的性能对比实验。我现在的习惯是:凡是写了事务的存储过程,一定先在两个会话里并发跑一遍,再用 SET STATISTICS IO 对比一下带不带索引的代价。这门课的价值不在表建得多漂亮,而在于你能不能把“为什么这样设计”讲清楚。希望帮到你。
本文还有配套的精品资源,点击获取