1. 从一个实际场景说起:为什么第二篇要盯着“操作”讲
很多朋友学 SQL Server 2019,第一篇还饶有兴致地装环境、建实例、连 SSMS,到第二篇就开始发懵:建好的库到底怎么用?表结构怎么设计才不返工?增删改查明明看着简单,为什么数据一多就卡成幻灯片?这篇就是来解决这些问题的。
我见过太多半路出家的开发者和运维,SQL 语法背得滚瓜烂熟,真上了生产环境却连“为什么这条 DELETE 走了全表扫描”“为什么加了索引还慢”都说不清。所以这一篇我不打算照着微软官方文档念一遍,而是把 SQL Server 2019 里和日常操作强相关的核心动作拆开讲:建库建表、增删改查、索引与性能、视图与存储过程、备份恢复、常见坑排查。尽量沿用前一篇的节奏,每一步都讲清“为什么这么做”,再给可以直接抄的写法。
如果你是刚装了 SQL Server 2019、正准备做课程设计或者公司小项目的人,恭喜你,这篇基本覆盖了你未来三个月最常用的数据库操作。如果你已经写了两年代码但对数据库底层的理解还停留在“能跑就行”,这篇也能帮你把很多模糊的概念理清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 建库与建表:把地基打得稳,后面才不折腾
2.1 CREATE DATABASE 不只是“建个库名”那么简单
很多初学者建库就是右键“新建数据库”,填个名字点确定,完事。这在学习环境里没毛病,但你稍微认真一点就会发现,SSMS 的图形界面背后其实给你塞了一堆默认值。当我们用 T-SQL 手动建库时,才能真正理解这些默认值意味着什么。
sql复制CREATE DATABASE ShopDB
ON PRIMARY
(
NAME = N'ShopDB',
FILENAME = N'D:\SQLData\ShopDB.mdf',
SIZE = 128MB,
MAXSIZE = 4GB,
FILEGROWTH = 64MB
)
LOG ON
(
NAME = N'ShopDB_log',
FILENAME = N'D:\SQLData\ShopDB_log.ldf',
SIZE = 64MB,
MAXSIZE = 2GB,
FILEGROWTH = 64MB
);
这里有几个点值得说道说道。
- 文件路径:mdf 是数据文件,ldf 是日志文件,两者尽量放在不同的物理磁盘上。日志文件是顺序写入的,数据文件是随机读写,混在一个盘里会互相拖慢。学习环境只有一块盘就算了,生产环境这是基本要求。
- 初始大小:我见过不少人默认 8MB 起步,结果数据一多频繁自动增长,每次增长还会产生 IO 抖动。SQL Server 的自动增长是“用完了才长”,增长时会有短暂的性能毛刺。所以宁可把初始大小给得宽松点,也别让它一天涨八回。
- MAXSIZE:不给上限会怎样?理论上可以一直涨到磁盘满。给一个上限对后续监控有好处,但别给得太小,否则业务高峰期报“数据库文件已满”你就等着挨骂吧。
再说一个经常被忽略的操作:建完库立刻做一次完整备份,并把数据库设为“简单恢复模式”或“完整恢复模式”,取决于你的数据安全性要求。别等到数据出问题了才想起来恢复模式这回事。
2.2 表结构设计:命名规范与类型选择是经验的体现
表设计是数据库操作里最能看出一个人水平的地方。SQL Server 2019 里 CREATE TABLE 的语法本身不复杂,复杂的是“你打算怎么组织你的数据”。
以一个简单的订单表为例:
sql复制CREATE TABLE dbo.Orders
(
OrderId INT IDENTITY(1,1) PRIMARY KEY,
OrderNo VARCHAR(32) NOT NULL,
CustomerId INT NOT NULL,
OrderStatus TINYINT NOT NULL DEFAULT 0,
TotalAmount DECIMAL(18,2) NOT NULL,
CreatedTime DATETIME2(3) NOT NULL CONSTRAINT DF_Orders_CreatedTime DEFAULT(SYSDATETIME()),
UpdatedTime DATETIME2(3) NULL
);
几个容易被忽略的细节:
- 为什么用 IDENTITY 而不是 GUID 做主键?因为聚集索引默认建在主键上,整数自增列做聚集索引时写入顺序和物理顺序基本一致,页拆分少。业务上需要对外暴露不可猜测的编号时,可以单独加一个业务编号列并建唯一索引。
- OrderStatus 用 TINYINT 而不是 VARCHAR(50) 存“待支付、已支付、已发货”?这是典型的枚举字段处理问题。TINYINT 占 1 字节,检索时走整数比较,性能远好于字符串比较。状态含义可以在前端或应用层映射,也可以建一张状态字典表。
- 金额用 DECIMAL(18,2),不用 FLOAT。这个我强调多少次都不够:FLOAT 是近似值,存金额算总账时会出现 0.1 + 0.2 = 0.30000000000000004 的尴尬。DECIMAL 是精确数值类型,才是钱的正经选择。
- 时间用 DATETIME2(3),而不是老旧的 DATETIME。DATETIME 的精度只有 3.33 毫秒,而且 2038 年问题虽然不直接命中它(它能到 9999 年),但精度和时区支持都不如 DATETIME2。SQL Server 2019 里习惯上新库一律 DATETIME2。
2.3 外键、索引与约束:数据库帮你兜底,而不是靠应用自觉
我经常看到一种论调:“外键影响性能,所以不用外键,靠应用层保证。”这种说法不是完全没道理,但要注意适用范围。大型互联网高并发场景下,外键确实可能成为写入瓶颈,但普通企业应用、课程设计、中小型系统,外键带来的完整性收益远大于那一点点性能损耗。
sql复制ALTER TABLE dbo.Orders
ADD CONSTRAINT FK_Orders_CustomerId
FOREIGN KEY (CustomerId) REFERENCES dbo.Customers(CustomerId);
这条 SQL 的意思是:Orders 表里的 CustomerId 必须在 Customers 表里存在。它防止你写出“查不到客户的订单”。如果没有外键,应用层做个判断也能拦住大部分情况,但总会有漏网之鱼——比如测试脚本灌数据时、运维直接改库时、两个应用共用一张表时。外键是数据库在说“这事我帮你盯着”,何乐而不为。
不过外键有一个坑:删除父表数据时,如果子表有引用,DELETE 会直接报错。你需要在删除前理清业务逻辑,要么先删子表,要么用逻辑删除(加 IsDeleted 字段标记),生产环境尤其不建议物理删除有业务关联的数据。
索引的话题我留到后面专门讲,建表阶段只要记住一句话:主键、唯一约束、外键会自动建索引,频繁查询的列可以随后按需补索引,千万别盲目“每个字段都建索引”,那会让插入变慢、磁盘膨胀、查询优化器选择困难。
3. 增删改查:你以为会了,其实坑都在细节里
3.1 INSERT:批量插入的性能分水岭
单条 INSERT 的语法实在没什么好讲的。真正拉开差距的是批量插入场景。
假设你有 10 万条订单数据要写入 Orders 表,第一种写法是一个循环逐条 INSERT,第二种是用 VALUES 拼接一次性插入,第三种是用 SELECT ... FROM 源表导入。它们的性能差异可能是一百倍。
在 SQL Server 2019 中,批量写入的常见姿势有:
sql复制-- 方式一:多行 VALUES
INSERT INTO dbo.Orders (OrderNo, CustomerId, TotalAmount, CreatedTime)
VALUES
('NO2025001', 1, 99.90, SYSDATETIME()),
('NO2025002', 2, 199.00, SYSDATETIME()),
('NO2025003', 3, 299.00, SYSDATETIME());
一次 INSERT 语句里带几百行 VALUES 是可行的,但别无脑拼一个几万行的超级语句,那样会超出参数上限或者把日志撑爆。更好的做法是利用 表值参数(Table-Valued Parameter) 从应用程序传入 DataTable 一次性写入,或者用 SqlBulkCopy。这些方式能有效减少网络往返和日志提交次数。
还有个小技巧:如果导入的数据不需要严格记录事务日志,可以考虑把目标表切换到简单恢复模式,导入完成后再切回完整模式并做一次备份。注意:这属于“用空间换时间”的操作,做好演练再上生产。
3.2 UPDATE 与 DELETE:忘加 WHERE 的代价有多大
“UPDATE 不打 WHERE”——每个数据库从业者都听说过这个段子,但真正经历过的人不多。我讲一个让我印象深刻的案例:某同事清理测试数据,写了一条:
sql复制DELETE FROM dbo.Orders;
他以为自己连的是测试库,其实连的是预发库。一秒钟后,Orders 表里的线上数据全没了。虽然事后从备份恢复了,但中间那段时间所有订单查询都返回空。这个教训告诉我们两件事:
- 执行 UPDATE 和 DELETE 前,先 SELECT 一下确认 WHERE 条件的命中范围。
- 在事务里操作,不要直接 AUTO COMMIT。
推荐写法是:
sql复制BEGIN TRANSACTION;
SELECT COUNT(*) FROM dbo.Orders WHERE OrderStatus = 0 AND CreatedTime < '2025-01-01';
-- 确认数量无误后再执行
DELETE FROM dbo.Orders WHERE OrderStatus = 0 AND CreatedTime < '2025-01-01';
-- 检查影响行数,符合预期才提交
COMMIT TRANSACTION;
如果影响行数不对,直接 ROLLBACK TRANSACTION 就能收手。有人嫌麻烦,但经历过一次误操作事故后,你会感谢这个习惯。
UPDATE 还有一个常见需求:关联另一张表更新。SQL Server 的 UPDATE 支持 FROM 子句,写法如下:
sql复制UPDATE o
SET o.TotalAmount = o.TotalAmount * 1.1
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE c.Level = 'VIP';
这种 UPDATE ... FROM ... JOIN 的写法在 SQL Server 里很常见,但要注意关联后可能出现一个目标行匹配多行的情况,那样 SQL Server 会“随机”选取其中一行的值来更新,结果不可控。所以用 JOIN UPDATE 前,最好先确认关联字段有唯一约束或者业务上能保证一对一。
3.3 SELECT:别再用 SELECT * 走天下了
我知道新手最喜欢 SELECT *,因为省事,打出来就完事。但它有几个问题:
- 当表结构发生变化,比如加了几个大字段(VARCHAR(MAX)、XML),SELECT * 会把不需要的大字段也查出来,网络传输和内存开销白白浪费。
- 在索引设计上,SELECT * 几乎可以肯定无法走覆盖索引,因为你要求返回所有列,索引里装不下。
- 代码的可读性也会受影响,别人看你的 SQL 根本不知道你关心哪些列。
正确的姿势是只查需要的列:
sql复制SELECT OrderId, OrderNo, TotalAmount, CreatedTime
FROM dbo.Orders
WHERE CustomerId = 123
ORDER BY CreatedTime DESC
OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;
OFFSET/FETCH 是 SQL Server 2012 以后推荐的分页方式,SQL Server 2019 自然支持。相比老式的 ROW_NUMBER() 窗口函数分页,OFFSET/FETCH 在简单分页场景下更简洁,但它有个缺点:页数越往后越慢。原因是数据库还是要扫描并丢弃前面所有的行。如果需要深度分页,更靠谱的做法是基于游标位置或上一页最后一条 ID 来取。
sql复制-- 推荐的高效翻页:带上一次查询拿到的最小/最大 ID 或时间
SELECT OrderId, OrderNo, TotalAmount, CreatedTime
FROM dbo.Orders
WHERE CustomerId = 123 AND OrderId > 100000
ORDER BY OrderId
OFFSET 0 ROWS FETCH NEXT 20 ROWS ONLY;
这种“键集分页”方式不会因为页码变大而性能变差,适合后台管理系统的列表查询。
4. 索引与性能:为什么你的查询越跑越慢
4.1 聚集索引与非聚集索引,一次讲明白
索引是 SQL Server 里最值得花时间理解的概念。不理解索引,你就无法理解为什么一条 SQL 有时候快有时候慢。
简单打个比方:一本字典(表)按拼音排序(聚集索引),你在目录里查某个字在第几页(非聚集索引),然后翻到那一页,这就是“书签查找”。如果你要查的字段刚好在目录里全都有,那都不用翻正文(覆盖索引)。
在 SQL Server 里,聚集索引决定了表数据的物理存储顺序,一张表只能有一个,通常建立在主键上。非聚集索引则是独立的存储结构,里面存放索引键值和指向数据行的定位符。
给 Orders 表加索引:
sql复制CREATE NONCLUSTERED INDEX IX_Orders_CustomerId_CreatedTime
ON dbo.Orders (CustomerId, CreatedTime)
INCLUDE (OrderNo, TotalAmount);
这条索引的作用是:当你按 CustomerId 筛选并按 CreatedTime 排序时,索引可以直接提供 OrderNo 和 TotalAmount,不需要回到主表取数据,这就叫覆盖索引。
建议多看执行计划,确认哪些查询走了 Table Scan 或 Key Lookup。如果一个查询经常出现 Key Lookup(书签查找),且返回行数很少,那这个查找开销往往还可以接受;但如果返回几万行还一个个回表,就要想办法改成覆盖索引或者调整查询逻辑。
4.2 索引不是越多越好,也不是建了就永远有效
索引的优点说完了,缺点也必须讲清楚:
- 每次 INSERT、UPDATE、DELETE,索引也要跟着更新。索引太多,写入会变慢。
- 索引占磁盘空间。真实生产环境里,索引空间经常比表数据本身还大。
- 统计信息过期会导致查询优化器做出错误的执行计划选择。
SQL Server 2019 的数据库引擎一般会自动更新统计信息,但大数据量下自动更新的阈值可能不够频繁,导致执行计划走偏。你可以手动更新关键表的统计信息:
sql复制UPDATE STATISTICS dbo.Orders;
不过别把它当成日常例行命令天天跑,这会增加额外 IO。通常做法是:在数据大批量导入后,或者发现某条 SQL 执行计划明显异常时,再手动更新。
还有索引碎片的问题。碎片率太高会让扫描效率变差,但也没必要对每个索引都频繁重建。经验值可以参考:
| 碎片率 | 处理方式 |
|---|---|
| 小于 5% | 无需处理 |
| 5% 到 30% | 重新组织索引(ALTER INDEX ... REORGANIZE) |
| 大于 30% | 重建索引(ALTER INDEX ... REBUILD) |
sql复制ALTER INDEX IX_Orders_CustomerId_CreatedTime ON dbo.Orders REORGANIZE;
ALTER INDEX IX_Orders_CustomerId_CreatedTime ON dbo.Orders REBUILD;
注意 REBUILD 是重度操作,大表上会长时间锁表或大量占用日志,尽量放在维护窗口执行。
4.3 学会看执行计划,比背一百个优化技巧都管用
SQL Server Management Studio 里有一条非常实用的快捷键:Ctrl+M,它会开启“实际执行计划”。你执行查询后,结果框里会多出一个“执行计划”页签,里面展示了每一步操作。
新手看执行计划最容易忽略两件事:
- 最贵的操作往往显示为“聚集索引扫描”或“RID 查找”。这不是说索引没用,而是你的查询条件没有被索引有效利用。
- 两个表 JOIN 时的连接运算符,是 Hash Match、Merge Join 还是 Nested Loop。如果看到 Hash Match,通常意味着 SQL Server 觉得两个输入集都比较大,没法用索引高效关联。这时候要审视 JOIN 列上是否有索引、WHERE 条件能否先行过滤。
举个例子,一条慢查询:
sql复制SELECT o.OrderNo, c.CustomerName
FROM dbo.Orders o
LEFT JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE o.TotalAmount > 1000;
执行计划如果显示 Orders 表扫描、Customers 表每行都做 Key Lookup,那多半是 Customers.CustomerId 上没有索引、或者 Orders 表缺一个覆盖 TotalAmount 的索引。
优化方向很简单——给 Orders.TotalAmount 建索引,或者把条件改成 o.TotalAmount 在某个范围内,减少扫描行数。
5. 视图、存储过程与参数化:让数据库更好维护
5.1 视图:封装复杂查询,但别滥用
视图本质是一段“保存下来的 SELECT”。它不存储数据,查询的时候动态生成结果集。用视图的好处是把复杂 JOIN 和过滤逻辑隐藏起来,业务层只面对一个简洁的表结构。
sql复制CREATE VIEW dbo.v_OrderDetail
AS
SELECT
o.OrderId,
o.OrderNo,
o.CustomerId,
o.TotalAmount,
o.CreatedTime,
c.CustomerName
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.CustomerId
WHERE o.OrderStatus <> 9;
查询时直接 SELECT * FROM dbo.v_OrderDetail WHERE CustomerId = 100。这个视图把 Orders 和 Customers 的关联封装了,业务侧不用关心底层表结构变化。但注意:视图上再套视图,性能可能一层层叠加损耗。视图不会帮你缓存什么,每层视图展开后都变成底层表操作。过度嵌套视图会让执行计划变得非常复杂,排查问题时也绕。我建议视图最多嵌套一层,超过就考虑拆出来写存储过程或直接改底层查询。
5.2 存储过程:不只是“把 SQL 存起来”
很多人喜欢在应用里写各种 SQL,理由是“这样不用维护数据库对象”。但这个习惯在复杂业务场景下会非常痛苦:SQL 散布在代码各处,改一个字段映射要全局搜索,还容易出现拼接 SQL 的注入风险。
存储过程的几个优势:
- 预编译执行计划,反复执行时减少编译开销(虽然 SQL Server 对普通 SQL 也有参数化缓存,但存储过程的计划更稳定)。
- 集中管理业务逻辑,数据库变更时只需要改一处。
- 天然参数化,防 SQL 注入。
- 可以配合事务做到原子操作,比如“先扣库存再生成订单”,在存储过程里控制明显更安全。
一个简单的存储过程示例:
sql复制CREATE OR ALTER PROCEDURE dbo.usp_GetOrdersByCustomer
@CustomerId INT,
@PageNumber INT = 1,
@PageSize INT = 20
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Offset INT = (@PageNumber - 1) * @PageSize;
SELECT OrderId, OrderNo, TotalAmount, CreatedTime
FROM dbo.Orders
WHERE CustomerId = @CustomerId
ORDER BY CreatedTime DESC
OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;
END;
这里每个点都有讲究:
CREATE OR ALTER是 SQL Server 2016 SP1 以后支持的语法,SQL Server 2019 可用,改写存储过程不用先 DROP 再 CREATE,权限也不会丢。SET NOCOUNT ON用来禁止输出“影响行数”的额外消息,减少网络流量。- 参数用
@开头,SQL Server 内部会做参数化处理,从机制上规避拼接字符串的注入风险。
调用:
sql复制EXEC dbo.usp_GetOrdersByCustomer @CustomerId = 100, @PageNumber = 2, @PageSize = 20;
5.3 临时表与表变量:什么时候用什么
写复杂查询时经常需要把中间结果暂存。SQL Server 里最常用的是 #临时表(本地临时表)和 @表变量。
sql复制-- 临时表
CREATE TABLE #TempOrders
(
OrderId INT,
TotalAmount DECIMAL(18,2)
);
INSERT INTO #TempOrders
SELECT OrderId, TotalAmount FROM dbo.Orders WHERE CreatedTime >= '2025-01-01';
-- 表变量
DECLARE @TempOrders TABLE
(
OrderId INT,
TotalAmount DECIMAL(18,2)
);
区别在于:
- 临时表会写 tempdb 并创建统计信息,擅长处理数据量较大(比如几千行以上)且需要 JOIN 的场景。
- 表变量完全在内存/内存压力下工作,没有统计信息,优化器往往低估其行数,数据量大时性能可能崩。
- 表变量适合小规模数据、循环处理或作为存储过程参数传入传出的场景。
还有一个细节:临时表的会话隔离是自动的。不同会话建同名的 #TempOrders 不会冲突,因为它物理上带着 session 后缀。如果你想让多个存储过程共享一份数据,可以用全局临时表 ##TempOrders,但用完记得手动删,否则会一直占着 tempdb。
6. 备份、恢复与导入导出:数据安全是最后的底线
6.1 备份策略:为什么不能只备份一次就高枕无忧
数据库操作里最容易被忽视的就是备份。学习阶段大家通常觉得“反正就是测试数据,丢就丢了”,但一旦上了生产环境,备份缺失就是事故的起点。
SQL Server 的备份类型大致分三种:
- 完整备份:备份整个数据库的数据文件和部分日志,是恢复的基础。
- 差异备份:基于最后一次完整备份,备份之后发生变化的数据。备份体积小、速度快,但恢复时依赖上一次完整备份。
- 事务日志备份:只在完整恢复模式下有意义,记录自上一次日志备份以来的所有事务。可以把数据库恢复到任意时间点。
典型策略:每天凌晨一次完整备份,每 4 小时一次差异备份,每 15 分钟一次日志备份(如果业务容忍度更高,频率还可以更高)。
命令行方式最简单直接:
sql复制BACKUP DATABASE ShopDB
TO DISK = N'D:\SQLBackup\ShopDB_FULL_20250601.bak'
WITH INIT, NAME = N'ShopDB-Full Database Backup', CHECKSUM;
这里有两个点值得展开:
CHECKSUM会在备份时校验页级校验和,恢复时可以较早发现介质损坏或备份文件损坏。不要为了省一点时间忽略它。INIT表示覆盖同名文件。如果不用 INIT,默认是追加到文件里,文件会越变越大,恢复时还可能读到多个备份集,容易搞混。
定期做恢复演练也是必须的。只做备份不做恢复演练,等于把保险箱钥匙丢了却以为一切安全。
6.2 恢复模式与时间点恢复
SQL Server 有三种恢复模式:完整(FULL)、大容量日志(BULK_LOGGED)、简单(SIMPLE)。
- 简单模式下不能做日志备份,上一次完整备份之后的数据基本无法找回。
- 完整模式可以做到时间点恢复,代价是日志文件会持续增长,必须配合定期日志备份来截断日志。
- 大容量日志模式在批量导入时能显著减少日志写入量,但会破坏日志链的连续性,一般只在批量导入前后临时切换。
恢复操作示例:
sql复制RESTORE DATABASE ShopDB
FROM DISK = N'D:\SQLBackup\ShopDB_FULL_20250601.bak'
WITH REPLACE, RECOVERY;
如果把生产库恢复到一个临时环境用于查数,建议使用 WITH NORECOVERY 恢复完整备份和不带恢复的差异备份,然后最后一条日志备份用 RECOVERY。这样可以把库恢复到最近的时间点。
6.3 导入导出:面对 Excel 和 CSV 的常规操作
数据迁移是日常高频动作。SQL Server 2019 提供了图形化的“导入/导出向导”,但如果是自动化任务,推荐用 BCP 命令或 OPENROWSET。
一个典型的 CSV 导入场景:
bash复制bcp ShopDB.dbo.Orders format nul -T -c -x -f "D:\orders_format.xml"
bcp ShopDB.dbo.Orders in "D:\orders_data.csv" -T -c -f "D:\orders_format.xml" -b 5000
不过 BCP 是传统的命令行工具,初次接触格式文件会觉得不太友好。也可以用 SQL Server 2019 的“导入平面文件”向导逐步完成,手工操作时比较直观。
使用 OPENROWSET 加载 Excel 的场景:
sql复制SELECT *
INTO dbo.Orders_Import
FROM OPENROWSET(
'Microsoft.ACE.OLEDB.12.0',
'Excel 12.0;Database=D:\orders.xlsx',
'SELECT * FROM [Sheet1$]'
);
这个操作取决于服务器上是否安装了 ACE OLEDB 驱动,而且 64 位环境下经常踩“未注册提供程序”的坑。能不依赖 Excel 还是尽量用 CSV + BCP 或者 SSIS,稳定性好很多。
7. 常见问题与排查技巧实录
7.1 “无法连接到实例”的完整排查路径
新手遇到“无法连接”,第一反应是重装 SQL Server。其实 80% 以上的问题都不是装坏了,而是网络、协议或服务的问题。
按这个顺序排查肯定靠谱:
- SQL Server 服务是否在运行?检查 Windows 服务里的 SQL Server (MSSQLSERVER) 或命名实例服务。
- 网络协议是否启用?SQL Server 配置管理器里确认 TCP/IP 协议已启用。默认实例端口是 1433,命名实例需要 SQL Browser 服务配合。
- Windows 防火墙是否放行?入站规则里允许 1433 端口或者允许 sqlservr.exe。
- 登录账号是否有权限?如果是 Windows 身份验证能连、SQL Server 身份验证不能连,重点检查“服务器身份验证模式”是不是改成了 Windows Only。
最有用的排查工具是 SQL Server 配置管理器里的“客户端协议”,以及命令行:
bash复制sqlcmd -S localhost -E
如果 sqlcmd 能连上,说明服务本身没问题,多半是 SSMS 的连接参数或网络问题。
7.2 数据库进入“可疑”状态怎么办
数据库突然在 SSMS 里显示“可疑(Suspect)”,很多人会慌。这个状态通常意味着 SQL Server 启动时无法访问数据库文件或日志文件损坏。
处理流程大致是:
- 先把数据库置为紧急模式,尝试只读访问数据:
sql复制ALTER DATABASE ShopDB SET EMERGENCY;
- 检查完整性并尝试修复:
sql复制DBCC CHECKDB (ShopDB) WITH NO_INFOMSGS, ALL_ERRORMSGS;
- 如果只报分配错误和一致性错误,可以尝试:
sql复制ALTER DATABASE ShopDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DBCC CHECKDB (ShopDB, REPAIR_REBUILD);
ALTER DATABASE ShopDB SET MULTI_USER;
注意,REPAIR_REBUILD 可能会丢失部分数据,执行前尽量先做文件拷贝备份。
生产环境里遇到“可疑”状态,首要动作永远是立刻停止用户写入,然后联系 DBA 或者经验更丰富的人做恢复,而不是自己开着 REPAIR 一顿操作。
7.3 死锁(Deadlock)到底怎么抓
死锁是两个会话互相等对方占用的资源,SQL Server 会主动选择一个会话作为牺牲者并回滚它的整个事务。遇到死锁错误(错误码 1205),不要光看“到底谁先抢到锁”,要从代码层面解决问题。
抓死锁最常用的方式是开启跟踪标志:
sql复制DBCC TRACEON(1222, -1);
这个标志会把死锁信息写入 SQL Server 错误日志。等死锁发生后,去错误日志里就能看到死锁图,包括两个会话各自执行的 SQL、持有的锁类型、等待的资源。有了这些信息,才能判断是索引缺失、事务过长还是锁顺序不一致导致的问题。
几条经验:
- 事务里操作表的顺序尽量全局一致,比如先更新 Orders 再更新 OrderItems,别一个地方反过来写。
- 事务尽量短,尤其不要在一个数据库事务里做 HTTP 调用、等待用户输入或长时间计算。
- 加 FOR UPDAT 之类的锁提示时要非常谨慎,用不好会放大阻塞范围。
7.4 日志文件无限增长的常用解法
一个数据库运行久了,日志文件可能膨胀到几十 GB 甚至几百 GB。这时应用还是正常的,但磁盘空间报警。
先看日志空间使用率:
sql复制DBCC SQLPERF(LOGSPACE);
如果 Log Space Used % 很低但文件很大,说明日志文件被撑大过,但一直没有收缩。常规做法是备份日志后收缩文件:
sql复制-- 完整恢复模式下先备份日志
BACKUP LOG ShopDB TO DISK = N'D:\SQLBackup\ShopDB_log.trn';
-- 再收缩日志文件
DBCC SHRINKFILE (N'ShopDB_log', 1024); -- 目标压缩到 1GB
注意:不要养成“日常收缩日志文件”的习惯。每次自动增长都是有性能成本的,你刚收缩完,业务一忙又会自动增长,反复膨胀收缩会让文件碎片化。正确的做法是:把日志文件初始大小和自动增长设置到合理值,然后让它稳定增长即可。
7.5 一个最常被忽略的坑:tempdb 竞争
SQL Server 2019 安装后默认只有一个 tempdb 数据文件,在高并发下很容易出现 PAGELATCH 等待。真实场景中,如果大量使用临时表、排序、哈希联合等操作,tempdb 会成为瓶颈。
解决方案是增加 tempdb 数据文件,并且让文件大小一致。有人问:为什么不直接建一个超大文件?有经验的 DBA 知道,多个大小相同的数据文件能让 SQL Server 按比例分配写入,减少分配页争用。建议是:
- 在 SQL Server 2019 上,tempdb 数据文件数量等于 CPU 核心数(或者按 0.25 到 1 倍逻辑核心数调整,不必超过 8 个)。
- 每个文件的初始大小保持一致,比如 512MB。
- 自动增长按相同的量增长,避免文件大小失衡。
注意:修改 tempdb 文件后需要重启 SQL Server 服务才能完全生效,所以请在维护窗口操作。
8. 顺手记几个 SQL Server 2019 的新特性
这一篇虽然是“从入门到熟练操作”,但 SQL Server 2019 带来了不少新能力,你可以在实践中逐渐感受到差异。
- 智能查询处理(Intelligent Query Processing):包括行模式内存授予反馈、近似查询处理、表变量延迟编译等,很多原本需要手工优化的问题,2019 会自动帮你处理。比如表变量问题,在兼容级别 150 时会启用延迟编译,效果比老版本好很多。
- 加速数据库恢复(ADR):数据库崩溃后重启恢复时间大幅缩短,长事务不再导致日志无限回滚。这个对运维是大福利,但需要数据库设为完整恢复模式。
- 轻量级查询分析(LQS):以前监控活动查询要用复杂的 DMV,2019 里查询计划里能直接看到运行中的轻量级分析信息,抓慢查询更方便。
不过要留意:新特性依赖数据库兼容级别。一个 2019 实例上如果挂着一个兼容级别 130 的旧库,很多新特性不会生效。想体验全部新能力,需要把数据库兼容级别改成 150:
sql复制ALTER DATABASE ShopDB SET COMPATIBILITY_LEVEL = 150;
这个操作通常不会引起功能问题,但建议在测试环境先验证所有应用查询正常再上生产。兼容级别本质上决定了查询优化器使用的模型版本,升级后可能改变执行计划,让某些 SQL 变快或变慢。
9. 一些面向实战的小建议
学 SQL Server 2019,光看教程是不够的,一定要上手搭一套自己的实验环境。没有服务器的话,直接在 Windows 10/11 上装 Developer 版就够了,功能和企业版完全一致,不用于生产就不用许可费。装好后找一份网上公开的数据集,比如订单表、用户表、产品表,自己写几十条查询,再制造几个性能问题练手,进步速度远比“看十篇教程”快。
做数据库相关的课程设计时,建议先画出业务表的关系图,梳理清楚主外键,再动手建库。调整字段类型和关系的时间成本,远比建完库再改便宜得多。
我自己的体会是,数据库操作这件事最难的从来不是语法,而是“出了问题时的思路”。建表好不好、SQL 优不优化,往往不在一开始被人注意,直到并发上来或者数据量过了千万级,差距才开始显现。不要怕踩坑,把每次踩坑的排查过程记录下来,用不了多久你就能从“照着文档敲”变成“遇到问题能快速定位”。SQL Server 2019 的功能非常庞大,这篇带你把最核心的日常操作过一遍,后面再遇到具体问题,就知道往哪个方向查了。
