我的 SQL Server 版本是 SQL Server 2019 (RTM-CU18)。以下重现代码需要创建一个内存文件组。对于后续的任何人,请记住内存中的文件组一旦创建就不能从数据库中删除。
我有一个简单的内存表,我在其中插入 1 - 1200 之间的整数:
DROP TABLE IF EXISTS [dbo].[InMem];
CREATE TABLE [dbo].[InMem] (
i [int] NOT NULL,
CONSTRAINT [PK_InMem] PRIMARY KEY NONCLUSTERED (i ASC)
) WITH ( MEMORY_OPTIMIZED = ON , DURABILITY = SCHEMA_ONLY );
INSERT INTO [dbo].[InMem]
SELECT TOP (1200) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
我还有以下本机编译的存储过程:
GO
CREATE OR ALTER PROCEDURE p1
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english')
SELECT c1.i, c2.i, c3.i
FROM dbo.[InMem] c1
CROSS JOIN dbo.[InMem] c2
CROSS JOIN dbo.[InMem] c3
WHERE c1.i + c2.i + c3.i = 3600;
END;
GO
该过程在执行时返回一行。在我的机器上大约需要 32 秒才能完成。在执行时,我无法观察到内存使用方面的任何异常行为。
我可以创建一个类似的表类型:
CREATE TYPE [dbo].[InMemType] AS TABLE(
i [int] NOT NULL,
INDEX [ix_WordBitMap] NONCLUSTERED (i ASC)
) WITH ( MEMORY_OPTIMIZED = ON );
以及相同的存储过程,但改用表类型:
GO
CREATE OR ALTER PROCEDURE p2 (@t dbo.[InMemType] READONLY)
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'us_english')
SELECT c1.i, c2.i, c3.i
FROM @t c1
CROSS JOIN @t c2
CROSS JOIN @t c3
WHERE c1.i + c2.i + c3.i = 3600;
END;
GO
新存储过程在大约一分钟后抛出错误:
消息 701,级别 17,状态 154,过程 p2,第 6 行 [批处理起始行 57] 资源池“默认”中的系统内存不足,无法运行此查询。
sys.dm_os_memory_clerks
当程序执行时,我可以通过查询dmv看到 MEMORYCLERK_XTP 内存管理员使用的内存量增加到数据库的大约 2800 MB 。根据sys.dm_db_xtp_memory_consumers
DMV,几乎所有的内存使用似乎都来自“64K 页面池”消费者:
作为参考,这里是我如何执行新存储过程的。它使用与表相同的 1200 行:
DECLARE @t dbo.[InMemType];
INSERT INTO @t (i)
SELECT i
from [dbo].[InMem];
EXEC p2 @t;
生成的查询计划是一个没有阻塞运算符的简单嵌套循环计划。根据请求,这是第二个存储过程的估计查询计划。
我不明白为什么当我使用表值参数时,这样的查询的内存使用量会增长到超过 2 GB。我已经阅读了各种文档和内存中 OLTP 白皮书,但找不到任何关于此行为的参考。
使用 ETW 跟踪,我可以看到第一个过程将其大部分 cpu 时间用于调用hkengine!HkCursorHeapGetNext
,而第二个过程将其大部分 cpu 时间用于调用hkengine!HkCursorRangeGetNext
. 我还可以获得这两个程序的 C 源代码。第一个程序在这里,第二个程序有内存问题,在这里。但是,我不知道如何阅读 C 代码,所以我不知道如何进一步调查。
为什么在对表值参数执行嵌套循环时,一个简单的本机编译存储过程会使用超过 2 GB 的内存?当我在存储过程之外运行查询时,也会出现此问题。
当通过 Bw 树(范围)索引使用和访问表变量时,当引擎找到起始条目(
hkengine!HkCursorRangeGetFirst
和hkengine!BwFindFirst
)时,会在每次扫描开始时分配内存。似乎没有维护排序的偏移数组,因此需要定位和排序第一页上的行(碰巧使用快速排序)。内存分配是使用 执行的
hkengine!IncrAllocAlloc
,它从一个块开始递增地工作。当需要一个新块时,hkengine!IoPagePool<65536>::AllocatePage
调用它,这是您看到的 64K 分配的来源。在为堆游标找到第一行后,不会释放此内存。
对于常规内存表,相应的内存分配是使用
hkengine!varAllocAlloc
从 varheap 分配的。hkengine!varAllocFree
与表变量的情况相反,分配之后不久就会调用,释放内存。Bw-trees 最近发生了几次“内存泄漏”。例如,SQL Server 2019 CU 17中列出了两个:
在表变量情况下每次扫描开始时的内存分配不会像这样泄漏,但它们不会在表变量超出范围之前释放。
当嵌套循环重新启动时,测试查询中的第二个和第三个表的游标扫描开始多次。在您的情况下,累积的内存太多,并且在变量超出范围释放内存之前查询中止。
SQL Server 2022 RTM 中的情况相同,只是
sys.dm_db_xtp_memory_consumers
不包括 64K 页面池。您仍然可以看到内存在增加sys.dm_os_memory_clerks
。似乎 2022 安排能够消耗所有可用内存。我必须将缓冲池大小减少到 2.6GB 以下才能得到 OOM 错误。SQL Server 2019 使用 4GB 缓冲池引发错误。从 SQL Server 2016 开始,每个 hekaton 表都有自己的 varheap。除其他好处外,这意味着可以独立于任何索引扫描表。SQL Server 2014 没有 hekaton 表扫描的概念,因为行仅通过索引连接。表变量尚未更新为使用 varheap 方案,因此无法支持表扫描。
游标代码可能已更新以反映新的 varheap 安排,但忽略了仍在用于表变量的原始实现。