托管 SQL Server 的运维边界:哪些 DBA 手段会失效,以及用什么替代原创
从自建 SQL Server 转到云托管实例(AWS RDS / Azure SQL MI 这类),最容易踩的不是性能问题,
而是手上那套 DBA 动作有一半会直接报权限错。托管服务把 sysadmin 收走了,你拿到的是一个
被裁剪过的高权限账号——能做绝大部分日常运维,但凡是「可能动到实例底层或宿主机」的操作全部封死。
这篇把边界逐条实测一遍:每条给出真实报错、失效原因,以及托管环境下的替代做法。 实例是一台 SQL Server 2019 Enterprise(RTM-CU32-GDR,15.0.4455.2)托管实例, 4 个用户库 + 一个服务商专用管理库,最大的业务库约 48 GB。
# 一、先确认自己站在哪一侧
上来先跑这一条,它决定了后面所有判断:
SELECT IS_SRVROLEMEMBER('sysadmin') AS IsSysAdmin;
托管实例上这里返回 0。不用怀疑账号配错了——这是设计如此。试图自己加进去会得到:
Msg 15151, Level 16, State 1
Cannot alter the server role 'sysadmin', because it does not exist or you do not have permission.
2
报错文本里的 "does not exist" 有迷惑性:sysadmin 当然存在,只是你连它的元数据都看不到,
SQL Server 对无权限对象一律报「不存在或无权限」。看到这条不要去排查角色是否被删,直接接受边界。
顺带一个容易误判的点:SERVERPROPERTY('MachineName') 会返回一个云厂商自动生成的主机名
(形如 EC2AMAZ-XXXXXXX)。这台机器你永远登不上去,这个名字只有一个用途——确认发生过实例替换。
# 二、逐条失效清单
下面每条都是实测跑出来的报错,不是文档摘抄。
# 1. xp_cmdshell —— 彻底没有
EXEC xp_cmdshell 'dir';
Msg 229, Level 14, State 5
The EXECUTE permission was denied on the object 'xp_cmdshell',
database 'mssqlsystemresource', schema 'sys'.
2
3
注意报错落在 mssqlsystemresource 上——不是「没开启」,是执行权限在资源库层面就被拒,
所以 sp_configure 那条常见的启用命令也救不回来(而 sp_configure 本身也被封,见第 4 条)。
替代:任何依赖 xp_cmdshell 的运维脚本(调 robocopy、读目录、拉外部文件)都必须重写为
「数据库外部的调度器 + 只读 SQL」。把 shell 那一层挪到你自己的运维机上,SQL 只负责取数。
# 2. BACKUP / RESTORE DATABASE —— 被完全接管
BACKUP DATABASE [业务库] TO DISK = 'C:\temp\test.bak';
Msg 262, Level 14, State 1
BACKUP DATABASE permission denied in database '业务库'.
Msg 3013, Level 16, State 1
BACKUP DATABASE is terminating abnormally.
2
3
4
RESTORE 同理:
Msg 3110, Level 14, State 1
User does not have permission to RESTORE database '业务库'.
2
这是影响最大的一条。自建时代那套「备份策略 = 我自己写的 SQL Agent 作业」在托管上完全不成立:
备份是托管服务的职责,你只能通过控制台/API 设置保留期与备份窗口,
以及走服务商提供的原生备份存储过程(RDS 是 rdsadmin 库里那组 rds_* 过程)
把 .bak 导入/导出到对象存储。
替代:
- 日常备份 → 交给托管快照 + PITR,不要自己造;
- 需要把库搬到别处 → 走服务商的原生备份过程导出到对象存储,再在目标端导入;
- 恢复演练必须换套路:托管环境下「验证备份可用」等于「实际发起一次时间点还原到新实例」,
没有办法在原实例上
RESTORE VERIFYONLY。这一步很多团队迁上云后就悄悄不做了,是真实风险。
# 3. DBCC 的缓存类命令 —— 拒绝
DBCC DROPCLEANBUFFERS;
DBCC FREEPROCCACHE;
2
Msg 2571, Level 14, State 1
User '<账号>' does not have permission to run DBCC DROPCLEANBUFFERS.
2
两条都是 Msg 2571。这直接影响的是性能测试方法论:自建时代「清缓存 → 跑 SQL → 看冷启动耗时」
这套对比手段在托管上做不了。
替代:改用 SET STATISTICS IO, TIME ON + 执行计划里的实际读页数做横向对比,
不追求冷缓存下的绝对耗时,只比较同一缓存状态下不同写法的逻辑读差异。逻辑读是与缓存无关的稳定指标,
本来就比墙钟时间更适合做优化对比。
注意不是所有 DBCC 都被封——只读的诊断类仍然可用:
DECLARE @T TABLE (TraceFlag INT, Status INT, Global INT, [Session] INT);
INSERT INTO @T EXEC('DBCC TRACESTATUS(-1)');
SELECT * FROM @T;
2
3
这条能正常返回。判断规律很简单:只读的 DBCC 基本都在,会改变实例状态的一律没有。
# 4. sp_configure —— 读得到,改不了
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
2
Msg 15247, Level 16, State 1
User does not have permission to perform this action.
Msg 5812, Level 14, State 1
You do not have permission to run the RECONFIGURE statement.
2
3
4
但不带参数的 EXEC sp_configure; 是可以跑的——你能看到全部实例级配置的当前值,
只是不能改。这个「可读不可写」的组合在托管环境里非常典型,值得单独记住:
巡检脚本里所有读配置的部分照常工作,所有改配置的部分要挪到控制台的参数组里去。
替代:实例级参数(max degree of parallelism、cost threshold、max server memory……) 统统改走托管服务的参数组,改完通常需要一次重启或等待动态生效——这也意味着 「临时调一下 MAXDOP 试试效果」这种操作在生产上成本变高了,要提前规划变更窗口。
# 5. SQL Server Agent —— 看不到自己的作业
EXEC msdb.dbo.sp_help_job;
Msg 229, Level 14, State 5
The EXECUTE permission was denied on the object 'sp_help_job',
database 'msdb', schema 'dbo'.
2
3
Agent 在托管实例上是被服务商用来跑内部维护任务的,你通常拿到的是一个受限子集 (能建作业但看不到全局),具体范围因服务商与版本而异——上线前一定要实测一遍,别照抄文档。
替代:把定时任务移出数据库。用外部调度器(cron / 托管调度服务 / 你自己的运维平台) 连上来执行 SQL,好处是作业定义、日志、告警都在你自己的可观测体系里, 不用再为「作业失败了但没人知道」写一套 Agent 通知。
# 6. 文件系统 —— 路径可见,内容不可及
SELECT physical_name FROM sys.database_files;
这条能跑,会返回形如 D:\rdsdbdata\DATA\<库名>.mdf 的真实路径。
但这只是元数据——没有 xp_cmdshell、没有 OPENROWSET(BULK) 的本地路径权限,
你看得到路径,碰不到文件。
这个组合会造成一个具体的误判:看到 D:\rdsdbdata\ 就以为能按自建思路规划文件布局。
实际上数据文件放在哪个盘、怎么分卷,全部由托管层决定,你唯一能控制的是
文件组与文件的逻辑划分(下一节会用到)。
# 7. 边界的另一半:rds_* 那组过程
被拿走的能力,服务商会以存储过程的形式还回来一小部分。在 RDS 上它们分散在
rdsadmin 与 msdb 里,用这条列出来:
SELECT name FROM sys.procedures WHERE name LIKE 'rds[_]%' ORDER BY name;
覆盖的是「必须有但又不能给你 sysadmin」的那几件事:原生备份还原、库的删除与重命名、 排序规则修改、日志文件收缩等。迁移前把这份清单和你现有的运维脚本对一遍, 每条被封的操作要么在这里找到对应过程,要么就得改架构。
# 三、还能做的部分:托管环境下的 DMV 巡检
边界看完了,反过来说好消息:DMV 几乎全部可用。托管收走的是「改」,不是「看」。 日常巡检的主力——慢查询、等待、阻塞、索引、容量——一条都没少。
下面三块是我实际在跑的,附上托管环境特有的判读陷阱。
# 1. 容量水位:要按文件看,不要按库看
SELECT
name AS [FileName],
type_desc AS [Type],
CAST(size * 8.0 / 1024 / 1024 AS DECIMAL(10,2)) AS [SizeGB],
CAST(FILEPROPERTY(name, 'SpaceUsed') * 8.0 / 1024 / 1024 AS DECIMAL(10,2)) AS [UsedGB],
CAST((size - FILEPROPERTY(name, 'SpaceUsed')) * 8.0 / 1024 / 1024 AS DECIMAL(10,3)) AS [FreeGB],
CASE WHEN max_size = 0 THEN 'NoGrow'
WHEN max_size = -1 THEN 'Unlimited'
ELSE CONVERT(VARCHAR(20), max_size * 8.0 / 1024 / 1024) + 'MB' END AS [MaxSize],
growth AS [Growth8KB],
is_percent_growth AS [IsPct]
FROM sys.database_files
ORDER BY type_desc, name;
2
3
4
5
6
7
8
9
10
11
12
13
一台真实业务库的输出(库名已脱敏):
| FileName | Type | SizeGB | UsedGB | FreeGB | MaxSize | Growth(8KB页) |
|---|---|---|---|---|---|---|
| 业务库_log | LOG | 8.76 | 0.03 | 8.73 | 2048MB | 32768 |
| 业务库 | ROWS | 2.19 | 1.93 | 0.26 | Unlimited | 16384 |
| 业务库_Money03 | ROWS | 43.85 | 40.06 | 3.79 | Unlimited | 65536 |
| 业务库_Report01 | ROWS | 1.07 | 0.75 | 0.32 | Unlimited | 8192 |
这张表要看三件事,第三件是托管环境下最容易被忽略的:
- 单文件剩余空间,不是整库剩余。上面这个库整体看还很宽松,但
Money03这个文件 已经用掉 91%,热点全在它身上——按库汇总的告警看不出来。 Growth8KB换算成实际大小:65536 × 8KB = 512MB,8192 × 8KB = 64MB。 增长步长过小会导致频繁自动增长(每次都是一次同步的文件扩展,期间写入会卡), 过大则单次扩展时间长。几十 GB 量级的热文件,512MB 是合理的;64MB 偏小。is_percent_growth必须是 0。百分比增长在文件变大后会失控——一个 44GB 的文件按 10% 增长, 单次要扩 4.4GB。这条在自建时代也成立,但托管环境下你无法用瞬时 I/O 指标去印证扩展带来的抖动 (宿主机指标不归你),只能靠事前把它配对。
另外注意 tempdb:这台实例的 tempdb 有 223 GB,实际用量 0.06 GB。
托管服务通常按实例规格预分配一个很大的 tempdb,看到它「几乎全空」不是异常,
不要照搬自建时代「tempdb 使用率」那套告警阈值,会一直不触发、形同虚设。
真正该盯的是 tempdb 的瞬时增长与版本存储占用,不是稳态使用率。
# 2. 等待统计:托管实例的噪音更大
标准写法是 sys.dm_os_wait_stats 排序取 top,但必须过滤空闲等待,否则结论全错。
先看一个没过滤干净的真实输出:
| wait_type | wait_time (秒) | waiting_tasks |
|---|---|---|
| SOS_WORK_DISPATCHER | 93,790,486 | 402,317,317 |
| HADR_FILESTREAM_IOMGR_IOCOMPLETION | 1,342,875 | 2,680,068 |
| HADR_CLUSAPI_CALL | 1,342,871 | 5,364,516 |
| DIRTY_PAGE_POLL | 1,342,865 | 13,299,100 |
| QDS_PERSIST_TASK_MAIN_LOOP_SLEEP | 1,342,824 | 22,381 |
| SP_SERVER_DIAGNOSTICS_SLEEP | 1,342,803 | 1,881,340 |
| CXPACKET | 24,971 | 13,153,871 |
| CXCONSUMER | 19,668 | 8,812,995 |
| BACKUPIO | 5,196 | 2,586,306 |
前六行全是噪音,一条都不能用来做判断:
SOS_WORK_DISPATCHER是后台工作线程池的空闲等待,数值大得离谱是正常的;- 中间四条的等待时长几乎完全相同(都是 1,342,8xx 秒)——这个「整齐」本身就是识别信号: 它们都等于实例的正常运行时长(约 15.5 天),说明这些线程从启动起就一直在睡, 从没被真正阻塞过。看到一组数值高度接近且约等于 uptime 的等待类型,直接全部划掉。
有意思的是那两条 HADR_*。这个实例并没有配置可用性组,为什么会有 Always On 相关的等待?
因为托管服务的多可用区高可用就是用 Always On 实现的,只是控制面在服务商手里。
这算是托管边界的一个侧写:底层机制还在,只是你既看不到也管不了它——
既然管不了,就更没有理由把它算进你的等待分析里。
过滤之后,真正有信息量的是最后三条:CXPACKET / CXCONSUMER 说明存在并行查询,
且 CXPACKET(协调线程等待)比 CXCONSUMER(消费者等待)高,
提示并行度分配可能不均——但在托管环境下你不能直接改 MAXDOP 去验证,得走参数组变更窗口(见第 4 条)。
BACKUPIO 的存在则印证了托管备份确实在按窗口跑。
一份可以直接用的过滤版本:
SELECT TOP 10
wait_type,
wait_time_ms / 1000.0 AS wait_time_sec,
waiting_tasks_count,
wait_time_ms / NULLIF(waiting_tasks_count, 0) AS avg_wait_ms
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'CLR_SEMAPHORE','LAZYWRITER_SLEEP','RESOURCE_QUEUE','SLEEP_TASK',
'SLEEP_SYSTEMTASK','WAITFOR','LOGMGR_QUEUE','CHECKPOINT_QUEUE',
'REQUEST_FOR_DEADLOCK_SEARCH','XE_TIMER_EVENT','BROKER_TO_FLUSH',
'BROKER_TASK_STOP','CLR_MANUAL_EVENT','CLR_AUTO_EVENT',
'DISPATCHER_QUEUE_SEMAPHORE','FT_IFTS_SCHEDULER_IDLE_WAIT',
'XE_DISPATCHER_WAIT','XE_DISPATCHER_JOIN',
-- 下面这些是托管实例上必须补的
'SOS_WORK_DISPATCHER','DIRTY_PAGE_POLL','SP_SERVER_DIAGNOSTICS_SLEEP',
'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','QDS_ASYNC_QUEUE',
'HADR_FILESTREAM_IOMGR_IOCOMPLETION','HADR_CLUSAPI_CALL',
'HADR_WORK_QUEUE','HADR_TIMER_TASK','HADR_LOGCAPTURE_WAIT'
)
ORDER BY wait_time_ms DESC;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
还有一个前提容易忘:wait_stats 是自实例启动以来的累计值,托管实例会因为维护窗口、
版本升级、可用区切换而重启,计数随之清零。所以做趋势对比必须自己落一份快照按差值比,
直接看绝对值只能得到「从上次重启到现在」这个含糊的口径。
# 3. 慢查询:按累计耗时排,不要按单次
SELECT TOP 10
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,
qs.execution_count,
CAST(qs.total_elapsed_time / 1000000.0 AS DECIMAL(12,2)) AS total_elapsed_sec,
CAST(qs.last_elapsed_time / 1000000.0 AS DECIMAL(12,2)) AS last_elapsed_sec,
qs.total_logical_reads
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_elapsed_time DESC;
2
3
4
5
6
7
8
9
10
11
真实输出的形态(SQL 文本已截断脱敏):
| 执行次数 | 累计耗时(秒) | 单次耗时(秒) | 累计逻辑读 | 形态 |
|---|---|---|---|---|
| 11,832,758 | 27,283 | 0.01 | 21.9 亿 | 带临时表的 CTE 查询 |
| 24,097,069 | 4,886 | 0.00 | 39.0 亿 | ORM 生成的单表按条件取列 |
| 23,944,371 | 4,803 | 0.00 | 38.8 亿 | 同一张表的 COUNT(*) |
| 8,242 | 2,901 | 0.06 | 1,405 万 | 报表聚合 |
| 44,829 | 2,536 | 0.05 | 3,046 万 | 递归 CTE 统计注册数 |
这张表最该讲的一点:排在第一梯队的全是单次 0.00~0.01 秒的查询。 按「单次执行超过 N 秒」的传统慢查询阈值去抓,这些一条都抓不到, 但它们贡献了绝大部分的累计耗时和几十亿次的逻辑读——真正压垮实例的是高频小查询,不是偶发大查询。
第 2、3 行尤其典型:同一张表,一条取数据一条 COUNT(*),
执行次数都是 2400 万级、逻辑读都在 39 亿级——这是典型的 ORM 分页写法
(先 count 总数再取当页),两条查询扫的是同一份数据。这类问题在慢查询日志里永远看不见,
只能靠 dm_exec_query_stats 按累计量排出来。
判读顺序建议:先按 total_elapsed_time 排一遍找总量大户,
再用 total_logical_reads / execution_count 算单次平均读页数找「本可以走索引却在扫表」的,
最后才看 last_elapsed_time 确认现在还在不在跑。只看其中一个维度都会漏。
同样注意这份数据的生命周期:dm_exec_query_stats 依附于计划缓存,
计划被逐出、重编译或实例重启,统计就没了。托管环境下你还不能用 DBCC FREEPROCCACHE
主动控制这个时机(见第 3 条),所以要么接受口径模糊,要么自己定时落快照。
# 四、一张迁移前的自查表
把上面的边界压缩成可执行的检查项,迁移前逐条过:
| 检查项 | 自建时的做法 | 托管上的结论 |
|---|---|---|
| 备份策略 | 自己写 Agent 作业 + 本地磁盘 | 必须重做:托管快照 + PITR;恢复演练改为「还原到新实例」 |
依赖 xp_cmdshell 的脚本 | 直接调 shell | 必须重写:shell 移到库外,SQL 只取数 |
| 实例参数调整 | sp_configure + RECONFIGURE | 改走参数组,需要变更窗口;sp_configure 仍可只读查看 |
| 定时任务 | SQL Server Agent | 移到外部调度器,顺带把告警接进自己的监控 |
| 性能对比测试 | 清缓存跑冷启动 | 改用逻辑读做对比指标 |
| 文件布局规划 | 自己分盘 | 只能控制文件组的逻辑划分,物理布局归托管层 |
| 容量告警 | 按库剩余空间 | 改为按文件;tempdb 稳态使用率阈值作废 |
| 等待统计基线 | 累计值直接看 | 自己落快照按差值比;过滤列表要补托管特有的噪音项 |
| 慢查询阈值 | 单次耗时 > N 秒 | 补上按累计耗时排的维度,否则漏掉高频小查询 |
# 小结
托管数据库的核心变化不是「少了几个命令」,而是运维的落点从实例内部挪到了实例外部: 调度移到外部调度器、备份移到服务商的控制面、参数变更移到变更窗口、 性能对比从依赖实例状态改为依赖与状态无关的指标。
而剩下留在你手里的部分——DMV 全家桶——反而变得更重要了: 在一个碰不到宿主机、看不到磁盘、改不了缓存的环境里,DMV 几乎是你唯一的观测入口。 把巡检脚本按上面那张表核一遍,比纠结失去的那些权限有用得多。
站内相关:SQL Server 2019 for Linux 安装与配置 Always On 高可用集群实战教程(自建侧的对照)