In-Memory Table 的性能比基于磁盘的表差

Cri*_*rsi 10 sql-server sql-server-2014 memory-optimized-tables

我在 SQL Server 2014 中有一个表,如下所示:

CREATE TABLE dbo.MyTable
(
[id1] [bigint] NOT NULL,
[id2] [bigint] NOT NULL,
[col1] [int] NOT NULL default(0),
[col2] [int] NOT NULL default(0)
)
Run Code Online (Sandbox Code Playgroud)

(id1,id2) 是 PK。基本上,id1 是一个标识符,用于对一组结果(id2、col1、col2)进行分组,其 pk 是 id2。

我正在尝试使用内存表来摆脱现有的基于磁盘的表,这是我的瓶颈。

  • 表中的数据写入->读取->删除一次。
  • 每个 id1 值都有几个(数十/数百)数千个 id2。
  • 数据在表中存储的时间很短,例如 20 秒。

对该表执行的查询如下:

-- INSERT (can vary from 10s to 10,000s of records):
INSERT INTO MyTable
  SELECT @fixedValue, id2, col1, col2 FROM AnotherTable

-- READ:
SELECT id2, col1
FROM MyTable INNER JOIN OtherTbl ON MyTable.id2 = OtherTbl.pk
WHERE id1 = @value
ORDER BY col1

-- DELETE:
DELETE FROM MyTable WHERE id1 = @value
Run Code Online (Sandbox Code Playgroud)

这是我用于表的当前定义:

CREATE TABLE dbo.SearchItems
(
  [id1] [bigint] NOT NULL,
  [id2] [bigint] NOT NULL,
  [col1] [int] NOT NULL default(0),
  [col2] [int] NOT NULL default(0)

  CONSTRAINT PK_Mem PRIMARY KEY NONCLUSTERED (id1,id2),
  INDEX idx_Mem HASH (id1,id2) WITH (BUCKET_COUNT = 131072)
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY)
Run Code Online (Sandbox Code Playgroud)

不幸的是,相对于基于磁盘的表的先前情况,此定义导致性能下降。数量级或多或少高出 10%(在某些情况下达到 100%,因此加倍)。

最重要的是,考虑到微软宣传的无锁架构,我期待在高并发场景中获得超级优势。相反,当有多个并发用户在表上运行多个查询时,性能最差。

问题:

  • 要设置的正确 BUCKET_COUNT 是什么?
  • 我应该使用什么样的索引?
  • 为什么性能比基于磁盘的表差?

sys.dm_db_xtp_hash_index_stats的查询返回:

total_bucket_count=131072
empty_bucket_count=0
avg_chain_len=873
max_chain_length=1009

我改变了桶数,所以sys.dm_db_xtp_hash_index_stats的输出是:

total_bucket_count=134217728
empty_bucket_count=131664087
avg_chain_len=1
max_chain_length=3

尽管如此,结果几乎相同,甚至更糟。

Sea*_*ser 7

虽然由于缺乏信息,这篇文章不会是一个完整的答案,但它应该能够为您指明正确的方向,或者以其他方式获得您以后可以与社区分享的见解。

不幸的是,相对于基于磁盘的表的先前情况,此定义导致性能下降。数量级或多或少高出 10%(在某些情况下达到 100%,因此加倍)。

最重要的是,考虑到微软宣传的无锁架构,我期待在高并发场景中获得超级优势。相反,当有多个并发用户在表上运行多个查询时,性能最差。

这是令人不安的,因为它绝对不应该是这种情况。某些工作负载不适用于内存表 (SQL 2014),并且某些工作负载适合它。在大多数情况下,仅通过迁移和选择适当的索引,性能的提升就很小。

最初,我对您有关此问题的问题的看法非常狭隘:

问题:

  • 要设置的正确 BUCKET_COUNT 是什么?
  • 我应该使用什么样的索引?
  • 为什么性能比基于磁盘的表差?

最初,我认为内存中的实际表和索引不是最佳的存在问题。虽然内存优化哈希索引定义存在一些问题,但我认为真正的问题在于所使用的查询。

-- INSERT (can vary from 10s to 10,000s of records):
INSERT INTO MyTable
  SELECT @fixedValue, id2, col1, col2 FROM AnotherTable
Run Code Online (Sandbox Code Playgroud)

如果此插入仅涉及内存表,则该插入应该非常快。然而,它也涉及一个基于磁盘的表,并受到与此相关的所有锁定和阻塞的影响。因此,这里的实时浪费是在基于磁盘的表上。

当我在将数据加载到内存后对基于磁盘的表中的 100,000 行插入进行快速测试时 - 这是亚秒响应时间。但是,您的大部分数据只会保留很短的时间,不到 20 秒。这并没有给它太多时间真正生活在缓存中。此外,我不确定实际有多大AnotherTable,也不知道这些值是否正在从磁盘中读取。我们必须依靠您来回答这些问题。

使用 Select 查询:

SELECT id2, col1
FROM MyTable INNER JOIN OtherTbl ON MyTable.id2 = OtherTbl.pk
WHERE id1 = @value
ORDER BY col1
Run Code Online (Sandbox Code Playgroud)

同样,我们受制于互操作 + 基于磁盘的表性能。此外,HASH 索引的排序并不便宜,应该使用非聚集索引。我在评论中链接的索引指南中提到了这一点。

为了提供一些基于实际研究的事实,我SearchItems在内存表中加载了1000 万行和AnotherTable100,000行,因为我不知道它的实际大小或统计数据。然后我使用上面的选择查询来执行。此外,我在 wait_completed 上创建了一个扩展事件会话并将其放入环形缓冲区。每次运行后都会清洗。我还跑DBCC DROPCLEANBUFFERS模拟了一个环境,其中所有数据可能都不是内存常驻的。

在真空中观察它们时,结果并不出众。由于我正在测试的笔记本电脑使用的是更高级别的 SSD,因此我人为地降低了我正在使用的 VM 的基于磁盘的性能。

在基于内存的表上运行 5 次查询后,结果没有等待信息(删除连接并且没有子查询)。这与预期的差不多。

然而,在使用原始查询时,我确实有等待。在这种情况下, PAGEIOLATCH_SH 是有意义的,因为正在从磁盘读取数据。由于我是该系统中唯一的用户,并且没有花时间为连接表的插入、更新、删除创建大量测试环境,因此我不希望任何锁定或阻塞生效。

在这种情况下,再一次,大部分时间都花在了基于磁盘的表上。

最后是删除查询。使用 has 索引查找仅基于 ID1 的行并不是非常有效。虽然相等谓词确实适合哈希索引,但数据落入的桶是基于整个哈希列的。因此 id1, id2 其中 id1 = 1, id2 = 2 和 id1 = 1, id2 = 3 将散列到不同的桶中,因为散列将跨越 (1,2) 和 (1,3)。这不会是简单的 B 树范围扫描,因为哈希索引的结构不同。然后我希望这不是此操作的理想索引,但是我不希望它花费的时间比所经历的时间长几个数量级。我有兴趣查看有关此的 wait_info。

最重要的是,考虑到微软宣传的无锁架构,我期待在高并发场景中获得超级优势。相反,当有多个并发用户在表上运行多个查询时,性能最差。

虽然锁用于逻辑一致性,但操作仍然必须是原子的。这是通过一个特殊的基于 CPU 的比较运算符完成的(这就是为什么 In-Memory 只适用于某些 [尽管几乎所有 CPU 都是在过去 4 年中制造的] 处理器)。因此我们不会免费获得所有内容,仍然需要一些时间来完成这些操作。

另一点要提出的事实是,在几乎所有查询中,使用的接口都是 T-SQL(而不是本机编译的 SPROC),它们都至少涉及一个基于磁盘的表。这就是为什么我相信,最终,我们实际上并没有提高性能,因为我们仍然受限于基于磁盘的表的性能。

跟进:

  1. 为 wait_completed 创建扩展事件会话并指定您已知的 SPID。运行查询并为我们提供输出或在内部使用它。

  2. 给我们一个关于#1 输出的更新。

  3. 没有用于确定哈希索引的桶数的魔法数字。基本上只要桶没有完全装满并且行链保持在 3 或 4 以下,性能就应该可以接受。这有点像问“我应该将日志文件设置为什么?” - 这将取决于每个进程、每个数据库、每个使用类型。