资讯详情

SQL Server点餐系统课程设计:事务、存储过程与索引优化实践

📅 2026/10/9 12:52:42 | 华诺云谱 👁 阅读
SQL Server点餐系统课程设计:事务、存储过程与索引优化实践
简介一套基于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 状态机先于建表一张订单从开台到结账要经过哪几个状态很多同学一上来就建表建到一半发现订单和桌台状态分不清。我习惯先列状态机再回来设计表结构这样每张表需要哪些字段、哪些约束会清晰很多。点餐业务的核心状态有三组表状态字段取值含义OrdersStatusCode0 / 1 / 2 / 3 / 40 已开台、1 点餐中、2 已下单、3 已结账、4 已取消DineTableStatusCode0 / 1 / 20 空闲、1 已开台、2 待清洁OrderItemsStatusCode0 / 1 / 20 正常、1 已退、2 已上菜订单状态和桌台状态必须分开这一点经常被忽略。坐下点菜时桌台已经算“已开台”但订单还处于“点餐中”结账完成后订单进入“已结账”桌台则进入“待清洁”两个状态的切换时机完全不同。如果把状态都塞在一张表里后面写存储过程时会出现大量互相矛盾的更新逻辑。状态设计完成后再看实体关系服务员开台、顾客点菜、后厨出菜、收银结账中间还有会员积分和支付流水。核心关系是 DineTable 一对多 OrdersOrders 一对多 OrderItemsDish 一对多 OrderItemsOrders 一对多 PaymentMember 一对多 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。最常见做法是建三个 Schemadbo 放业务表、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 传桌台 IDEmpId 传当前服务员 IDItems 传餐品明细集合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 N13800138000, 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存中文的字段全部用 NVARCHARSQL 里的中文字面量统一带 N 前缀比如N宫保鸡丁。程序端的连接参数也要检查字符编码设置JDBC 连接串在 URL 后面加 characterEncodingutf-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 NT2025 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 对比一下带不带索引的代价。这门课的价值不在表建得多漂亮而在于你能不能把“为什么这样设计”讲清楚。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑