为什么使用表变量时简单的本机编译存储过程会耗尽内存?

Joe*_*ish 14 sql-server stored-procedures memory-optimized-tables table-valued-parameters

我的 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;
Run Code Online (Sandbox Code Playgroud)

我还有以下本机编译的存储过程:

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  
Run Code Online (Sandbox Code Playgroud)

该过程执行时返回一行。在我的机器上大约需要 32 秒才能完成。我在执行时无法观察到内存使用方面的任何异常行为。

我可以创建一个类似的表类型:

CREATE TYPE [dbo].[InMemType] AS TABLE(
i [int] NOT NULL,
INDEX [ix_WordBitMap] NONCLUSTERED (i ASC)
) WITH ( MEMORY_OPTIMIZED = ON );
Run Code Online (Sandbox Code Playgroud)

以及相同的存储过程,但使用表类型:

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
Run Code Online (Sandbox Code Playgroud)

新的存储过程在大约一分钟后抛出错误:

消息 701,级别 17,状态 154,过程 p2,第 6 行 [批处理起始行 57] 资源池“默认”中没有足够的系统内存来运行此查询。

当该过程执行时,通过查询 dmv,我可以看到 MEMORYCLERK_XTP 内存管理员使用的数据库内存量增加到大约 2800 MB sys.dm_os_memory_clerks。根据sys.dm_db_xtp_memory_consumersDMV,几乎所有内存使用量似乎都来自“64K 页池”使用者:

在此输入图像描述

在此输入图像描述

作为参考,以下是我执行新存储过程的方式。它使用与表相同的 1200 行:

DECLARE @t dbo.[InMemType];

INSERT INTO @t (i)
SELECT i
from [dbo].[InMem];

EXEC p2 @t;
Run Code Online (Sandbox Code Playgroud)

生成的查询计划是一个简单的嵌套循环计划,没有阻塞运算符。根据请求,下面是第二个存储过程的估计查询计划。

我不明白为什么当我使用表值参数时,此类查询的内存使用量会增长到超过 2 GB。我已经阅读了各种文档和内存中 OLTP 白皮书,但找不到任何对此行为的参考。

使用 ETW 跟踪,我可以看到第一个过程花费了大部分 cpu 时间来调用hkengine!HkCursorHeapGetNext,第二个过程花费了大部分 cpu 时间来调用hkengine!HkCursorRangeGetNext。我还可以获得这两个程序的 C 源代码。第一个过程在这里,第二个过程,有内存问题,在这里。但是,我不知道如何阅读 C 代码,所以我不知道如何进一步调查。

为什么一个简单的本机编译存储过程在对表值参数执行嵌套循环时会使用超过 2 GB 的内存?当我在存储过程之外运行查询时也会出现此问题。

Pau*_*ite 17

当通过 Bw 树(范围)索引使用和访问表变量时,当引擎找到起始条目(hkengine!HkCursorRangeGetFirsthkengine!BwFindFirst)时,每次扫描开始时都会分配内存。似乎没有维护排序的偏移数组,因此需要定位第一页上的行并对其进行排序(碰巧使用快速排序)。

快速排序调用栈

内存分配是使用 执行的hkengine!IncrAllocAlloc,它从块开始增量工作。当需要新块时,hkengine!IoPagePool<65536>::AllocatePage会调用 ,这就是您看到的 64K 分配的来源。

64K 分配调用堆栈

找到堆游标的第一行后,不会释放该内存。

对于常规内存表,使用 执行相应的内存分配hkengine!varAllocAlloc,它从 varheap 进行分配。与表变量的情况相反,分配之后不久就会调用hkengine!varAllocFree,释放内存。

最近,Bw-tree 出现了几次“内存泄漏”。例如,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 排列,但忽略了仍在用于表变量的原始实现。