简介:本资源是一份完整的数据库课程设计报告,面向高校计算机、软件工程及信息管理类专业学生,聚焦银行管理系统这一典型业务场景的数据库设计与实现。报告系统覆盖需求分析、E-R模型构建、五张核心数据表(用户、银行卡、转账、贷款、还贷)的设计规范、字段类型与约束说明,以及C#与SQL Server 2008的技术选型依据,为课程设计提供从理论建模到落地实践的全流程参考。压缩包含1个2.91MB的Word文档(.doc),内容结构清晰,包含功能模块划分(管理员开户/销户/查询;用户存取款/贷款/转账/还贷等)、数据库概念与逻辑设计细节、SQL关系截图及程序流程图,便于理解事务处理、一致性保障与安全性设计要点。目前已有112人学习下载,适合作为数据库原理课程大作业范例、毕业设计参考或SQL实战能力提升的结构化学习材料。
1. 这不是Word模板套用作业:一份能跑通、能查账、能抗并发的银行管理系统数据库设计,到底要填满哪几块硬骨头?
“数据库课程设计报告——银行管理系统.doc”——光看标题,90%的学生第一反应是:找份往届范文,改改表名字段,凑够ER图+SQL脚本+3000字描述,交差。但真正跑过生产级银行类系统的人知道:这份文档若真要落地,它得扛住三件事——账户余额不能算错一分钱(事务隔离必须可验证)、柜台同时开5个窗口办业务不锁死(并发控制不能靠“等”)、凌晨批量结息时日志还能精准定位到某张存单(审计追踪必须可回溯)。这不是教科书里的“学生管理系统”,而是把ACID、索引策略、约束设计、日志结构全焊进业务逻辑里的实战沙盘。本文不讲PPT怎么排版,只拆解:从需求建模开始,如何用MSSQL Server 2019(兼容SQL Server 2016+)一砖一瓦垒出一个能被C# WinForms客户端真实连接、执行转账、查询流水、生成对账单的最小可行数据库骨架。重点不在“写报告”,而在“让报告里每行SQL都经得起BEGIN TRAN; UPDATE ...; ROLLBACK;的反复锤打”。
2. 从客户开户到跨行转账:用实体关系建模锁定核心业务边界
银行系统不是“用户+账户+交易”三个表就能糊弄过去的。真实业务中,一个客户可能有多个证件类型(身份证/护照/港澳居民来往内地通行证),一张银行卡背后关联着主账户、子账户、保证金账户,一笔转账可能触发手续费计算、反洗钱标记、实时余额校验。建模第一步,不是打开SSMS建表,而是用带业务语义的ER图划清责任边界。
2.1 客户与账户:为什么“客户表”不能直接存身份证号?
常见错误:Customers(ID, Name, IDCardNo, Phone)—— 看似简洁,但违反三大现实约束:
- 证件唯一性 ≠ 客户唯一性:同一人持身份证+护照在不同网点开户,应为同一客户;
- 证件有效期需独立管理:身份证过期不影响账户存续,但影响新业务办理;
- 客户信息变更需留痕:姓名修改必须记录历史版本,而非覆盖原值。
✅ 正确做法:拆分为Customers(客户主档)、CustomerIdentifications(证件档案)、CustomerHistory(变更日志)三张表:
-- 客户主档:仅存不可变标识和状态 CREATE TABLE Customers ( CustomerID UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), Status CHAR(1) NOT NULL CHECK (Status IN ('A','I','F')), -- A=Active, I=Inactive, F=Fraud CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(), CreatedBy NVARCHAR(50) NOT NULL ); -- 证件档案:一对多,支持多证件、多有效期 CREATE TABLE CustomerIdentifications ( ID UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), CustomerID UNIQUEIDENTIFIER NOT NULL, IDType VARCHAR(20) NOT NULL CHECK (IDType IN ('IDCARD','PASSPORT','HKMACAU')), IDNumber VARCHAR(50) NOT NULL, ValidFrom DATE NOT NULL, ValidTo DATE NULL, IsPrimary BIT NOT NULL DEFAULT 0, FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE ); -- 变更日志:每次姓名/地址修改都插入新行,不更新旧数据 CREATE TABLE CustomerHistory ( LogID BIGINT IDENTITY(1,1) PRIMARY KEY, CustomerID UNIQUEIDENTIFIER NOT NULL, FieldChanged VARCHAR(30) NOT NULL, -- 'Name', 'Address' OldValue NVARCHAR(200), NewValue NVARCHAR(200), ChangedAt DATETIME2 NOT NULL DEFAULT GETDATE(), ChangedBy NVARCHAR(50) NOT NULL, FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) );参数说明:
UNIQUEIDENTIFIER用NEWID()而非NEWSEQUENTIALID()—— 银行系统对插入性能敏感度低于对分布式ID唯一性的要求;Status字段用单字符枚举而非外键表,避免简单状态查询引发额外JOIN;CustomerHistory不设外键约束到Customers的ON DELETE CASCADE,因历史记录需永久保留,即使客户注销。
2.2 账户体系:为什么“账户表”必须区分产品类型与持有关系?
学生作业常写Accounts(AccountID, CustomerID, Balance, Currency),但实际中:
- 同一客户可持有活期、定期、理财、保证金四类账户,每类利率规则、计息方式、冻结逻辑完全不同;
- 一张借记卡可能关联多个子账户(如美元户、人民币户),而信用卡是独立授信额度;
- 账户可被多人共有(联名账户),也可由机构代管(托管账户)。
✅ 正确分层:AccountProducts(产品定义)→Accounts(账户实例)→AccountHolders(持有关系)
-- 产品定义:预置所有账户类型规则 CREATE TABLE AccountProducts ( ProductCode CHAR(4) PRIMARY KEY, -- 'CHK'活期, 'SAV'储蓄, 'CD'大额存单 ProductName NVARCHAR(50) NOT NULL, InterestRate DECIMAL(5,4) DEFAULT 0.0, MinBalance DECIMAL(18,2) DEFAULT 0.0, IsInterestBearing BIT NOT NULL DEFAULT 0 ); -- 账户实例:绑定产品,存储余额与状态 CREATE TABLE Accounts ( AccountID VARCHAR(20) PRIMARY KEY, -- 银行卡号/存单号,业务主键 ProductCode CHAR(4) NOT NULL, Currency CHAR(3) NOT NULL DEFAULT 'CNY', Balance DECIMAL(18,2) NOT NULL DEFAULT 0.0, Status CHAR(1) NOT NULL CHECK (Status IN ('O','C','F','L')), -- O=Open, C=Closed, F=Frozen, L=Locked OpenedAt DATETIME2 NOT NULL DEFAULT GETDATE(), FOREIGN KEY (ProductCode) REFERENCES AccountProducts(ProductCode) ); -- 持有关系:支持联名、代理、托管 CREATE TABLE AccountHolders ( AccountID VARCHAR(20) NOT NULL, CustomerID UNIQUEIDENTIFIER NOT NULL, HolderType CHAR(1) NOT NULL CHECK (HolderType IN ('P','A','T')), -- P=Primary, A=Additional, T=Trustee SharePercent DECIMAL(5,2) NULL CHECK (SharePercent BETWEEN 0 AND 100), PRIMARY KEY (AccountID, CustomerID), FOREIGN KEY (AccountID) REFERENCES Accounts(AccountID) ON DELETE CASCADE, FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) );逻辑说明:
Accounts.AccountID用业务主键(如银行卡号)而非自增ID,因对外交互(柜面、网银、对账文件)均以账号为唯一标识;AccountHolders设复合主键并启用ON DELETE CASCADE,确保删除客户时自动清理其名下所有持有关系,避免孤儿记录;SharePercent允许NULL,因托管账户无需分配份额。
2.3 交易流水:为什么“交易表”必须分离动作与结果?
学生常把转账写成一条UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID='123',但这埋下三颗雷:
- 无法追溯资金去向:只知道A账户扣了100,不知这100进了哪个B账户;
- 无法支持冲正:若B账户入账失败,A已扣款,无依据回滚;
- 无法满足监管报送:央行要求每笔交易含交易对手、渠道、设备号、风控标记。
✅ 正确设计:Transactions(交易主档) +TransactionEntries(分录明细) +TransactionAudit(操作日志)
-- 交易主档:全局唯一交易号,记录业务动作 CREATE TABLE Transactions ( TransactionID VARCHAR(32) PRIMARY KEY, -- 格式:YYYYMMDDHHMMSSSSS+随机码 TransactionType CHAR(3) NOT NULL CHECK (TransactionType IN ('TRF','WDR','DEP','FEE')), Channel VARCHAR(20) NOT NULL, -- 'COUNTER','ATM','MOBILE','API' TerminalID VARCHAR(50) NULL, -- 柜台号/ATM编号/API网关ID RiskLevel TINYINT NOT NULL DEFAULT 1 CHECK (RiskLevel BETWEEN 1 AND 5), CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(), Status CHAR(1) NOT NULL CHECK (Status IN ('P','S','F','R')), -- P=Processing, S=Success, F=Failed, R=Reversed ReversedBy VARCHAR(32) NULL -- 关联原交易ID,用于冲正链路 ); -- 分录明细:每笔交易至少两行(借贷平衡),支持多边交易 CREATE TABLE TransactionEntries ( EntryID BIGINT IDENTITY(1,1) PRIMARY KEY, TransactionID VARCHAR(32) NOT NULL, AccountID VARCHAR(20) NOT NULL, Amount DECIMAL(18,2) NOT NULL, DebitCredit CHAR(1) NOT NULL CHECK (DebitCredit IN ('D','C')), -- D=Debit, C=Credit Description NVARCHAR(100) NULL, FOREIGN KEY (TransactionID) REFERENCES Transactions(TransactionID) ON DELETE CASCADE, FOREIGN KEY (AccountID) REFERENCES Accounts(AccountID) ); -- 操作日志:记录谁、何时、在哪台机器上发起该交易 CREATE TABLE TransactionAudit ( AuditID BIGINT IDENTITY(1,1) PRIMARY KEY, TransactionID VARCHAR(32) NOT NULL, OperatorID NVARCHAR(50) NOT NULL, -- 柜员号/系统账号 WorkstationIP VARCHAR(15) NULL, ClientAppVersion VARCHAR(20) NULL, CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(), FOREIGN KEY (TransactionID) REFERENCES Transactions(TransactionID) ON DELETE CASCADE );关键参数解释:
Transactions.TransactionID采用时间戳+随机码组合,避免自增ID暴露业务量;TransactionEntries.DebitCredit字段强制借贷平衡校验(应用层需保证总和为0);TransactionAudit表不设外键到Operators表,因操作员信息可能变更,日志需固化当时上下文。
3. 让C#客户端真正连上、查到、转成功:MSSQL连接与事务控制实操
设计再完美,若C#代码连不上库、读不到数据、转账中途崩溃,整套设计就是废纸。本节直击WinForms项目中最常翻车的三个环节:连接字符串安全配置、事务嵌套陷阱、高并发下的死锁规避。
3.1 连接字符串:为什么不能写死密码?如何用Windows身份验证绕过明文风险?
学生项目常写:"Server=localhost;Database=BankDB;User Id=sa;Password=123456;"—— 这等于把保险柜钥匙贴在门上。生产环境必须禁用SQL Server认证,改用Windows集成认证(Integrated Security)。
✅ 正确做法:在开发机加入域或本地组,用Visual Studio以当前Windows用户身份运行程序,并配置连接字符串:
// C# WinForms 中获取连接字符串(推荐放在 App.config 或加密配置文件中) string connectionString = @"Data Source=YOUR-SQL-SERVER\INSTANCE;Initial Catalog=BankDB;Integrated Security=true;Connect Timeout=30;";注意:若必须使用SQL Server认证(如测试环境),绝不可硬编码密码。应使用
SqlCredential类动态构造:
var credential = new SqlCredential( new SecureString(), // 用户名(明文) new SecureString() // 密码(SecureString封装) ); // 实际中需从受保护密钥库读取,此处仅为示意 using (var conn = new SqlConnection(connectionString, credential)) { conn.Open(); // ... }3.2 转账事务:为什么SqlTransaction必须显式Commit(),且不能依赖using自动释放?
常见错误代码:
using (var conn = new SqlConnection(connStr)) { conn.Open(); using (var tran = conn.BeginTransaction()) // 错!tran未Commit { var cmd1 = new SqlCommand("UPDATE Accounts SET Balance=Balance-100 WHERE AccountID=@from", conn, tran); cmd1.Parameters.AddWithValue("@from", "123"); cmd1.ExecuteNonQuery(); var cmd2 = new SqlCommand("UPDATE Accounts SET Balance=Balance+100 WHERE AccountID=@to", conn, tran); cmd2.Parameters.AddWithValue("@to", "456"); cmd2.ExecuteNonQuery(); // 忘记 tran.Commit()! } // tran.Dispose() 会 Rollback,但开发者以为已提交 }✅ 正确模式:try-catch-finally显式控制,且finally中检查tran != null再Dispose
SqlConnection conn = null; SqlTransaction tran = null; try { conn = new SqlConnection(connStr); conn.Open(); tran = conn.BeginTransaction(); var cmd1 = new SqlCommand("UPDATE Accounts SET Balance=Balance-@amt WHERE AccountID=@from", conn, tran); cmd1.Parameters.AddWithValue("@amt", 100m); cmd1.Parameters.AddWithValue("@from", "123"); int rows1 = cmd1.ExecuteNonQuery(); if (rows1 == 0) throw new InvalidOperationException("转出账户不存在"); var cmd2 = new SqlCommand("UPDATE Accounts SET Balance=Balance+@amt WHERE AccountID=@to", conn, tran); cmd2.Parameters.AddWithValue("@amt", 100m); cmd2.Parameters.AddWithValue("@to", "456"); int rows2 = cmd2.ExecuteNonQuery(); if (rows2 == 0) throw new InvalidOperationException("转入账户不存在"); // 关键:必须显式Commit tran.Commit(); } catch (Exception ex) { // 记录错误日志 Logger.Error(ex, "转账失败"); if (tran != null) tran.Rollback(); throw; // 重新抛出,让UI层处理 } finally { if (tran != null) tran.Dispose(); // 安全释放 if (conn != null && conn.State == ConnectionState.Open) conn.Close(); }血泪经验:
SqlTransaction对象本身不持有连接,Dispose()仅释放事务资源,不会关闭连接。务必在finally中单独处理连接状态。
3.3 并发优化:为什么UPDLOCK比WITH (NOLOCK)更安全?如何用sp_getapplock防重复提交?
当两个柜员同时操作同一账户,SELECT Balance FROM Accounts WHERE AccountID='123'读到相同余额,各自扣款后UPDATE,导致超扣。NOLOCK(脏读)看似快,但会读到未提交数据,彻底破坏一致性。
✅ 正确方案:SELECT ... WITH (UPDLOCK, ROWLOCK)+ 应用层乐观锁
-- 在转账前锁定目标账户行,阻塞其他更新请求 SELECT Balance, Version FROM Accounts WITH (UPDLOCK, ROWLOCK) WHERE AccountID = @accountID;同时,在Accounts表增加Version列(ROWVERSION类型),每次UPDATE自动递增:
ALTER TABLE Accounts ADD Version ROWVERSION NOT NULL; -- UPDATE语句必须校验Version UPDATE Accounts SET Balance = Balance - @amt, Version = Version WHERE AccountID = @accountID AND Version = @expectedVersion; -- 若@@ROWCOUNT=0,说明Version已变,需重试玄学提示:
UPDLOCK在SELECT时即加更新锁,直到事务结束才释放,有效防止幻读;ROWLOCK避免锁升级为页锁或表锁;ROWVERSION比TIMESTAMP更语义清晰,且无需手动维护。
4. 避坑指南:那些让银行系统在验收前夜崩掉的5个高频雷区
数据库设计最怕的不是不会写SQL,而是踩中隐蔽的坑。以下是我带学生做课设时,每年必出现、且修复成本最高的5个问题,按现象→原因→解决逐条拆解:
4.1 现象:转账成功但余额不对,查日志发现两条UPDATE都执行了,却只有一条生效
原因:UPDATE Accounts SET Balance=Balance-100 WHERE AccountID='123'未加WHERE Balance >= 100校验,导致透支扣款(负余额)
解决:所有余额变更必须前置校验,且校验与更新在同一SQL中完成,避免竞态:
UPDATE Accounts SET Balance = Balance - 100 WHERE AccountID = '123' AND Balance >= 100; IF @@ROWCOUNT = 0 THROW 50000, '余额不足,转账失败', 1;4.2 现象:C#调用存储过程返回“对象已被释放”,但SQL Server Profiler显示执行成功
原因:存储过程中使用了SET NOCOUNT ON,导致C#SqlCommand.ExecuteNonQuery()误判结果集为空而提前释放连接
解决:在存储过程开头显式关闭NOCOUNT,或C#端改用ExecuteScalar()捕获返回值:
CREATE PROCEDURE TransferMoney @FromAccount VARCHAR(20), @ToAccount VARCHAR(20), @Amount DECIMAL(18,2) AS BEGIN SET NOCOUNT OFF; -- 关键!否则C#无法正确接收影响行数 BEGIN TRY BEGIN TRANSACTION; -- ... 转账逻辑 COMMIT TRANSACTION; SELECT 1 AS Result; -- 显式返回成功标识 END TRY BEGIN CATCH ROLLBACK TRANSACTION; SELECT 0 AS Result; END CATCH END4.3 现象:导出对账单时,SELECT * FROM Transactions WHERE CreatedAt > '2023-01-01'极慢,执行计划显示全表扫描
原因:CreatedAt列未建索引,且查询条件用字符串而非DATETIME2类型,导致隐式转换
解决:创建覆盖索引,并强制参数化查询:
-- 创建索引(含常用查询字段,避免Key Lookup) CREATE NONCLUSTERED INDEX IX_Transactions_CreatedAt_Status ON Transactions(CreatedAt, Status) INCLUDE (TransactionID, TransactionType, Channel); -- C#中传参必须用DateTime类型,而非字符串 cmd.Parameters.Add("@date", SqlDbType.DateTime2).Value = new DateTime(2023, 1, 1);4.4 现象:批量导入客户数据时,INSERT INTO Customers ... SELECT ... FROM OPENROWSET报错“拒绝访问Excel文件”
原因:SQL Server默认禁用Ad Hoc Distributed Queries,且Excel驱动需32/64位匹配
解决:启用高级选项并指定驱动(推荐改用CSV+BULK INSERT):
-- 启用Ad Hoc查询(仅开发环境) EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'Ad Hoc Distributed Queries', 1; RECONFIGURE; -- 但更稳妥:用C#读取Excel,批量`SqlBulkCopy` var bulk = new SqlBulkCopy(conn) { DestinationTableName = "Customers" }; bulk.WriteToServer(dataTable); // dataTable已预处理为强类型4.5 现象:部署到老师电脑后,所有日期函数(GETDATE())返回北京时间,但要求按东八区标准时间
原因:SQL Server时区由操作系统决定,GETDATE()返回服务器本地时间,若服务器时区非CST则偏差
解决:统一使用SYSDATETIMEOFFSET()+AT TIME ZONE标准化:
-- 存储过程内统一用此获取标准时间 DECLARE @Now DATETIME2 = SYSDATETIMEOFFSET() AT TIME ZONE 'China Standard Time'; INSERT INTO Transactions (CreatedAt, ...) VALUES (@Now, ...);提示:
AT TIME ZONE在SQL Server 2016+可用,若用2014及以下,需用GETUTCDATE()+ 手动加8小时,但需考虑夏令时——故强烈建议升级到2016+。
5. 用日志分析反推设计缺陷:从MSSQL日志里揪出隐藏的性能杀手
很多同学做完课设就扔,但真正的工程师会用SQL Server自带的日志分析能力,把“能跑”变成“跑得稳”。本节教你三招:用fn_dblog()查事务细节、用sys.dm_exec_query_stats揪慢SQL、用扩展事件(Extended Events)捕获死锁图——全部基于MSSQL原生工具,无需第三方软件。
5.1 查事务日志:确认每一笔转账是否真的原子提交?
当转账后余额异常,别急着改代码,先查日志确认SQL Server底层是否执行成功:
-- 查询最近1小时内所有UPDATE操作(需db_owner权限) SELECT [Current LSN], [Operation], [Context], [Transaction ID], [Begin Time], [End Time], [SPID], [Description] FROM fn_dblog(NULL, NULL) WHERE [Operation] IN ('LOP_BEGIN_XACT', 'LOP_COMMIT_XACT', 'LOP_ABORT_XACT') AND [Begin Time] > DATEADD(HOUR, -1, GETDATE()) ORDER BY [Begin Time] DESC;解读技巧:找到对应转账的
Transaction ID,再查该ID下所有LOP_MODIFY_ROW操作,确认Accounts表的Balance字段是否被正确修改两次(一减一加);若只有一次,说明事务中途ROLLBACK,需检查C#代码中的异常捕获逻辑。
5.2 捕获慢SQL:用DMV定位拖垮系统的查询
学生常抱怨“系统越来越慢”,却不知罪魁祸首可能是某次忘记加索引的SELECT * FROM Transactions:
-- 查CPU耗时Top 10的查询(单位:微秒) SELECT TOP 10 qs.execution_count, qs.total_logical_reads / qs.execution_count AS avg_logical_reads, qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time_ms, SUBSTRING(qt.text, (qs.statement_start_offset/2)+1, ((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text) ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS query_text FROM sys.dm_exec_query_stats AS qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt WHERE qs.last_execution_time > DATEADD(HOUR, -1, GETDATE()) ORDER BY qs.total_elapsed_time / qs.execution_count DESC;避坑提醒:
statement_start_offset和statement_end_offset是字节偏移,除以2才是字符位置;qt.text可能截断,若需完整SQL,用sys.dm_exec_query_plan(qs.plan_handle)提取执行计划XML。
5.3 可视化死锁:用扩展事件实时抓取死锁图
UPDLOCK虽好,但若两个事务按不同顺序锁定账户(如A先锁123再锁456,B先锁456再锁123),必然死锁。传统TRACEFLAG 1204输出文本难读,扩展事件更直观:
-- 创建扩展事件会话捕获死锁 CREATE EVENT SESSION [DeadlockCapture] ON SERVER ADD EVENT sqlserver.xml_deadlock_report ADD TARGET package0.event_file(SET filename=N'C:\XEvents\Deadlock.xel') WITH (STARTUP_STATE=ON); ALTER EVENT SESSION [DeadlockCapture] ON SERVER STATE = START; -- 查看死锁图(SSMS中右键.xel文件 → “查看目标数据” → 切换到“图表”页签)实战技巧:死锁图中红色进程是牺牲者(被kill),绿色是赢家;箭头方向表示资源等待关系;点击每个进程可看到其正在执行的SQL文本——据此可快速定位是哪段C#代码的事务顺序不合理,进而调整
SELECT顺序或加HOLDLOCK。
5.4 终极验证:用压力测试证明你的设计能扛住真实负载
课设验收常被问:“这个系统能支持多少人同时操作?” 答“理论上可以”不如跑一次sqlcmd压测:
# 模拟10个并发线程,各执行100次转账(需提前准备测试账户) for /l %i in (1,1,10) do ( start "" sqlcmd -S YOUR-SQL-SERVER\INSTANCE -d BankDB -Q " DECLARE @i INT = 0; WHILE @i < 100 BEGIN BEGIN TRY BEGIN TRAN; UPDATE Accounts SET Balance=Balance-1 WHERE AccountID='TEST001'; UPDATE Accounts SET Balance=Balance+1 WHERE AccountID='TEST002'; COMMIT TRAN; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRAN; END CATCH SET @i = @i + 1; END " )判断标准:观察SQL Server资源监视器(Resource Monitor)中
Page Life Expectancy(应>300秒)、Buffer Cache Hit Ratio(应>95%)、Lock Waits/sec(应接近0);若Batch Requests/sec持续低于500,说明设计存在瓶颈(如缺少索引、事务过长)。
我带过的最后一届学生,有个小组坚持用这套方法:先画ER图,再写SQL建库,接着用C#写最小转账界面,然后跑日志分析查事务,最后用扩展事件抓死锁。他们交的不是一份Word文档,而是一个带README.md的GitHub仓库,里面包含可一键部署的SQL脚本、可编译的C#工程、以及三份截图:fn_dblog输出、慢查询TOP3、死锁图。老师当场说:“这已经不是课程设计,是小型项目交付。”
希望帮到你。
本文还有配套的精品资源,点击获取