估算内存优化表的内存需求

适用于:SQL ServerAzure SQL 数据库Azure SQL 托管实例

内存优化表要求存在足够的内存,以便将所有行和索引保留在内存中。 因为内存是有限的资源,所以了解和管理系统的内存使用情况非常重要。 本节中的主题介绍一些常见的内存使用和管理方案。

必须合理估计每个内存优化表的内存需求,以便可以使用足够的内存来预配服务器。 这适用于新的表格以及从基于磁盘的表格迁移过来的表格。 本节介绍如何估算使用内存优化表存放数据时所需的内存大小。

如果要考虑从基于磁盘的表迁移到内存优化表,请参阅 确定表或存储过程是否应移植到 In-Memory OLTP ,以获取有关哪些表最适合迁移的指导。 迁移到内存中 OLTP 下的所有主题均提供了有关从基于磁盘的表迁移至内存优化表的指导。

估计内存需求量的基本指南

在 SQL Server 2016(13.x)及更高版本中,内存优化表的大小没有限制,但表确实需要容纳在内存中。 在 SQL Server 2014(12.x)中,SCHEMA_AND_DATA表支持的数据大小为 256 GB。

内存优化表的大小与数据大小加上一些行标题的开销大小之和相对应。 内存优化表的大小大致对应于基于磁盘的原始表的聚集索引或堆的大小。

内存优化表的索引通常比基于磁盘的表的非聚集索引小。 非聚集索引的大小按照 [primary key size] * [row count]顺序排列。 哈希索引的大小为 [bucket count] * 8 bytes

当有活动的工作负载时,则需要更多的内存以应对行版本控制和各种操作。 所需的内存量取决于工作负荷,但要安全,建议从内存优化表和索引的预期大小两倍开始,并观察实际内存消耗。 行版本控制的开销始终取决于工作负荷的特征 - 特别是长时间运行的事务会增加开销。 对于使用较大数据库(例如大于 100 GB)的大多数工作负荷,开销往往受到限制(25% 或更少)。

有关 In-Memory OLTP 引擎中潜在的内存开销的详细信息,请参阅 内存碎片

内存需求量的详细计算

内存优化表示例

考虑以下内存优化的表架构:

CREATE TABLE t_hk
(  
  col1 int NOT NULL  PRIMARY KEY NONCLUSTERED,  

  col2 int NOT NULL  INDEX t1c2_index   
      HASH WITH (bucket_count = 5000000),  

  col3 int NOT NULL  INDEX t1c3_index   
      HASH WITH (bucket_count = 5000000),  

  col4 int NOT NULL  INDEX t1c4_index   
      HASH WITH (bucket_count = 5000000),  

  col5 int NOT NULL  INDEX t1c5_index NONCLUSTERED,  

  col6 char (50) NOT NULL,  
  col7 char (50) NOT NULL,   
  col8 char (30) NOT NULL,   
  col9 char (50) NOT NULL  

)   WITH (memory_optimized = on)  ;
GO  

使用此架构,让我们确定此内存优化表所需的最小内存。

表占用的内存

内存优化表行有三个部分:

  • 时间戳
    行标题/时间戳 = 24 个字节。

  • 索引指针
    对于表中的每个哈希索引,每行包含一个指向索引中下一行的 8 字节地址指针。 由于有四个索引,因此每行为索引指针分配 32 个字节(每个索引的 8 字节指针)。

  • Data
    行中数据部分的大小由各数据列类型大小的总和决定。 示例表中包含五个 4 字节整数、三个 50 字节的字符列和一个 30 字节的字符列。 因此,每行的数据部分为 4 + 4 + 4 + 4 + 4 + 50 + 50 + 30 + 50 共 200 个字节。

下面是内存优化表中 5,000,000(5 百万)行的大小计算: 按以下方式估计数据行使用的总内存:

表行占用的内存

根据上述计算,内存优化表中每行的大小为 24 + 32 + 200,即 256 个字节。 由于我们有 500 万行,因此该表消耗 5,000,000 * 256 个字节,或 1,280,000,000 个字节 - 大约 1.28 GB。

索引占用的内存

每个哈希索引占用的内存

每个哈希索引是一个由 8 字节地址指针组成的哈希数组。 数组的大小最好由该索引的唯一索引值数确定。 在当前示例中,唯一 Col2 值的数目是t1c2_index数组大小的一个很好的起点。 哈希数组太大,浪费内存。 哈希数组太小会降低性能,因为索引值与同一索引项存在过多冲突。

哈希索引可实现高速查找,如:

SELECT * FROM t_hk  
   WHERE Col2 = 3;

非聚集索引在执行范围查找时更快,如:

SELECT * FROM t_hk  
   WHERE Col2 >= 3;

如果是迁移磁盘表,可使用下面的方法确定索引 t1c2_index 的唯一键的个数。

SELECT COUNT(DISTINCT [Col2])  
  FROM t_hk;

如果要创建新表,则需要在部署之前估算数组大小或从测试中收集数据。

关于内存中 OLTP 内存优化表中哈希索引的工作原理,请参阅《哈希索引》

设置哈希索引数组的大小

哈希数组大小由 (bucket_count= value) 设置,其中 value 为大于零的整数值。 如果 value 不为 2 的幂,实际的 bucket_count 将舍入为下一个最近的 2 的幂。 在示例表中,(bucket_count = 5000000),由于 5,000,000 并非 2 的幂,实际桶计数舍入为 8,388,608 (2^23)。 在计算此哈希数组所需的内存时,必须使用该值,而非 5,000,000。

因此,在本示例中,每个哈希数组所需的内存为:

8 * 8 = 2^23 * 8 = 2^23 * 2^3 = 2^26 = 67,108,864,约 64MB。

有三个哈希索引,所以哈希索引占用的内存为 3 * 64MB = 192MB。

非聚集索引占用的内存

非聚集索引采用 Bw 树实现,其内部节点包含索引键和指向后续节点的指针。 叶节点包含索引键和指向内存中表行的指针。

与哈希索引不同的是,非聚集索引的存储桶大小不固定。 非聚集索引随数据动态扩展/收缩。

可按如下方式计算非聚集索引占用的内存:

  • 分配给非叶节点的内存
    对于典型配置,分配给非叶节点的内存是索引占用的总内存的一小部分。 其占用的内存少到可以安全地忽略。

  • 叶节点占用的内存
    叶节点对于表中的每个唯一键都具有 1 行,并且指向具有该唯一键的数据行。 如果有多个行具有相同键(即,具有既非唯一又非聚集的索引),索引叶节点中只有一行指向其中一个行,并且其他行都相互链接。 因此,可通过下面的公式估算所需的总内存:

    • 非聚集索引所需内存 = (指针大小 + Sum (键列数据类型大小))* 唯一键行数

非聚集索引的最佳应用是范围查找,下面的查询即为例证:

SELECT * FROM t_hk  
   WHERE c2 > 5;  

行版本控制占用的内存

为避免锁定,内存 OLTP 在更新或删除行时采用乐观并发策略。 也就是说,对某行进行更新时,将另创建一个该行的版本。 此外,删除是逻辑性的 — 现有行会标记为已删除,但并未立即删除。 系统将旧行版本(包括已删除的行)保持可用状态,直至所有可能使用这些版本的事务执行完毕。

在任意时刻,内存中都可能存在很多额外行等待垃圾回收周期释放其内存,因此,必须具有足够的内存来容纳这些额外的行。

可以通过计算每秒的最大行更新数和最大删除数,然后将其乘以事务所用的最长秒数(最少为 1)来估算额外的行数。

再用该值乘以行大小即可获得行版本控制所需占用的字节数。

rowVersions = durationOfLongestTransactionInSeconds * peakNumberOfRowUpdatesOrDeletesPerSecond

然后,通过将过时行数乘以内存优化表行的大小来估计过时行的内存需求。 有关详细信息,请查看 表的内存

memoryForRowVersions = rowVersions * rowSize

表变量占用的内存

只有在表变量超出范围时,才能释放表变量占用的内存。 来自表变量的行(包括作为更新的一部分删除的行)不会进行垃圾回收。 在表变量退出作用域之前不会释放内存。

在大型 SQL 批处理而不是存储过程中定义的表变量,并且在许多事务中使用的表变量可能会消耗大量的内存。 因为不对它们进行垃圾回收,所以,表变量中已删除的行可能会使用大量内存并且降低性能,这是因为读取操作需要扫描已删除的行。

表增长占用的内存

之前的计算是对表格当前状态下的内存需求进行的估计。 除该内存大小外,还需对表的增长作出预测并提供充足的内存来满足增长所需。 例如,如果预计 10% 增长,则需要将以前的结果乘以 1.1 来获取表所需的总内存。

内存碎片

为了避免内存分配调用的开销并提高性能,In-Memory OLTP 引擎始终使用 64 KB 块(称为超级块)从 SQL Server 操作系统(SQLOS)请求内存。

每个超级块仅包含特定大小范围内的内存分配,称为大小类。 例如,超级锁 A 在 1-16 字节大小类中可能有内存分配,而超级锁 B 在 17-32 字节大小类中可能有内存分配,等等。

默认情况下,超级块也按逻辑 CPU 进行划分。 这意味着,对于每个逻辑 CPU,都有一组单独的超级块,并按大小类进一步细分。 这减少了在不同 CPU 上执行的请求之间的内存分配争用。

当 In-Memory OLTP 引擎进行新的内存分配时,它会首先尝试在针对请求的大小类和处理该请求的 CPU 的现有超级块中查找可用内存。 如果此尝试成功,特定内存使用者在used_bytes中的列的值将按请求的内存大小增加,而allocated_bytes列中的值保持不变。

如果现有超级锁中没有可用内存,则会分配新的超级锁,并 used_bytes 按请求的内存大小增加值,而列中的值 allocated_bytes 将增加 64 KB。

随着时间的推移,随着超级锁中的内存被分配和解除分配,In-Memory OLTP 引擎消耗的内存总量可能会明显大于已用内存量。 换句话说,内存可能会碎片化。

垃圾回收 可能会减少已用内存,只有当一个或多个超级块变为空并被解除分配时,才会减少分配的内存。 这适用于使用 sys.sp_xtp_force_gc 系统存储过程的自动和强制垃圾回收。

如果 In-Memory OLTP 引擎内存碎片和分配的内存使用率高于预期,则可以启用 跟踪标志 9898。 这会将超级块分区策略从每个 CPU 更改为每个 NUMA 节点,从而减少超级块的总数,降低高内存碎片的潜在风险。

此优化更适用于具有许多逻辑 CPU 的大型计算机。 这种优化的权衡在于可能会因为超级块减少导致内存分配争用增加,从而降低总体工作负荷的吞吐量。 根据工作负荷模式,按 NUMA 内存分区使用的吞吐量减少可能会明显,也可能不明显。