SQL Server 与 T-SQL 深度学习笔记与实战记录(2025版)
无论你是刚刚接触数据库的新手,还是想要系统复习 T-SQL 及高级特性(如:复杂联表、窗口函数、存储过程调优、事务全隔离级别并发、游标细节、各类锁机制等)的开发者,这份学习笔记都能为你提供一条从入门到骨灰级的清晰路径。全篇几乎囊括了所有后端研发必考必用的核心知识点。
万字高阶目录
- 1. 深入理解 SQL Server 与 T-SQL 体系架构
- 2. DDL 进阶:数据库、表设计与 6 大精密约束
- 3. DML 深度实战:CRUD 与 高级数据操纵
- 4. DQL 王者:复杂查询、关联、子查询与窗口函数
- 5. T-SQL 编程精髓:变量、控制流、内置函数与游标
- 6. 模块化封装:视图 (View) 与 自定义函数 (UDF)
- 7. 性能核心:存储过程 (Stored Procedure) 与动态 SQL
- 8. 数据库的心脏:事务 (Transaction) 与 4大隔离级别(锁)
- 9. 隐形看门狗:触发器 (Trigger) 深度剖析
- 10. 极致调优:索引 (Index) 的聚簇本质与执行计划调优
- 11. 高阶查询艺术:CTE 递归、PIVOT 行列转换与 CROSS/OUTER APPLY
- 12. 终极数据操纵:MERGE(Upsert)与 临时表/表变量体系
- 13. 极客防身术:随心所欲的动态 SQL 与执行提示 (Query Hints)
1. 深入理解 SQL Server 与 T-SQL 体系架构
1.1 什么是 SQL Server 和 T-SQL?
- SQL Server 是微软开发的企业级关系型数据库管理系统(RDBMS)。它的网络层使用 TDS 协议,底层拥有独立的 SQLOS 操作系统用来管理内存、线程调度和 I/O,极大地榨干了 Windows Server 环境的物理机性能。
- T-SQL (Transact-SQL) 是微软在标准 SQL (ANSI/ISO) 基础上做出的图灵完备级别扩展。普通的 SQL 仅仅是“结构化查询指令”,而 T-SQL 是一门完整的编程语言,包含局部变量声明、
IF-ELSE/WHILE控制流、TRY...CATCH异常捕获、数学与字符串函数、以及专有的数据类型处理能力。
1.2 系统数据库简介
安装完毕后,引擎会自带 4 个核心的系统库(千万不能乱删):
- master:掌控全局的命门。记录了所有用户登录名、端点配置、系统内所有其他数据库的信息及物理文件位置。一旦顺坏引擎将无法启动。
- model:模具库。你在服务器上每次
CREATE DATABASE创建新库,引擎其实都是粗暴地把model库的内容复制一份过去。 - msdb:调度中心库。给 SQL Server Agent 服务使用,保存了所有的定时作业计划 (Jobs)、告警邮件和数据库备份还原的历史记录。
- tempdb:内存垃圾站。存放所有局部临时表 (
#Temp)、全局临时表 (##Temp),还有大量涉及GROUP BY或ORDER BY时内存不够用溢出到硬盘上的中间排序变量。它是高并发下最容易成为系统 I/O 瓶颈的组件。
2. DDL 进阶:数据库、表设计与 6 大精密约束
DDL (Data Definition Language) 是定义数据结构的语言,一旦执行就会被记录在系统表并真实分配硬盘簇分配。
2.1 数据库的高级创建方式
在生产环境,绝不能简单的 CREATE DATABASE foo,你需要指定 MDF(主数据文件)和 LDF(日志文件)的物理大小和自动增长率,防止磁盘空间突然被碎片撑爆:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 企业级正规建库脚本
CREATE DATABASE CorpERP
ON PRIMARY
(
NAME = 'CorpERP_Data', -- 逻辑名
FILENAME = 'D:\SQLData\CorpERP.mdf', -- 物理文件全路径
SIZE = 100MB, -- 初始大小
MAXSIZE = 500GB, -- 最大限制(防止写满整盘)
FILEGROWTH = 10% -- 自动扩容步长参数
)
LOG ON
(
NAME = 'CorpERP_Log',
FILENAME = 'E:\ SQLLogs\CorpERP_Log.ldf', -- 建议日志单独放到高速读写的纯净磁盘(如 SSD)
SIZE = 50MB,
MAXSIZE = 200GB,
FILEGROWTH = 50MB -- 日志暴涨时建议不要按百分比算,按固定兆数扩张减少碎片
);
GO
2.2 表的 6 大核弹级约束 (Constraints)的设计
创建健壮的架构,数据类型精准与约束严格缺一不可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
USE CorpERP;
GO
-- 1. 创建部门主表
CREATE TABLE Department (
-- [1] 主键约束 (PRIMARY KEY): 唯一 + 绝不为空 + 默认生成聚集物理索引树
-- [2] 标识/自增属性 (IDENTITY(起始值, 步长)): 让引擎自己接管分配独立ID
DeptId INT IDENTITY(1,1) PRIMARY KEY,
-- [3] 唯一键约束 (UNIQUE): 保证全表部门名不可重复,且自带一个唯一的非聚集索引
DeptName NVARCHAR(50) NOT NULL UNIQUE,
ManagerName NVARCHAR(50) NULL
);
-- 2. 创建高度约束的员工表
CREATE TABLE Employee (
EmpId BIGINT IDENTITY(1000, 1) PRIMARY KEY, -- 大企业建议用 BIGINT 防止越界
EmpName NVARCHAR(50) NOT NULL,
-- [4] 检查约束 (CHECK): 由引擎在插入/修改时把控取值范围逻辑,杜绝脏数据
Age TINYINT CHECK (Age >= 18 AND Age <= 65),
-- [5] 默认值约束 (DEFAULT): 插入时如果不给此列赋值,则启用默认配置
Gender CHAR(2) DEFAULT '男',
Salary DECIMAL(18, 2) NOT NULL, -- 金额:总长度18位,包含小数点后2位精确
JoinDate DATETIME2 DEFAULT CURRENT_TIMESTAMP, -- DATETIME2 精度比 DATETIME 要更高
IsDeleted BIT DEFAULT 0, -- 【软删除标记】BIT = 相当于布尔值 True/False
DeptId INT NOT NULL,
-- [6] 外键约束 (FOREIGN KEY): 数据血统关系最强硬的防线!
-- 如果有员工还挂着这个 DeptId,那么在部门表是绝对无法直接 Delete 删掉那个部门的。
CONSTRAINT FK_Employee_Department
FOREIGN KEY (DeptId)
REFERENCES Department(DeptId)
-- ON DELETE CASCADE -- 可选级联操作:如果填了这个,部门一旦被删,旗下所有关联员工被系统瞬间全部同时除名。
);
GO
2.3 修改现存结构的利器 (ALTER)
1
2
3
4
5
6
7
8
-- 比如:忘了加某列,现在在线无感增加
ALTER TABLE Employee ADD Email VARCHAR(100) NULL;
-- 比如:给某列加上 CHECK 约束
ALTER TABLE Employee ADD CONSTRAINT CK_Employee_Salary CHECK (Salary >= 3000.00);
-- 清理外键关联后,强制删除全表结构
-- DROP TABLE Employee;
3. DML 深度实战:CRUD 与 高级数据操纵
DML 负责所有涉及到数据进出的交互。
3.1 猛烈的数据充填:Insert 的所有形态
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 形态A:单条 / 极简多条批量插入 (SQL 2008 之后支持)
INSERT INTO Department (DeptName, ManagerName)
VALUES
('研发核心部', 'Tony'),
('财务风控部', 'Amy'),
('市场部', 'Bob');
-- 形态B:不指定特定列,按表字段顺序强行插入。如果表加列该写法必报错 (极不推荐!)
-- INSERT INTO Department VALUES ('行政部', 'Lily');
-- 形态C:查出某个表,并塞进一个已存在的结构同样的表 (INSERT INTO ... SELECT)
INSERT INTO EmployeeBackup (EmpName, Salary, DeptId)
SELECT EmpName, Salary, DeptId FROM Employee WHERE Age > 50;
-- 形态D:凭空创造新表并塞满数据 (SELECT ... INTO)。
-- 这种写法超级快,常用于制作一次性的数据报表镜像或跨库拉取日志:
SELECT * INTO Employee_Mirror_2025
FROM Employee
WHERE IsDeleted = 0;
3.2 冷酷的抹杀:Delete 与 Truncate
1
2
3
4
5
6
7
8
9
-- 条件删除 (慢,按日志逐行打标记伪删除然后物理覆盖)
DELETE FROM Employee WHERE EmpId = 1005 AND IsDeleted = 1;
-- 【面试常考】TRUNCATE 与 DELETE 对于全表清除的区别:
-- 1. Truncate 极其暴力,不写单独回滚事务日志,只释放数据页,瞬间清空几亿条数据。
-- 2. Truncate 不需要加上 WHERE。
-- 3. Truncate 会将 IDENTITY 自增标识的种子无情重置为 1 !
-- 4. 如果这张表被别人用来做外键参考了,由于引擎为了保护引用完整性,直接禁止你执行 Truncate。
-- TRUNCATE TABLE Employee_Mirror_2025;
3.3 精心雕琢的更新:Update
1
2
3
4
5
6
7
8
9
10
11
-- 普通的定向涨薪
UPDATE Employee
SET Salary = Salary * 1.05
WHERE DeptId = 1 AND Age > 30;
-- 【高阶联表更新】: 只通过另一张发来的核算表,去更新本体的薪资
UPDATE e
SET e.Salary = temp.NewSalary
FROM Employee AS e
INNER JOIN TempPayRaiseTable AS temp ON e.EmpId = temp.TargetEmpId
WHERE e.IsDeleted = 0;
4. DQL 王者:复杂查询、关联、子查询与窗口函数
DQL(Data Query Language)即 SELECT 家族,这是占日常工作 90% 的灵魂。
4.1 基础查询、条件、模糊匹配与重命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- AS 取别名:方便前端接收。如果不想要原名,可以用 '' 包裹。
SELECT EmpName AS '姓名', Salary AS '月薪' FROM Employee;
-- 强大的条件匹配与运算
SELECT * FROM Employee
WHERE Age BETWEEN 25 AND 35 -- 【范围】等同于 >=25 AND <=35
AND DeptId IN (1, 3, 5) -- 【集合】只取这个数组里面的
AND EmpName LIKE '张_%' -- 【模糊】姓张,且名字至少两个字(_代表必然有一个单字符),%代表后面的都无所谓。
AND ManagerName IS NOT NULL; -- 【NULL处理】SQL里比对 NULL 绝对不能用 = 号!只能用 IS
-- 排序组合分页与顶尖截取
-- 获取薪水排名前 10 的活人,如果在平薪的情况下再按照更年轻的进行排序
SELECT TOP 10 * FROM Employee
WHERE IsDeleted = 0
ORDER BY Salary DESC, Age ASC;
-- 【SQL 2012+ 分页新主宰:OFFSET FETCH】
-- 查询出第 3 页的数据(假设每页 20 条),它完全替代了以前丑陋的 ROW_NUMBER() 分页法
SELECT * FROM Employee
ORDER BY EmpId
OFFSET 40 ROWS -- 跳过前 40 行 (即抛弃第 1 到 2 页)
FETCH NEXT 20 ROWS ONLY; -- 再拿出紧接着的 20 行数据 (即这才是第 3 页)
4.2 聚合的分水岭:GROUP BY 与 HAVING
一旦你使用了聚合算子:COUNT(), SUM(), AVG(), MAX(), MIN(),你就进入了压缩统计模式。
死门规则:SELECT 后面如果有独立的列名(非聚合函数),那么这个列名必须出现在 GROUP BY 后面,否则立马语法报错。
1
2
3
4
5
6
7
8
9
10
11
12
-- 统计各个部门各种性别在职的人头数、平均薪水、以及该组合下挑出的最高的那笔工资
SELECT
DeptId,
Gender,
COUNT(1) AS 'TotalWorkers', -- COUNT(1) 和 COUNT(*) 性能在现代编译器下基本无异
AVG(Salary) AS 'AvgSalary',
MAX(Salary) AS 'TopSalaryOutThere'
FROM Employee
WHERE IsDeleted = 0 -- 【第一层过滤】WHERE,它是发生在 GROUP BY 【聚类之前】 剔除没用的散落死数据的
GROUP BY DeptId, Gender
HAVING COUNT(1) >= 5 -- 【第二层过滤】HAVING,它必须写在【聚类之后】,用于对刚刚压缩算好的总数进行进一步的高级筛选!
ORDER BY TotalWorkers DESC;
4.3 表与表的交织:JOIN(连接)家族全覆盖
商业系统不可能把所有字段平铺丢在一个表里(这完全违反了 3NF 数据库第三范式)。通过挂载的主外键拆分出去的表,查询时必须重构连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- A. 内连接 (INNER JOIN):门当户对。只会挑选在两张表中靠 ON 都能双花匹配的那些完美的数据带出来。如果某员工没有部门,他就被剔除丢弃。
SELECT e.EmpName, d.DeptName
FROM Employee e
INNER JOIN Department d ON e.DeptId = d.DeptId;
-- B. 左外连接 (LEFT JOIN / LEFT OUTER JOIN):以和为贵,以左为尊。
-- 强制保留左表(Employee)里的每一行!哪怕右表(Department)里的那个ID刚好被删了,他左边的名字也照样出来,只是这行关于右边 DeptName 的格子会显示 NULL 补齐。
SELECT e.EmpName, ISNULL(d.DeptName, '暂无安排') AS '所属部门'
FROM Employee e
LEFT JOIN Department d ON e.DeptId = d.DeptId;
-- C. 右外联 (RIGHT JOIN):与 LEFT JOIN 同理,反向保右不保左。
-- D. 全外连接 (FULL OUTER JOIN):极其罕见。不管你俩有没有配对上,所有的行孤儿和寡母全在这一张表里合体集结,没有的数据相互铺 NULL。
-- E. 交叉连接 (CROSS JOIN):更罕见。笛卡尔积乘法。不要带 ON。如果表 A 有 1000 人,表 B 有 10 个部门,那最后由于连出来的结果是 1000*10 = 10000 行纯组排列的数据爆炸。
4.4 子查询、CTE 与 分析利器: 窗口函数 (Window Functions)
传统的子查询写法有些丑陋且难读:
1
2
3
4
5
6
7
-- 需要知道哪些人赚的钱超过了财务部的平均工资
SELECT EmpName, Salary
FROM Employee
WHERE Salary > (
SELECT AVG(Salary) FROM Employee
WHERE DeptId = (SELECT DeptId FROM Department WHERE DeptName = '财务部')
);
利用 CTE (公共表表达式) 进行化繁为简的魔法: CTE 实质上是在当前批处理运行内存中定义的一张高可读的逻辑小表,你后面可以直接拿它像用真实表一样用。
1
2
3
4
5
6
7
8
9
10
11
WITH FinanceInfo AS (
SELECT DeptId FROM Department WHERE DeptName = '财务部'
),
AvgSalTable AS (
SELECT AVG(Salary) AS FinanceAvg FROM Employee
WHERE DeptId = (SELECT DeptId FROM FinanceInfo)
)
SELECT e.EmpName, e.Salary, a.FinanceAvg
FROM Employee e
CROSS JOIN AvgSalTable a
WHERE e.Salary > a.FinanceAvg;
【终极杀器】窗口函数 OVER (PARTITION BY):它让你鱼和熊掌兼得。
以前做聚合,比如算个平均分,所有人的行全给压缩折叠成一行结果返回了。
加上 OVER... 后,你不仅能在每行的旁边算出你要的那个区间的排位或平均分,还能保留所有详细行不被压缩消灭!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 查询所有人原本的薪水,并额外在每行的末尾,算出一个该行所处在本部门群体内部的前后大榜排名:
-- 这如果用老语法做可能要成百上千行的极其变态的临时表。
SELECT
EmpId,
EmpName,
DeptId,
Salary,
-- ROW_NUMBER():排名函数。
-- PARTITION BY DeptId 说的是针对部门切分开来算,ORDER BY 说的是根据薪水降序。
ROW_NUMBER() OVER(PARTITION BY DeptId ORDER BY Salary DESC) AS '本部门内薪资排名',
-- 额外在这附赠一个该组的所有人平均值,当做一个普通的列平铺在旁边作为对比!
AVG(Salary) OVER(PARTITION BY DeptId) AS '部门平均标杆'
FROM Employee
WHERE IsDeleted = 0;
5. T-SQL 编程精髓:变量、控制流、内置函数与游标 (Cursor)
5.1 批处理与内置标量函数
SQL Server 自带许多好用到哭的杀器函数。
- 字符串魔法:
1 2 3 4 5 6
SELECT LEN('HelloWorld'); -- 提取长度: 10 SELECT SUBSTRING('abcdefg', 2, 3); -- 万金油截取(起始点是按 1 计算的而不是 0): 返回 'bcd' SELECT CHARINDEX('T', 'HelloTWorld');-- 查找位置:返回 6 SELECT REPLACE('A,B,C', ',', '|'); -- 将逗号翻新成竖线:A|B|C -- 去除前后空格(2017+ 支持了组合去两端:TRIM,以前是 LTRIM 和 RTRIM 结合使用) SELECT TRIM(N' 这前后的空格都能被清掉 ');
- 日期魔法:
1 2 3 4 5 6 7
DECLARE @Now DATETIME = GETDATE(); -- 获取世界末端当前刻画 SELECT YEAR(@Now), MONTH(@Now), DAY(@Now); SELECT DATEADD(MONTH, 2, @Now); -- 增加 2 个月。你要减就是传 -2 SELECT DATEDIFF(DAY, '2020-01-01', @Now); -- 计算两个日期间跨越了多少天 -- 史上最难缠也是必用的格式化:FORMAT (Sql 2012+ 引入) SELECT FORMAT(GETDATE(), 'yyyy/MM/dd HH:mm:ss'); -- 完全等价于 C# 的 ToString 格式
- 转换魔法 (
CAST和CONVERT):1 2 3
SELECT '我有 ' + CAST(100 AS VARCHAR(10)) + ' 元'; -- CONVERT 在老式的风格下更强劲:支持第三个参数专门针对特定日期的转化规则风格码 SELECT CONVERT(NVARCHAR(20), GETDATE(), 120); -- 120是特定的 ODBC yyyy-mm-dd hh:mi:ss 格式
5.2 流程控制:IF-ELSE, WHILE 与 CASE 分流机制
T-SQL 完全支持条件编程体系。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DECLARE @TargetSalary INT =ROUND(RAND() * 20000, 0); -- 产生个随机薪资用来玩
PRINT '生成的随即目标是:' + CAST(@TargetSalary AS VARCHAR);
IF @TargetSalary < 5000
PRINT '这个数很小不影响大局。';
ELSE IF @TargetSalary BETWEEN 5000 AND 12000
BEGIN
PRINT '落入中等核算区间。';
-- 这里可以写很长的多行 UPDATE 语句等批量核算操作
END
ELSE
PRINT '超额阈值预警!';
-- ========== CASE WHEN ... THEN ... END : 分流数据改造王者 ==========
-- 它属于查询改造级的高手。用于在一行内部,直接对输出呈现的东西做条件翻转翻译变形。
SELECT EmpName, Salary,
CASE
WHEN Salary >= 20000 THEN '企业高管'
WHEN Salary >= 10000 AND Salary < 20000 THEN '中层骨干'
WHEN Salary < 10000 THEN '基层建设者'
ELSE '统计异常' -- 如果连上面的条件都没拦得住的最终安全垫
END AS '薪资地位等级'
FROM Employee;
5.3 在错误中涅槃:TRY…CATCH
和 C# 一样,T-SQL 的一切核心运行脚本都应该套在这个无情的结构中。它可以确保即便是发生了除 0、字段转换爆死、插入重复主键,存储过程也不会被粗暴弹红字打断抛给前端,而是交由你主动收网:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
BEGIN TRY
-- 声明并插入一个已经存在的名字导致主键约束大崩溃...
INSERT INTO Department (DeptId, DeptName) VALUES (1, '违规植入');
END TRY
BEGIN CATCH
-- 所有关于这次引擎报错的核心参数都在这这这
PRINT '出现预期外的极度严重操作异常...';
PRINT '错误级别 (Severity): ' + CAST(ERROR_SEVERITY() AS VARCHAR);
PRINT '错误代号 (Number): ' + CAST(ERROR_NUMBER() AS VARCHAR);
PRINT '错误所在的行数 (Line): ' + CAST(ERROR_LINE() AS VARCHAR);
PRINT '原汁原味的引擎错误红字文本: ' + ERROR_MESSAGE();
-- 下一步,你通常可以将这些详情强行偷偷再 INSERT 入你们公司的专用日志查错临时表
END CATCH
5.4 性能毒药还是神级救火兵?游标 (Cursor) 完全解析
无论是什么 ORM 都强调绝不使用游标。为什么?
正常情况下我们写 UPDATE Employee SET Age = Age + 1 是全量内存锁运算一扫而过(集合论操作 Set-based)。属于降维打击的高速模式。
但游标(Cursor)是一种死板的低级行为。它极其缓慢(由于强锁消耗、CPU高压跳转、一行一行的 Fetch 验证)。它是逼迫引擎脱离集合操作,而是把你指定的几千行记录取出来,通过循环控制“第一行、第二行”,一行一行的扫过去执行!
通常只有极其特殊无法使用集合完成(比如:调用外部特殊的 CLR 程序接口每行交互验证一遍,或极度错杂逻辑无法汇集为一条 JOIN UPDATE 时)才作为最后的底牌使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- 定义几个临时承载内存数据的杯子
DECLARE @CurrentEmpId BIGINT;
DECLARE @CurrentSalary DECIMAL(18,2);
-- 1. 【宣告游标】告诉数据库:给我划出一片内存专门给我的遍历使用
DECLARE emp_cursor CURSOR FOR
SELECT EmpId, Salary FROM Employee WHERE IsDeleted = 0;
-- 2. 【开启游标】正式在内存生成指向第一行头顶的一个虚拟只读指针
OPEN emp_cursor;
-- 3. 【提线推进】拉伸第一次:取出实际数据倾倒放进我们刚才声明好的杯子里准备计算
FETCH NEXT FROM emp_cursor INTO @CurrentEmpId, @CurrentSalary;
-- 4. 【开启死亡大循环】
-- 全局的状态变量 @@FETCH_STATUS 只要返回 0,意思就是:不仅刚才那句 FETCH 取到了货而且这底下还有活儿没干完
WHILE @@FETCH_STATUS = 0
BEGIN
-- 【在这写你的单行究极复杂的业务】
PRINT '当前扫描到 ID = ' + CAST(@CurrentEmpId AS VARCHAR) + ' 该兄弟工资有:' + CAST(@CurrentSalary AS VARCHAR);
-- ... 可以去别的什么遥远的系统拉取什么特殊的发票配置信息
-- ... IF 一下
-- 修改这单行特定的数据
-- 【最重要的:提线向前再走一格】!如果忘了这句,你就是死循环爆服!
FETCH NEXT FROM emp_cursor INTO @CurrentEmpId, @CurrentSalary;
END
-- 5. 【极其严重警告:必须扫尾关闭及释放】如果不执行这句不仅内存爆光整个锁全不放
CLOSE emp_cursor;
DEALLOCATE emp_cursor;
6. 模块化封装:视图 (View) 与 自定义函数 (UDF)
为了解决“每天有一百个地方写着同样的三百行超级连表 JOIN 和冗长聚合”,T-SQL 提供了优雅的复用解决方案。
6.1 视图 (View) 的美学
就像透过特定角度切割的一扇窗。它是一张虚拟存在的影子表,本身一般不在硬盘存储实体。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- CREATE VIEW:给这个虚假的表定制造型组合
CREATE VIEW v_SeniorEmployeesWithDept
AS
SELECT
e.EmpId, e.EmpName, e.Salary,
d.DeptName
FROM Employee e
INNER JOIN Department d ON e.DeptId = d.DeptId
WHERE e.Age > 40 AND e.IsDeleted = 0;
GO
-- 前端或是其他开发人员使用时,极其整洁。
-- 绝大多数情况下,不要去试图对视图底层执行 INSERT、UPDATE,哪怕有些特殊配置允许这样做。它只是一种用来查询的数据隔离防盗门。
SELECT * FROM v_SeniorEmployeesWithDept;
6.2 用户自定义函数 (UDF: User Defined Function) 深度解密
如果说视图是一张固定的窗,那么函数就是个可输入参数然后制造返回值的黑盒小机器。
分为两类:
- 标量函数 (Scalar Function):返回一枚标量单一值。(如字符串、整形、布尔时间等)。可以在所有
SELECT列内部和WHERE后方完美当做工具人拼合。 - 表值函数 (Table-Valued Function):它返回的一整个数据集就像表格一样的变量,甚至还能带内联运算,调用时把它直接放在
FROM OOOO()后面做连表或者展示。由于能接收参数灵活性极高,在老项目开发中被称为参数化大视图!
注意:UDF 内不能执行带有副作用的物理操作!(即里面不能出现任何导致数据真实落地的增删改,哪怕是创建真实的临时物理表都不行)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-- 标量函数范例:输入入职日期,给你吐出现实算出的“工龄”数字
CREATE FUNCTION fn_CalculateWorkYears (@JoinDate DATETIME)
RETURNS INT
AS
BEGIN
RETURN DATEDIFF(YEAR, @JoinDate, GETDATE());
END;
GO
-- 外部愉快调用: (在调用标量时引擎有个死规矩: 一般都得加上系统所属名 dbo.)
SELECT EmpName, dbo.fn_CalculateWorkYears(JoinDate) AS 'WorkYears' FROM Employee;
-------------------------------------------------------------------------------------------------
-- 内联表值函数 (TVF) 范例:接收一个薪资要求,只找出超过薪资的完整多列集合结构返回
CREATE FUNCTION fn_GetHighPaidWorkers (@Threshold DECIMAL(18,2))
RETURNS TABLE
AS
RETURN
(
SELECT EmpId, EmpName, Salary
FROM Employee
WHERE Salary >= @Threshold AND IsDeleted = 0
);
GO
-- 外部把它当做一个需要带进去门票的高端虚表来用:
SELECT * FROM fn_GetHighPaidWorkers(20000.00);
7. 性能核心:存储过程 (Stored Procedure) 与动态 SQL
这是真正的性能之王。你所有的核心高频调用业务逻辑几乎都最终会被打包封存在这。 它是一大段具有完整 C# 功能逻辑特征(带事务带游标带判断带报错捕捉带返回结构)的大型执行体。
而且它被第一次建立和运行的时候,引擎会把对里面成千上百个表交织算好的最优解路线图存到内部内存之中(执行计划缓存 Execution Plan Caching)。 下次即便是换了传入参数,引擎不会再从零开始扫描计算这长篇大论,直接挂着最优图谱极速出片完成。相比把长串 SQL 字符通过 C# ORM 无尽传输有不可磨灭的低延时优势。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-- 我们来写一个标准的带全套操作特性的高阶存储过程
CREATE PROCEDURE sp_HandleEmployeeBonus
@InDeptId INT, -- 接收普通的传递进来的参数
@InBaseBonus DECIMAL(18,2),
@OutTotalGrantedBonus DECIMAL(18,2) OUTPUT -- 输出型特殊参数(做完事之后带出给调用方读取的)
AS
BEGIN
SET NOCOUNT ON; -- 切忌别漏了!让引擎别在那浪费网速在那傻发"(影响 1 行)" 这种极其占用 IO 流量返回无用信息包了。
-- 初始化输出总金额计数
SET @OutTotalGrantedBonus = 0;
BEGIN TRY
-- 进行逻辑筛选,没有必要进入修改循环了就快速阻断。
IF NOT EXISTS(SELECT 1 FROM Employee WHERE DeptId = @InDeptId AND IsDeleted = 0)
BEGIN
PRINT '该部门一个活人都没有,终止发奖!';
RETURN;
END
-- 计算总奖金额给外围
SELECT @OutTotalGrantedBonus = SUM(Salary * 0.1M + @InBaseBonus)
FROM Employee WHERE DeptId = @InDeptId;
-- 执行批量的真切物理修改!
UPDATE Employee
SET Salary = Salary + (Salary * 0.1M + @InBaseBonus)
WHERE DeptId = @InDeptId AND IsDeleted = 0;
PRINT '存储过程执行完成发薪完毕';
END TRY
BEGIN CATCH
-- 【此处本应写更高级别的回滚报错收集代码,请参考后续事务篇章】
PRINT '执行严重失败! ' + ERROR_MESSAGE();
SET @OutTotalGrantedBonus = -1;
END CATCH
END;
GO
-- ================= C# 或客户端调用它的模拟过程 ==================
DECLARE @ResultTotalBonus DECIMAL(18,2); -- 建一个参数准备接收
-- EXEC 指令挂接大名然后传入匹配字典名称
EXEC sp_HandleEmployeeBonus
@InDeptId = 1,
@InBaseBonus = 500.00,
@OutTotalGrantedBonus = @ResultTotalBonus OUTPUT; -- 这里这个特性的对应不能忘写 OUTPUT
SELECT '本次发放到账总数计算报告为:' + CAST(@ResultTotalBonus AS VARCHAR);
高阶:动态抛出拼接的 SQL 语句 (Dynamic SQL)
有时候,我们的列名表名,甚至查询条件极度错综,比如用户在一个报表上选了二十个复选框和五十个维度组合,很难用静态查出。必须要在存储过程中执行靠字符串组合运算出的“临时变量中的 SQL 语句片段”。
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 核心使用自带专杀拼接的系统子过程: sp_executesql (千万别用直接的 EXEC(@string) 因为没有执行防注入化)
DECLARE @DynamicSQL NVARCHAR(MAX);
DECLARE @TableName NVARCHAR(50) = 'Employee'; -- 甚至连表名都是用户界面传进来的字符串
DECLARE @SearchName NVARCHAR(50) = 'Tony';
-- 极度惊心动魄地利用变量凑出了这段纯文本 T-SQL 字符串
SET @DynamicSQL = N'SELECT * FROM ' + QUOTENAME(@TableName) + N' WHERE EmpName = @NameParam';
-- 直接通过系统底层命令运行这行字符串,并且安全地把内嵌参数的引用抛过去建立防注入防弹衣!
EXEC sp_executesql
@stmt = @DynamicSQL,
@params = N'@NameParam NVARCHAR(50)',
@NameParam = @SearchName; -- 给那个抛过去的参数填充真的子弹内容
8. 数据库的心脏:事务 (Transaction) 与 4大隔离级别(锁)
ACID(原子性 Atomicity、一致性、隔离性 Isolation、持久性 Durability)是一切关系型数据库信仰体系中万神殿的神祗准则。 当你处理转账、处理淘宝买东西减去库存增加物流这种不可分割的任务包裹(Transaction)时候,必须要对数据库显式下达 “开启全系统级一致性锁控回滚保护”。要么全部这十三张表修改完成,要么有一张表出错整个这一摊子全给我撤销复原!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
CREATE PROCEDURE sp_SecureMoneyTransfer
@FromAcc INT,
@ToAcc INT,
@Amount DECIMAL(18,2)
AS
BEGIN
SET NOCOUNT ON;
-- 1. 显性宣告:锁控护卫系统启动 (从这一秒开始执行的修改在没有走到底下提交时都是悬在锁里锁死的,其他人想读这些行的内容统统被阻挡冻结在门外排队!)
BEGIN TRANSACTION;
BEGIN TRY
-- 判断 A 的钱够不够
IF (SELECT Balance FROM Accounts WHERE Id = @FromAcc) < @Amount
THROW 50001, '余额绝对不够啦', 1; -- THROW 是现代 T-SQL 版直接把程序砸懵强制把代码跳转扔进下方 CATCH 的抛砖神技
-- A 扣款
UPDATE Accounts SET Balance = Balance - @Amount WHERE Id = @FromAcc;
-- B 入账 (此时突然 B 账户由于某人手残加的外键约束问题直接爆掉了红色异常崩溃报错了!!)
UPDATE Accounts SET Balance = Balance + @Amount WHERE Id = @ToAcc;
-- 如果天下太平没出什么幺蛾子,那么恭喜全部放行写入物理介质真正提交完本!
COMMIT TRANSACTION;
PRINT '转账全链路成功结束。';
END TRY
BEGIN CATCH
-- 【这是事务中最核心保护的一课】刚才如果 B 账户代码由于引擎级错误直接崩溃到了这里被你兜住,之前那一步被扣了钱的 A 的可怜的钱怎么救回来?
-- @@TRANCOUNT 这个全局变量如果大于 0 意味着 “当前身上还挂着个已经开始但没收摊的锁包”
IF @@TRANCOUNT > 0
BEGIN
-- 无情撤回:时间倒流,数据库所有之前被锁死的这批修改全部完美复原回它什么事都没发生之前的最初的样子。并将加锁阻塞池清空释放其他人。
ROLLBACK TRANSACTION;
END
PRINT '极度危机!事务已经熔断回滚。具体错误报告:' + ERROR_MESSAGE();
END CATCH
END;
并发问题与四大隔离级别深度干解:
当你高并发下,同时有个事务A和事务B对同一批数据狂轰滥炸操作。
你需要自己手动向引擎输入指令告诉他采取多强度的看守大门措施。这就是隔离级别调整(SET TRANSACTION ISOLATION LEVEL XXX)。
- READ UNCOMMITTED (未提交读):最垃圾不要脸的裸奔。 A 事务对某行刚写了个数据但还没敲定
Commit,甚至极大概率两秒之后他就直接不要给撤销RollBack了,在此期间 B 事务跑过来查这种悬在半空的鬼魂数据,竟然也能查出来并且拿去使用了。引发的核心灾难现象:【脏读 Dirty Read】。适用于仅仅只要求读取快比如查询总用户统计排行榜这种完全不敏感的地方。 - READ COMMITTED (提交读) [SQL SERVER 的默认默认天尊]: 它堵上了脏读的漏洞,A只要没
Commit数据就是上锁绝对不可视的。只有Commit落定的真实数据才能被 B 取到读出。但有【不可重复读】的现象:就是在一个极长的查询 B 事务里,刚查了一遍这个人的名字叫 “张三”,转头做别的事做了十秒此时有个叫 A 的修改线程刚好冲过来把它名字改成“李四”了。当这个 B 事务中第二句再次回头确认这人的名字时,会惊异发现在我同一个事务里面,查同个东西怎么变性了! - REPEATABLE READ (可重复读): 只要是 B 所在的查询事务在查的数据行,他引擎就给加死了一把读取不许改长串护城锁。就是 B 只要在查询期不结束这数据,A 这个时候想跑过来修改成李四直接被卡死排队堵在门外永远无法执行修改,直到 B 自己查完所有的事撤除长锁放行。彻底保护了 B 同一轮里面无论什么时候回头再查,叫“张三”的人绝对不可能被人偷改变成别人的现象。但挡不住 A 在旁边利用空间凭空给总库里
INSERT插入新加了一个全新的人。这就叫诡异的【幻读 Phantom Read】。 - SERIALIZABLE (可串行化极限绝密锁):性能坟墓。 凡是对这堆符合某区间要求的几十万行表甚至大面积块查的过程中,不仅不准你进去改已存在的数据,你也休想在这个这片区域给我增加新插入一条符合这个区间的新数据!隔离级别全宇宙满点最强,并发性能全宇宙卡成死猪!用于极严苛的特殊计费汇总强锁定节点!
9. 隐形看门狗:触发器 (Trigger) 深度剖析
触发器就是个隐藏很深的特殊形式的存储过程。当有人对某表做了它暗中监视的 INSERT / UPDATE / DELETE 动作后,根本不用人显式调用它,系统会强行在主查询锁结束的刹那静默召唤出它的伴生执行附体。
虽然现代系统出于降低复杂排查链路难度不再提倡滥用业务触发器。但它是做防核心删除日志兜底审计的最佳组件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TRIGGER trg_EmployeeDeleteAudit
ON Employee
AFTER DELETE -- 表明只有 Delete 指令发出确认并干掉行后,我狗子立刻就会激活扑出来
AS
BEGIN
-- 【魔法天书系统表】:inserted (存放修改后的新数据或者刚进来的新数据), deleted (存放已被覆盖的老残数据或者刚被删去的数据)。这两个表是在执行当瞬内存中极其短暂存在的!!
-- 本触发器监视的明明就是把几十个离职人员强制从数据库干除这个命令!所以引擎给我的魔法表 deleted 中这会装满了那整整被杀的五十个人员信息。
-- 趁还没来得及消失的零点几秒,在后门利用这个删留下的亡语把所有的尸体信息悄悄塞给归档备忘录表
INSERT INTO Employee_DismissedArchive (EmpId, Name, SalaryWas, DeleteTime)
SELECT
EmpId,
EmpName,
Salary,
GETDATE() -- 利用这种关联式神速复制技巧进行暗中的记录
FROM deleted;
PRINT '离职人员清理监控:底层数据库核心档案移交已通过无形触发器执行完成。';
-- 请注意如果内部这种日志移交业务本身报错了,刚才的那批在界面发生的前端删除动作也会因发生全连锁雪崩全盘倒退被强制撤回。这就是同属一条底层锁血命的好处和拖累坏处所在。
END;
10. 极致调优:索引 (Index) 的聚簇本质与执行计划调优
当你面对 5 亿的数据跑出一个 Select 感到系统近乎心跳停止宕机时,你的第一个也是唯一的终极武器只有——剖析并增加索引。
不加索引扫表,在计算机领域的代号叫:Table Scan (无差别的全表瞎扫)。即使引擎拥有八百个计算核心也会在这里翻船。
10.1 聚集决定此生的物理身躯 (Clustered Index)
- 实质:一张表的聚集索引只有一个!因为它只能代表这本超级巨型的 5 亿字典里面的这几页究竟是按照第一页往后真实怎么用纸张按照什么顺序去装订叠放的物理身位图谱。而且这个底层数据结构是一个 B+Tree(二叉平衡演进多叉大树)。
- 触发原因:一旦你指明哪一列是唯一主键标记为强约束 (
PRIMARY KEY),引擎它就是顺水推舟在建库一瞬间自动帮你把聚集索引也同时安放挂在这上面。而且通常由于系统建议大家主键都是利用递增连续增加数字IDENTITY(1,1),所以底层的数据就像排队打卡的人一样一个个紧挨在屁股后头极其规整舒适没有碎片(Page Split 爆裂)。如果一旦哪个无知开发把主键设置成随意字母无规律生成的GUID。由于排序强制插入中间,那底层的聚集这棵物理树就如同被炸弹不停重排。 - 查询恐怖威力:查找极其快,比如你的员工号(主键),找主键=五千万员工号,只需顺树跳跃几下磁盘IO就命中了直接掏出,这就叫极速的 Clustered Index Seek(深水炸弹精准寻址),相比 Table Scan 百慢倍率的差距!!
10.2 后天辅挂的额外地图旁支 (Non-Clustered Index 非聚集索引)
- 实质:比如我们总是在 WHERE 里面搜索员工邮箱 (Email),这显然它可不是主键物理表不是按它排列的。于是你需要再外挂新建一本辅书小日记!日记里面只有按照 a-z 清清楚楚首字母排序的一千万单列清爽的所有的“邮件列表结构拼音字符”,并给每个拼音字符后面拿一根针挂上线连接告诉系统这个邮箱是其实是存放在那本大物理书的哪个具体的 8KB 长条形抽屉(Row Pointer 地址关联)。
- 查询过程现象:先神速翻小日记直接命中(Index Seek 非聚集深层寻读)–> 但我光拿个邮箱又没有这人他长什么样的全部别的数据资料啊 –> 去利用末尾那根关联线返回原主聚集大表直接抽走(这个耗时稍微有点点长的经典过程绝招,它术语被称之为:Key Lookup!也就是神龙脱骨折返查表(RID/键查找))的致命问题。
- 怎么建立这本强力辅助字典:
1
2
3
4
5
6
7
8
9
10
-- 建树!将极大提高所有带 WHERE Email = '?' 及 ORDER BY Email 结尾相关操作的十万倍性能
CREATE NONCLUSTERED INDEX IX_Employee_EmailSearchLookup
ON Employee (Email ASC);
-- 再建树!!如果既搜邮箱又搜部门
CREATE NONCLUSTERED INDEX IX_Employee_MailWithDeptCombiner
ON Employee (Email ASC, DeptId DESC);
-- 这句话查询立马变得顺滑:
-- SELECT * FROM Employee WHERE Email = 'jack@exp.net' AND DeptId = 5;
10.3 “覆盖索引术” (Covering Index) (防 Lookup 原大表回表绝杀技)
假如有个 API,它经常查邮箱只求一并带出它的电话。但由于我只建了一个只包含单独邮件这一条树分支的小日记表索引。上面提到我要去翻那本原体大书(去发费极度高昂磁盘消耗利用 Key Lookup 去原表查出附加电话字段)。
如果能够建立的时候只把电话这一字段强行也顺便复印在每个邮箱小日记索引项的结尾不就能免回原表大抽查了吗!。这种专门克制回表带附属随从的小技巧,被称为包含列 (INCLUDE) 加持覆盖。
1
2
3
4
5
6
7
8
9
-- 把这招砸向上面刚才建小树的操作进行覆写 (WITH DROP_EXISTING 高级重建)
CREATE NONCLUSTERED INDEX IX_Employee_EmailSearchLookup
ON Employee (Email ASC)
INCLUDE (Phone, Status) -- 注意!不要包含几百个字段的大肉块。这非常增加平时插入数据的拖慢开销(因为增删需要时间更新你的这本超重的日记)
WITH (DROP_EXISTING = ON);
-- 从此以后如下这行极为常见高发频的接口底层查询,因为他不仅通过 Seek 闪电找到了邮件定位区间,所有顺带要求的这俩参数也就在它挂件上一起存在直接带回:
-- 这次他这查询再也不会发生任何一次 Key Lookup 原表提取物理抽拉拖累:性能达到了全网理论封顶极限水平!
-- SELECT Phone, Status FROM Employee WHERE Email = 'jack@exp.net';
对于索引的极致把控是在一个表上不多建一个垃圾索引,也绝不漏掉一个有几十万查询量 Join/Where 支撑的核心复合查询带包含索引,这考验着高级数据库选手的顶级功底经验。
11. 高阶查询艺术:CTE 递归、PIVOT 行列转换与 CROSS/OUTER APPLY
真正的报表查询和复杂运算往往超出了常规 JOIN 的能力范畴,我们需要极其高段位的操作手段。
11.1 无限套娃:递归 CTE (Recursive CTE)
在构建菜单树、组织架构图、评论盖楼等具有“无限层级引用”的场景中,传统的自连接无法穷尽所有的层级。T-SQL 的递归 CTE 可以完美地通过一次查询把所有层深的树状图全部遍历拉出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 测试用:创建一个自己引用自己的员工上下级树表
-- CREATE TABLE OrgChart (EmpId INT, EmpName NVARCHAR(50), BossId INT NULL);
-- INSERT INTO OrgChart VALUES (1, '大老板', NULL), (2, '销售总监', 1), (3, '研发总监', 1), (4, '销售经理A', 2), (5, '前端程序员', 3);
-- 使用递归 CTE 拉出包含树层级和全部从属路线的高级结果集
WITH RecursiveOrgCTE AS
(
-- 1. [锚点成员] 定位起点:也就是找到所有根本没有上司的顶级大老板作为第 1 级
SELECT EmpId, EmpName, BossId, 1 AS TierLevel, CAST(EmpName AS NVARCHAR(MAX)) AS 'Chain'
FROM OrgChart
WHERE BossId IS NULL
UNION ALL
-- 2. [递归成员] 不断自我循环:拿着上面的结果 CTE 的表名 (RecursiveOrgCTE) 去内部连表!
SELECT o.EmpId, o.EmpName, o.BossId,
cte.TierLevel + 1 AS TierLevel, -- 层级逐级递增
cte.Chain + ' -> ' + o.EmpName AS 'Chain' -- 把名字像珠子一样顺势串起来
FROM OrgChart o
INNER JOIN RecursiveOrgCTE cte ON o.BossId = cte.EmpId -- 将员工的 BossId 指向上一步拿出来的老大的 EmpId
)
SELECT * FROM RecursiveOrgCTE
ORDER BY TierLevel ASC;
11.2 PIVOT 与 UNPIVOT:数据透视表行列魔术
当我们接到 DBA 需求,要求把“竖着存储的各月份销售额清单”硬生生用 SQL 语句变成“每一个月份变成一个单独的一字排开的动态横向列头”时(常用于财务年终导出报表),PIVOT 是绝对王者。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 原始表大概长这样: 销售员(Name) | 月份(Month) | 业绩(Amount) [是竖向无穷多行的]
-- 希望输出长这样: | Name | 1月 | 2月 | 3月 | 4月 |
SELECT * FROM
(
SELECT Name, Month, Amount
FROM SalesRecord
WHERE Year = 2025
) AS SourceTable -- 原片基础提取集
PIVOT
(
-- PIVOT 魔术引擎启动:把所有相同的 Month 下的 Amount 进行挤压求和
SUM(Amount)
-- 强行把原本作为内容存在的 [1月, 2月] 数据值变成了真正的列名字母!
FOR Month IN ([1月], [2月], [3月], [4月], [5月], [6月])
) AS PivotTable;
(注意:反向操作则是使用 UNPIVOT,将横向的宽表炸毁还原成符合第一范式的无穷纵列表。)
11.3 APPLY 算子:CROSS APPLY 与 OUTER APPLY
很多时候我们需要在连表时(比如通过 JOIN 某一个返回表格结构结果的函数 TVF),把主外表某一列带入进后面那张函数表当做条件参数,JOIN 根本做不到,这时候必须动用 APPLY。
CROSS APPLY: 相当于INNER JOIN的强化版,左边找不到右边的匹配就整行丢弃。OUTER APPLY: 相当于LEFT JOIN的强化版,就算函数没返回,左边主表数据依然而然健在,并把右表空白全铺NULL。
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 获取每个部门下,工资排名稳居前两名(Top 2)的绝顶高手的资料,并拼接在一起。
-- 这个用单纯的 JOIN 是极其难写的,有了 APPLY 以及传递参数就变得极为简单:
SELECT d.DeptName, TopEmp.EmpName, TopEmp.Salary
FROM Department d
CROSS APPLY
(
-- 每从外部读取到一条 Department 部门信息,执行器都会携带它的 d.DeptId 钻进这个子盒子里执行一遍这个拿 Top 2 的独立运算!
SELECT TOP 2 EmpName, Salary
FROM Employee e
WHERE e.DeptId = d.DeptId AND e.IsDeleted = 0
ORDER BY Salary DESC
) AS TopEmp;
12. 终极数据操纵:MERGE(Upsert)与 临时表/表变量体系
12.1 MERGE 巨型缝合怪:无敌的修改合并(有则更新,无则插入)
以前我们做“如果这行存在咱们就 UPDATE 刷新字段值,如果不存在这人咱们就 INSERT 插一条新记录”,要在程序里查询和写入交织跑很多遍。MERGE 提供了源表对目标表的终极毁灭级一体覆盖能力。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 常用于定时同步任务,将昨天的销售新流水增量集合(Source)合并贴死到主库汇总表(Target)里
MERGE INTO MainSummaryTable AS target
USING DailyImportTable AS source
ON target.UniqueId = source.UniqueId
WHEN MATCHED THEN
-- 如果找到了两边共有的身份证 ID,就做定向刷新
UPDATE SET target.TotalSales = target.TotalSales + source.LastSales,
target.LastUpdate = GETDATE()
WHEN NOT MATCHED BY TARGET THEN
-- 目标主机上根本没这个人,判定为纯全新增加新员工
INSERT (UniqueId, TotalSales, LastUpdate)
VALUES (source.UniqueId, source.LastSales, GETDATE())
WHEN NOT MATCHED BY SOURCE THEN
-- 甚至支持反向处理:如果是主库里有但新进表里反而消失了的人员(可能离职了),在此地强行打上死亡标记
UPDATE SET target.IsActive = 0;
-- (注意:最后必须跟上一个英文半角分号)
12.2 #Temp 临时表 vs @Table 表变量:生命周期的抉择
SQL Server 提供了两种短命数据表容器,了解差异能大幅减少内存耗尽并极大提高几万数据处理时的性能:
-
本地临时表 (以
#开头,如#TempOrders) 存在于系统的tempdb硬盘块(部分驻留内存)。 绝对优势:你可以为临时表建立多栏的高级物理聚集索引(Primary Key)和统计信息字典。在大几十万数据量级时使用它进行二次JOIN过滤是极为靠谱的快操作。它的生命周期伴随你的那个调用 Session 结束自动火葬销毁。 -
表变量 (以
@声明,如DECLARE @TblOrders TABLE (...)) 纯粹生存在当前批处理进程极易挥发的直接内存中。 绝对优势:它甚至不发生写锁、不写大量的回滚事务日志负担。但缺点极其严重:它没有统计信息指引图谱!优化器看到一亿条的表变量也会傻乎乎把它估算成了只有 1 行数据。如果在大型关联时用它,极容易逼得执行计划错乱死锁崩溃。适合存储不到 1 百或者几百条的超小型临时数组 ID。
1
2
3
4
5
6
7
8
9
10
-- 1. 表变量:轻盈小巧 (不用 DROP)
DECLARE @LocalIdList TABLE (Id INT PRIMARY KEY, Val NVARCHAR(20));
INSERT INTO @LocalIdList VALUES (1, 'A'), (2, 'B');
-- 2. 临时物理表:能抗下几十万数据大旗 (用完最佳习惯还是手工 DROP TABLE #HeavyDataTable 释放硬盘)
CREATE TABLE #HeavyDataTable (
OrderNo VARCHAR(50) NOT NULL PRIMARY KEY INDEX IX_OrderTree, -- 直建树状索引!
Amt DECIMAL(18,2)
);
INSERT INTO #HeavyDataTable SELECT OrderNo, Amount FROM OldHistory WHERE CreateY = 2025;
13. 极客防身术:随心所欲的动态 SQL 与执行提示 (Query Hints)
13.1 掌控优化器大脑:查询提示 Query Hints
我们深知 SQL 是一门“说明你想干什么,路线图让底下的老爷爷(执行器)自动计算”的声明语句。
但某些特殊情况底下老爷爷确实脑抽了导致了极度荒谬的极慢的死锁路线,我们可以利用语句末尾的 OPTION 强制进行执行级别的脑干劫持干预。
WITH (NOLOCK): 最常用大杀器。在你写SELECT拿大量数据的末尾贴上这句。强行指令引擎:不准向正在执行修改的人要访问锁,无视其他人挂起的防守动作,横冲直撞去扫盘!虽然极度可能拿到“脏读”甚至不完整的一半的修改鬼魂数据,但对于那些允许一点统计误差的总控报表中心等不敏感的地方,它让速度起死回生。OPTION (RECOMPILE): 对抗“参数嗅探(Parameter Sniffing)”的终极解药。有时候因为你之前传了个冷门参数使得存储过程给算了一条蠢到家的极其缓慢的老路线锁死了缓存。你在结尾挂上这句后,将命令它:不要去读缓存以前的那张图纸路线结构!今天带进来的这个参数,给我当场硬着头皮重新在脑子里构思一条最佳寻找路线重新跑!
1
2
3
4
5
6
-- 强行穿刺读取那些正在被执行 UPDATE 锁着的数据项 (极其不安全,但极快无阻塞)
SELECT COUNT(1) FROM HeavyLogs WITH (NOLOCK) WHERE CreateDate > '2025-01-01';
-- 当天重新强制分析新传入的参数 @DeptId,废除曾经该存储过程缓存的全部老式查询路线规划
SELECT * FROM Employee WHERE DeptId = @DeptId
OPTION (RECOMPILE);
至此,关于所有 T-SQL 从开辟疆土的基础核心,讲到了索引内聚并发锁,甚至是一网打尽了 CTE 递归、MERGE 合体、PIVOT 数据透视 以及强制突破优化器的脑干封印指令 Query Hints。整篇笔记已然升华为超强火力压制的万字长篇大论封顶句号。
能够完全消化、吸收并能真切地在 C# 代码及数据库内部实施以上防线技术的开发者,已经具备在复杂的大型高并发、高可用电商及后端中枢业务生态体系下,扛下绝对首席核心关系型数据库架构以及深度调优专家的技术水准水准了!