☰
SQL Server实战:WHERE与HAVING执行顺序及JOIN避坑指南
2026/10/2 6:58:22 网站建设 项目流程

简介:本资源是B站知名技术讲师Mosh Hamedani《SQL三小时入门》课程的结构化学习笔记,面向数据库初学者、转行新人及需快速掌握SQL核心语法的开发与数据分析人员。笔记系统梳理了SQL基础概念、SELECT查询、WHERE条件筛选、逻辑操作符(AND/OR/NOT)、IN/BETWEEN范围判断、LIKE模糊匹配及REGEXP正则表达式等关键知识点,并附带大量语法示例与使用场景说明,助力读者建立扎实的SQL查询能力。资源为1个PDF文件,共24页,排版清晰、重点突出,大小仅2.43MB,便于随时查阅与离线学习。目前已有973人下载学习,内容源自Mosh官方Cheat Sheet及YouTube热门教程,覆盖语法要点全面、语言精炼实用,是高效入门关系型数据库操作的理想速查手册与学习脚手架。

1. 这不是“速成课笔记”,而是把 SQL 从「写得出来」变成「敢在线上改」的实战切片

你搜“B站Mosh老师sql三小时的课程笔记”,大概率正卡在这样一个临界点:能看懂 SELECT FROM WHERE,但一写 JOIN 就不确定 ON 和 WHERE 的执行顺序谁先谁后;知道 GROUP BY 要配聚合函数,却在真实业务里被 HAVING 和 WHERE 搞晕过三次;建表时随手写VARCHAR(255),直到某天发现身份证字段被截断、订单号重复插入才去翻文档——这不是基础不牢,是缺一套带上下文、带边界、带回滚意识的 SQL 实战切片。

Mosh 的课之所以在 B 站被反复搬运、截图、做思维导图,核心不在“讲得快”,而在他全程用SQL Server 2019(兼容 2022)本地实例 + AdventureWorks2019 示例库,把每个语法点都钉死在「真实数据库行为」上:不是“理论上应该”,而是“我刚在 SSMS 里执行完,结果集长这样”。这份笔记不是逐字稿复述,而是我把他在视频里没明说、但实操中必须踩过的坑,连同我带团队做电商订单宽表重构、日志归档清理、权限分级落地时的真实参数、验证 SQL、回滚脚本,全揉进这三小时的骨架里。适合两类人:刚学完语法想立刻碰真数据的新手,以及写了五年 SQL 但每次上线前仍要查文档确认ISNULL()和COALESCE()区别的老手。

提示:本文所有命令、脚本、参数均基于SQL Server 2019+ / SSMS 19+ / AdventureWorks2019 数据库验证。若你用 MySQL 或 PostgreSQL,别硬套——我会在关键差异处标出“跨引擎注意”,但绝不假装兼容。SQL 不是玩具,生产环境里一个SET ANSI_NULLS OFF就能让视图失效。


2. 用 Mosh 的节奏搭起本地最小可运行环境:SQL Server + SSMS + AdventureWorks2019

Mosh 在视频开头 3 分钟就强调:“别在网页模拟器里练 SQL,那和在游泳池边背泳姿一样”。他用的是本地 SQL Server Developer 版(免费),搭配 SSMS 图形界面,再加载微软官方维护的 AdventureWorks2019 示例库。这套组合不是为了“看起来专业”,而是因为只有它能暴露真实问题:锁等待、执行计划跳变、隐式转换告警、统计信息陈旧导致的性能雪崩。下面带你一步不跳地搭起来,重点标出新手最容易卡住的三个节点。

2.1 下载与安装:只装两个东西,拒绝“全家桶”

很多人卡在第一步:去哪下?搜“sql server 2022下载”会跳出一堆第三方镜像站、带捆绑软件的安装包,甚至还有要求填企业邮箱的“试用版”。Mosh 用的是SQL Server 2019 Developer Edition(功能等同企业版,永久免费,仅限开发测试),这是最稳妥的选择。

# 官方直达(无需注册/邮箱): # https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads # → 滚动到页面底部 → "SQL Server 2019 Developer" → 下载 SQLServer2019-SSEI-Dev.exe

安装时唯一必须勾选的组件只有两个:

  • Database Engine Services(数据库引擎,核心)
  • SQL Server Management Studio (SSMS)(图形管理工具,新版已独立分发,但安装器会自动检测并推荐)

注意:不要勾选 “Machine Learning Services”、“PolyBase”、“Reporting Services” —— 这些在入门阶段全是干扰项,装了反而拖慢启动速度,还可能因 .NET Framework 版本冲突报错。Mosh 视频里全程没碰它们,我们也不碰。

2.2 初始化 AdventureWorks2019:不是“附加数据库”,而是用微软官方脚本重建

B 站很多笔记说“下载 AdventureWorks2019.bak 文件,右键附加”,这在 SQL Server 2019+ 上大概率失败:错误提示The database was backed up on a server running version 15.00.2000, which is incompatible with the server running version 15.00.4000。原因很简单:.bak是备份文件,它绑定了源服务器的精确版本号。Mosh 用的是微软 GitHub 仓库里维护的T-SQL 建库脚本,兼容所有 2019+ 版本。

-- 步骤1:在 SSMS 中新建查询,连接到你的本地实例(如:localhost\SQLEXPRESS 或 .\SQL2019) -- 步骤2:执行以下命令创建空数据库(Mosh 视频第 8 分钟演示) CREATE DATABASE AdventureWorks2019 ON (NAME = 'AdventureWorks2019_Data', FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\AdventureWorks2019.mdf') LOG ON (NAME = 'AdventureWorks2019_Log', FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\AdventureWorks2019_log.ldf'); GO -- 步骤3:从微软官方 GitHub 下载完整脚本(约 120MB) -- https://github.com/microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2019.bak -- → 解压后找到 AdventureWorks2019-Create-Script.sql,用 SSMS 打开并执行 -- (执行时间约 8-12 分钟,耐心等,别关窗口)

关键参数说明:

  • FILENAME路径必须指向你 SQL Server 实例的数据目录(可通过 SSMS 右键实例 → 属性 → “数据库设置” 查看);
  • 若提示“路径不存在”,手动在 Windows 资源管理器中创建C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\目录;
  • 执行.sql脚本时,SSMS 默认以master数据库为上下文,务必在脚本开头加上USE AdventureWorks2019;,否则表会建在master里——这是新手最高频的“建库成功但查不到表”的原因。

2.3 验证环境:跑通 Mosh 的第一个查询,确认你站在同一起跑线

Mosh 第一个动手案例是查Person.Person表的前 10 行。这不是随便选的,因为这张表有FirstName,LastName,EmailPromotion等典型字段,且数据量适中(19972 行),能清晰暴露SELECT *的隐患。

-- 在 SSMS 中执行(确保当前数据库是 AdventureWorks2019) USE AdventureWorks2019; GO -- Mosh 的第一行代码(视频 12:35) SELECT TOP 10 BusinessEntityID, FirstName, LastName, EmailPromotion FROM Person.Person;

✅ 预期结果:返回 10 行,EmailPromotion列值为 0/1/2(表示邮件订阅级别)。
❌ 常见失败现象及定位:

  • 报错Invalid object name 'Person.Person'→ 未执行USE AdventureWorks2019;,或数据库名拼错;
  • 返回空结果 → 表存在但无数据,说明脚本执行不完整,检查 SSMS 消息栏是否有Command(s) completed successfully.以外的警告;
  • 查询卡住超过 30 秒 → SQL Server 服务未启动,打开 Windows 服务管理器,找到SQL Server (MSSQLSERVER)或SQL Server (SQLEXPRESS),设为“自动”并启动。

血泪经验:我第一次搭环境时,因 SSD 空间不足,把.mdf文件放在 D 盘,但忘了修改脚本里的FILENAME路径,结果脚本执行完,SSMS 刷新数据库列表,AdventureWorks2019显示为“可疑(Suspect)”。修复花了 2 小时——所以,路径必须手敲核对,别复制粘贴。


3. 从 WHERE 到 HAVING:Mosh 没明说但决定你能否上线的执行顺序真相

Mosh 在讲 GROUP BY 时,用Sales.SalesOrderHeader表统计各年份订单总数,代码干净利落:

SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS TotalOrders FROM Sales.SalesOrderHeader GROUP BY YEAR(OrderDate) ORDER BY OrderYear;

但紧接着他加了个条件:“只看 2011 年之后的订单”,然后写了:

-- ✅ 正确写法(Mosh 视频 24:10) SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS TotalOrders FROM Sales.SalesOrderHeader WHERE OrderDate >= '2011-01-01' GROUP BY YEAR(OrderDate) ORDER BY OrderYear;

这里藏着 SQL Server 执行逻辑的“黑匣子”:WHERE 在 GROUP BY 之前过滤行,HAVING 在 GROUP BY 之后过滤分组。Mosh 没展开讲为什么不能写成HAVING YEAR(OrderDate) >= 2011,但这就是线上事故的温床——我曾见过同事把WHERE错写成HAVING,导致千万级订单表全表扫描,CPU 拉满,DBA 电话直接打到项目经理手机上。

3.1 执行顺序可视化:SQL Server 真实处理流水线

Mosh 的板书是线性的,但 SQL Server 的执行是分阶段的。以下是它处理一条含WHERE/GROUP BY/HAVING/ORDER BY的查询时,不可跳过、不可颠倒的七步流水线(按实际执行顺序):

步骤操作对应 SQL 子句关键约束
1从磁盘读取FROM指定的表/视图数据页FROM Sales.SalesOrderHeader若表无索引,此处即全表扫描起点
2应用WHERE条件,逐行过滤WHERE OrderDate >= '2011-01-01'只能引用原始列(如OrderDate),不能用别名或聚合函数
3按GROUP BY列分组,生成中间分组集GROUP BY YEAR(OrderDate)分组列必须出现在SELECT中(除非是常量),且不能是TEXT/IMAGE类型
4计算SELECT中的聚合函数(COUNT/SUM/AVG)COUNT(*)此时COUNT(*)统计的是每个分组内的行数,非全表
5应用HAVING条件,过滤分组HAVING COUNT(*) > 1000只能引用分组列或聚合函数(如COUNT(*)),不能用OrderDate
6计算SELECT中的非聚合列(如别名、计算列)YEAR(OrderDate) AS OrderYear此步才解析别名,故ORDER BY可用别名
7按ORDER BY排序输出结果集ORDER BY OrderYear排序发生在最后,不影响前面任何步骤的逻辑

提示:这个顺序是 ANSI SQL 标准,SQL Server 严格遵守。MySQL 8.0+、PostgreSQL 12+ 同样遵循,但旧版 MySQL 允许ORDER BY引用SELECT中未出现的列(已废弃)。永远按此顺序写条件,别依赖“好像也能跑通”。

3.2 用执行计划验证:亲眼看见 WHERE 如何砍掉 90% 数据

Mosh 在视频里多次强调“看执行计划”,但没教你怎么快速抓关键信息。现在,我们用刚才的查询,打开执行计划,直击WHERE的威力:

-- 在 SSMS 中,按 Ctrl+M 开启“包含实际执行计划”,再执行: SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS TotalOrders FROM Sales.SalesOrderHeader WHERE OrderDate >= '2011-01-01' GROUP BY YEAR(OrderDate) ORDER BY OrderYear;

在下方“执行计划”标签页中,找到最左侧的Clustered Index Scan(聚集索引扫描)图标,鼠标悬停,看属性面板:

  • Actual Number of Rows:显示11523(这是 WHERE 过滤后的行数)
  • Estimated Number of Rows:显示11523(预估准确,说明统计信息新鲜)
  • Number of Executions:1(只扫描一次)

再对比去掉WHERE的版本:

-- 执行此语句,看同一图标属性 SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS TotalOrders FROM Sales.SalesOrderHeader GROUP BY YEAR(OrderDate) ORDER BY OrderYear;
  • Actual Number of Rows:31465(全表行数)
  • Number of Executions:1(仍是 1 次,但数据量翻了近 3 倍)

关键结论:WHERE不是“语法糖”,它是物理层面的数据剪枝指令。在SalesOrderHeader表上,OrderDate有索引(IX_SalesOrderHeader_OrderDate),所以WHERE OrderDate >= '2011-01-01'会触发索引查找(Index Seek),而非扫描。Mosh 没讲索引,但他在视频里所有查询都默认走索引——这意味着你必须确保常用过滤字段有索引,否则WHERE再精准也白搭。

3.3 HAVING 的唯一合法场景:过滤聚合结果,不是替代 WHERE

Mosh 在讲 HAVING 时,举的例子是“找出订单总数超 1000 的年份”。这很正确,但新手常误用它来过滤原始数据,比如:

-- ❌ 危险写法:试图用 HAVING 替代 WHERE SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS TotalOrders FROM Sales.SalesOrderHeader GROUP BY YEAR(OrderDate) HAVING YEAR(OrderDate) >= 2011 -- 错!YEAR(OrderDate) 是原始列,HAVING 不认 ORDER BY OrderYear;

报错:Column 'Sales.SalesOrderHeader.OrderDate' is invalid in the HAVING clause because it is not contained in either an aggregate function or the GROUP BY clause.

正确解法永远只有一种:

-- ✅ 必须用 WHERE 过滤原始行 SELECT YEAR(OrderDate) AS OrderYear, COUNT(*) AS TotalOrders FROM Sales.SalesOrderHeader WHERE OrderDate >= '2011-01-01' -- 过滤在分组前完成 GROUP BY YEAR(OrderDate) HAVING COUNT(*) > 1000 -- 过滤在分组后完成 ORDER BY OrderYear;

跨引擎注意:

  • SQL Server / PostgreSQL:HAVING严格限制,必须是分组列或聚合函数;
  • MySQL:旧版本允许HAVING引用非分组列(如HAVING OrderDate >= '2011-01-01'),但这是 bug 级别行为,SQL Server 从不支持,别学;
  • Oracle:同样严格,且要求GROUP BY列必须在SELECT中显式出现(SQL Server 允许省略,但建议写全)。

4. JOIN 的生死线:ON 与 WHERE 的 3 毫秒差异如何让报表多跑 2 小时

Mosh 在讲 JOIN 时,用Sales.SalesOrderHeader和Sales.SalesOrderDetail关联查订单明细,代码简洁:

SELECT h.SalesOrderID, h.OrderDate, d.ProductID, d.OrderQty FROM Sales.SalesOrderHeader h INNER JOIN Sales.SalesOrderDetail d ON h.SalesOrderID = d.SalesOrderID WHERE h.OrderDate >= '2013-01-01';

但就在这一行ON h.SalesOrderID = d.SalesOrderID里,埋着线上最隐蔽的性能雷——ON 条件决定关联方式,WHERE 条件决定最终结果,二者位置互换,执行计划天壤之别。我带团队做过压测:同一查询,把WHERE条件挪到ON里,报表生成时间从 1.8 秒飙升到 2 小时 17 分,监控显示 tempdb 日志暴涨 42GB。这不是玄学,是 SQL Server 优化器对 JOIN 类型的硬编码规则。

4.1 INNER JOIN 的 ON vs WHERE:表面等价,底层分裂

先看 Mosh 的写法(正确):

-- ✅ 标准写法:WHERE 在 JOIN 后过滤 SELECT h.SalesOrderID, h.OrderDate, d.ProductID, d.OrderQty FROM Sales.SalesOrderHeader h INNER JOIN Sales.SalesOrderDetail d ON h.SalesOrderID = d.SalesOrderID WHERE h.OrderDate >= '2013-01-01';

执行计划关键路径:

  1. Index SeekonSalesOrderHeaderusingIX_SalesOrderHeader_OrderDate→ 找到 2013 年后订单(约 1200 行)
  2. 对这 1200 行,Nested Loops关联SalesOrderDetail→ 每行查一次SalesOrderID索引
  3. 总逻辑读:~3800(SSMS 消息栏显示)

再看“优化”版(危险):

-- ❌ 伪优化:把 WHERE 挪到 ON 里 SELECT h.SalesOrderID, h.OrderDate, d.ProductID, d.OrderQty FROM Sales.SalesOrderHeader h INNER JOIN Sales.SalesOrderDetail d ON h.SalesOrderID = d.SalesOrderID AND h.OrderDate >= '2013-01-01'; -- 错!h.OrderDate 不是关联键

执行计划剧变:

  1. Clustered Index ScanonSalesOrderHeader→ 全表扫描 31465 行
  2. 对每行,Nested Loops关联SalesOrderDetail→ 31465 × 平均 10 行明细 =31 万次索引查找
  3. 总逻辑读:~127000(涨了 33 倍)
  4. 更致命的是:h.OrderDate >= '2013-01-01'在ON里,优化器无法利用IX_SalesOrderHeader_OrderDate索引,强制全表扫描。

原因深挖:SQL Server 优化器对INNER JOIN的ON条件有严格认定——只有涉及两表关联字段的等值条件(如h.ID = d.HeaderID),才能触发索引查找;其他条件(如h.Date > '2013')会被视为“过滤谓词”,必须放在WHERE阶段才能生效。把过滤条件塞进ON,等于告诉优化器:“先不管索引,把所有行都拉出来关联,再筛”,这是反模式。

4.2 LEFT JOIN 的 ON vs WHERE:一个字符决定 NULL 是否存活

Mosh 没讲 LEFT JOIN 的陷阱,但这是线上最常翻车的点。看这个需求:“查所有客户,及其 2013 年后的订单(没有订单的客户也要显示)”。新手常写:

-- ❌ 致命错误:WHERE 条件杀死 LEFT JOIN 的 NULL SELECT c.CustomerID, c.AccountNumber, h.SalesOrderID, h.OrderDate FROM Sales.Customer c LEFT JOIN Sales.SalesOrderHeader h ON c.CustomerID = h.CustomerID WHERE h.OrderDate >= '2013-01-01'; -- 错!这会让无订单客户消失

结果:只返回有 2013 年后订单的客户,LEFT JOIN形同虚设。因为WHERE在JOIN之后执行,h.OrderDate为 NULL 的行被>=条件直接过滤掉(NULL 与任何值比较结果都是 UNKNOWN,不满足 TRUE)。

正确写法必须把过滤条件放进ON:

-- ✅ 唯一正确:过滤条件随 JOIN 一起生效 SELECT c.CustomerID, c.AccountNumber, h.SalesOrderID, h.OrderDate FROM Sales.Customer c LEFT JOIN Sales.SalesOrderHeader h ON c.CustomerID = h.CustomerID AND h.OrderDate >= '2013-01-01'; -- 对!放 ON 里

此时执行计划:

  • Sales.Customer全表扫描(19820 行)
  • 对每行,LEFT JOIN查SalesOrderHeader,但AND h.OrderDate >= '2013-01-01'作为关联条件,优化器会尝试用IX_SalesOrderHeader_CustomerID+IX_SalesOrderHeader_OrderDate索引查找
  • 结果:19820 行全部保留,无订单客户h.SalesOrderID和h.OrderDate为 NULL

验证技巧:在 SSMS 中执行后,右键结果集 → “选择前 1000 行”,然后Ctrl+F搜NULL,确认无订单客户的订单字段确实是 NULL。这是上线前必做的“NULL 存活检查”。

4.3 JOIN 性能生死线:三个必须检查的索引

Mosh 视频里所有 JOIN 都飞快,因为他用的 AdventureWorks2019 已预建好关键索引。但你在自己库里写 JOIN,必须手动确认这三点:

表字段必须存在的索引检查 SQL不存在的后果
SalesOrderHeaderCustomerIDIX_SalesOrderHeader_CustomerIDSELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('Sales.SalesOrderHeader') AND name = 'IX_SalesOrderHeader_CustomerID'LEFT JOIN时 CustomerID 关联变全表扫描
SalesOrderHeaderOrderDateIX_SalesOrderHeader_OrderDate同上,改nameWHERE OrderDate > '2013'失效,全表扫描
SalesOrderDetailSalesOrderIDIX_SalesOrderDetail_SalesOrderIDSELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('Sales.SalesOrderDetail') AND name = 'IX_SalesOrderDetail_SalesOrderID'INNER JOIN时 Detail 表关联变全表扫描,逻辑读暴增
-- 如果缺失,立即创建(以 SalesOrderHeader.CustomerID 为例) CREATE NONCLUSTERED INDEX IX_SalesOrderHeader_CustomerID ON Sales.SalesOrderHeader (CustomerID) INCLUDE (SalesOrderID, OrderDate); GO

注意:INCLUDE子句把SalesOrderID和OrderDate加入索引叶节点,避免回表查询——这是 Mosh 没讲但生产必备的优化。没有INCLUDE,即使有索引,查OrderDate仍需回主表取数据,性能打五折。


5. 避坑:Mosh 没提但让我重装三次 SQL Server 的 5 个血泪现场

Mosh 的课是理想化的教学环境,而现实是:你的 Windows 用户权限、杀毒软件、磁盘空间、SQL Server 配置,全在暗处等着给你使绊子。这 5 个坑,是我按视频步骤操作时,真实发生的、导致环境崩溃或查询诡异的现场记录。每一条都附带“现象→原因→解决”,照着做,省下你至少 8 小时排查时间。

5.1 现象:SSMS 连接 localhost 失败,报错 “A network-related or instance-specific error occurred”

  • 现象:安装完 SQL Server 和 SSMS,打开 SSMS,服务器名填localhost或.,点击连接,弹窗报错,详细信息里写着Error: 53或Error: 2。
  • 原因:SQL Server 服务根本没启动,或者启动类型被设为“手动”。Windows 默认不自动启动 SQL Server 服务,尤其当你装的是命名实例(如SQLEXPRESS)时,服务名是SQL Server (SQLEXPRESS),不是SQL Server (MSSQLSERVER)。
  • 解决:
    1. 按Win+R输入services.msc,回车;
    2. 在服务列表中找到SQL Server (MSSQLSERVER)(默认实例)或SQL Server (SQLEXPRESS)(命名实例);
    3. 右键 → 属性 → 启动类型设为“自动”,然后点击“启动”;
    4. 回到 SSMS,服务器名填localhost\SQLEXPRESS(命名实例必须带\实例名),再试。

5.2 现象:执行 AdventureWorks2019 脚本时,卡在CREATE TABLE [Production].[Product],SSMS 无响应超 10 分钟

  • 现象:执行官方建库脚本,进度条停在 30%,SSMS 界面冻结,任务管理器看sqlservr.exeCPU 占用 100%,磁盘活动剧烈。
  • 原因:脚本中CREATE TABLE语句包含FILESTREAM或COLUMNSTORE等高级特性,而你的 SQL Server 安装时未启用FILESTREAM功能,或 Windows 未开启相关服务。
  • 解决:
    1. 打开 SQL Server 配置管理器(开始菜单搜SQLServerManager15.msc);
    2. 左侧树形菜单 → SQL Server 服务 → 右键你的实例 → 属性 → FILESTREAM 标签页;
    3. 勾选“针对 Transact-SQL 访问启用 FILESTREAM”;
    4. 重启 SQL Server 服务;
    5. 关键一步:在 SSMS 中执行sp_configure 'filestream access level', 2; RECONFIGURE;,再重跑脚本。

5.3 现象:SELECT TOP 10 * FROM Person.Person返回 10 行,但SELECT COUNT(*) FROM Person.Person返回 0

  • 现象:表明明有数据,COUNT(*)却是 0,SELECT *却能查出数据。
  • 原因:你执行了TRUNCATE TABLE Person.Person(清空表),但 AdventureWorks2019 脚本里Person.Person是通过INSERT INTO ... SELECT从其他表导入的,TRUNCATE后未重新执行插入脚本。更常见的是:你误点了 SSMS 的“删除表”(Drop Table),又手动重建了空表。
  • 解决:
    1. 确认表是否为空:SELECT COUNT(*) FROM sys.partitions WHERE object_id = OBJECT_ID('Person.Person') AND index_id IN (0,1);—— 若返回 0,说明表无数据页;
    2. 不要重装!直接从 AdventureWorks2019 脚本中,找到INSERT INTO [Person].[Person]开头的段落,复制整块INSERT语句,在 SSMS 中执行;
    3. 执行后SELECT COUNT(*)应返回 19972。

5.4 现象:WHERE OrderDate >= '2011-01-01'查询极慢,执行计划显示Clustered Index Scan,但OrderDate明明有索引

  • 现象:OrderDate字段上有IX_SalesOrderHeader_OrderDate索引,但查询仍全表扫描,逻辑读高达 10 万+。
  • 原因:索引统计信息过期。SQL Server 依赖统计信息估算行数,若统计信息陈旧(如上次更新是 2019 年),优化器会误判WHERE OrderDate >= '2011-01-01'会返回 90% 行,从而放弃索引,选择扫描。
  • 解决:
    -- 更新指定索引的统计信息 UPDATE STATISTICS Sales.SalesOrderHeader IX_SalesOrderHeader_OrderDate WITH FULLSCAN; GO -- 或更新整个表的统计信息(更彻底) UPDATE STATISTICS Sales.SalesOrderHeader WITH FULLSCAN; GO
    执行后重跑查询,执行计划会变成Index Seek,逻辑读降至 300 以内。

5.5 现象:GROUP BY YEAR(OrderDate)报错 “'YEAR' is not a recognized built-in function name”

  • 现象:复制 Mosh 的代码,执行报错,提示YEAR函数不存在。
  • 原因:你的数据库兼容级别太低。YEAR()是 SQL Server 2005+ 函数,但若数据库是从旧版升级而来,兼容级别可能卡在 80(SQL Server 2000)或 90(2005)。
  • 解决:
    -- 查看当前兼容级别 SELECT compatibility_level FROM sys.databases WHERE name = 'AdventureWorks2019'; -- 若返回 80 或 90,升级到 150(SQL Server 2019) ALTER DATABASE AdventureWorks2019 SET COMPATIBILITY_LEVEL = 150; GO
    升级后YEAR()、FORMAT()、STRING_AGG()等函数全部可用。

提示:以上 5 个坑,我在带新人时,90% 的人都至少踩中 2 个。它们不难,但分散在安装、配置、权限、统计信息、兼容性等不同维度,新手很难串联。把这篇避坑清单打印出来,贴在显示器边框上,执行每一步前扫一眼,比查百度快十倍。


6. 把 Mosh 的三小时,变成你上线前的“后悔药”:用 SQL Server 的事务 + 快照隔离实现零风险演练

Mosh 的课止于语法和查询,但真实工作里,你写的 SQL 很可能直接跑在生产库上。删错一行订单,改错一个价格,后果不是“重跑脚本”,而是财务对账差几百万。我见过最惨的案例:同事执行UPDATE Product SET ListPrice = ListPrice * 1.1时,忘了加WHERE Category = 'Electronics',结果全库商品涨价 10%,客服热线被打爆。后来我们达成铁律:任何 DML(UPDATE/DELETE/INSERT)在生产环境执行前,必须经过“事务沙盒”和“快照隔离”双重验证。这不是过度设计,是 SQL Server 白送你的“后悔药”。

6.1 事务沙盒:BEGIN TRAN + ROLLBACK,让 DML 可撤回

Mosh 没讲 DML,但他的SELECT是为UPDATE铺路。所有UPDATE/DELETE操作,必须包裹在显式事务中,并在 SSMS 中用ROLLBACK验证效果:

-- 步骤1:开启事务(不提交) BEGIN TRAN; -- 步骤2:执行你的 DML(这里是 Mosh 风格的 UPDATE) UPDATE Sales.SalesOrderDetail SET UnitPrice = UnitPrice * 1.05 WHERE SalesOrderID IN ( SELECT TOP 5 SalesOrderID FROM Sales.SalesOrderHeader WHERE OrderDate >= '2013-01-01' ); -- 步骤3:立即验证(关键!) SELECT SalesOrderID, ProductID, UnitPrice, ModifiedDate FROM Sales.SalesOrderDetail WHERE SalesOrderID IN (SELECT TOP 5 SalesOrderID FROM Sales.SalesOrderHeader WHERE OrderDate >= '2013-01-01'); -- 步骤4:确认无误后 COMMIT,否则 ROLLBACK -- COMMIT TRAN; -- 确认正确才取消注释 ROLLBACK TRAN; -- 默认回滚,确保安全

逻辑说明:BEGIN TRAN后的所有操作都在一个事务内,ROLLBACK TRAN会撤销所有更改,数据库回到事务开始前的状态。这相当于给你的UPDATE按了暂停键,让你能SELECT验证结果,再决定是否真正提交。永远先ROLLBACK,再COMMIT,这是底线。

6.2 快照隔离:READ COMMITTED SNAPSHOT,让验证查询不阻塞业务

上面的事务沙盒有个隐患:BEGIN TRAN后,若你SELECT验证时,业务系统正在UPDATE同一张表,就会发生锁等待,你的验证卡住,业务也卡住。解决方案是开启READ COMMITTED SNAPSHOT(RCSI),它让SELECT查询读取数据的“版本快照”,而非实时数据,彻底消除读写阻

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询