在 SQL Server 等关系型数据库中,存储过程执行时出现内存不足,通常并不是服务器物理内存真的耗尽,而是会话级内存配额、缓冲池压力或查询计划申请的内存 grant 超过了当前可用值。面对这类问题,使用内存优化表承接中间数据,或将大事务拆成分段批处理,是两种工程上成熟且互不影响思路。

一、内存优化表解决内存压力的原理与用法
内存优化表(Memory-Optimized Table)将行数据完全驻留在内存的用户空间,不依赖缓冲池页结构,也不会因磁盘临时表产生大量随机 IO。存储过程中原本写入 #temp 表或表变量的大型中间集,如果访问模式以插入和按主键点查为主,放到内存优化表后可以避免 plan cache 里的 memory grant 膨胀。
需要注意,内存优化表对事务日志和本地编译过程更友好,但并非所有场景都适合。若中间结果需要复杂联结和聚合,且行宽很大,内存占用反而可能高于传统表。创建时须指定 WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY),后者表示只保留结构不持久化数据,非常适合暂存。
-- 创建仅保留结构的内存优化表作为暂存区
CREATE TABLE dbo.stage_orders
(
order_id BIGINT NOT NULL PRIMARY KEY NONCLUSTERED,
customer_id INT NOT NULL,
amount DECIMAL(18,2) NOT NULL
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);
GO
-- 在存储过程中使用
CREATE PROCEDURE dbo.proc_load_stage
AS
BEGIN
DELETE FROM dbo.stage_orders;
INSERT INTO dbo.stage_orders (order_id, customer_id, amount)
SELECT order_id, customer_id, amount
FROM dbo.orders
WHERE order_date >= DATEADD(DAY, -1, GETDATE());
END;
GO
上面的脚本把昨日订单放进内存优化表,后续步骤直接基于 stage_orders 做计算,避免了在每次调用时重复申请大块临时空间。由于 SCHEMA_ONLY 不写数据日志,写入速度也明显优于磁盘临时表。
不过内存优化表也有约束:不支持某些文本类型、外键引用受限、索引必须为哈希或非聚集内存优化类型。在迁移旧过程前,应先评估字段类型和并发写入量,防止出现编译期报错。
二、分段处理降低单批次内存占用的做法
分段处理的核心是把原本一次性 INSERT...SELECT 或 UPDATE 的大集合,按主键区间或行数拆成多批,每批处理完立即提交并释放游标或表变量。这样单条语句申请的 memory grant 始终处于低位,会话不会触碰上限。
常见实现是用 WHILE 循环配合 TOP 或偏移量。下面示例将百万级订单状态更新分为每批两万行,处理间隔可选做短暂等待以平滑 IO。
CREATE PROCEDURE dbo.proc_update_segment
AS
BEGIN
DECLARE @batch INT = 20000;
DECLARE @rows INT = 1;
WHILE @rows > 0
BEGIN
UPDATE TOP (@batch) o
SET o.flag = 1
FROM dbo.orders o
WHERE o.flag = 0;
SET @rows = @@ROWCOUNT;
-- 可选:WAITFOR DELAY '00:00:01';
END
END;
GO
分段处理不依赖特殊表结构,对任何版本数据库都适用。它的缺点是总执行时间变长,且在分批边界如果不用事务包裹,可能出现部分可见状态。工程上通常每批包一个显式事务,保证每批原子性。
与内存优化表相比,分段处理不改变数据存储位置,只是改变提交节奏;因此它更擅长解决“单次语句内存 grant 过大”的问题,而对“临时表换页导致缓冲池紧张”的帮助有限。
三、两种方案的组合与容量估算
在真实业务里,二者经常组合:用内存优化表承接分段产出的小批量中间结果,既避免磁盘压力,又控制单批规模。容量估算时,内存优化表每行开销约等于行身加索引指针,万级行通常只占几十 MB;分段大小则建议设在使单批 memory grant 不超过实例授予上限百分之五的水平。
| 方案 | 适用瓶颈 | 主要优点 | 主要限制 |
|---|---|---|---|
| 内存优化表 | 缓冲池换页、磁盘临时表 IO | 读写快、无日志持久化可选 | 类型与索引受限 |
| 分段处理 | 单语句内存 grant 过高 | 通用、易实现 | 总耗时增加 |
| 组合使用 | 上述两者兼具 | 峰值最低、稳定 | 逻辑稍复杂 |
实施前应在测试库用 sys.dm_exec_query_memory_grants 观察过程执行中的申请值,确认瓶颈类型再选型。若 granted_memory 接近 requested_memory 且频繁等待,优先分段;若 wait_type 出现大量 PAGEIOLATCH,则优先内存优化表。
最后提醒,存储过程内也应避免声明过大的表变量或拼接超长动态 SQL 字符串,这些隐性分配同样计入会话内存。把集合规模、暂存方式和提交节奏三者一起设计,才能从根本上解决执行期内存不足。