概述
此示例展示了 SQL Server 2014 中新的 In-Memory OLTP 功能。 它显示了新的内存优化表和本机编译的存储过程,可用于演示 In-Memory OLTP 的性能优势。
注释
若要查看 SQL Server 2016 的本主题,请参阅 用于演示 In-Memory OLTP 的 AdventureWorks 扩展
该示例将 AdventureWorks 数据库中的 5 个表迁移到内存优化,并包括用于销售订单处理的演示工作负荷。 可以使用此演示工作负荷查看在服务器上使用 In-Memory OLTP 的性能优势。
在示例的说明中,我们讨论了在将表迁移到 In-Memory OLTP 时所做的权衡,以考虑 SQL Server 2014 中内存优化表不支持的功能。
此示例的文档的结构如下:
安装示例并运行演示工作负载的先决条件
示例表和过程的说明 - 这包括 In-Memory OLTP 示例添加到 AdventureWorks 中的表和过程的说明,以及将一些原始 AdventureWorks 表迁移到内存优化的注意事项
有关使用演示工作负荷执行性能度量的说明 - 这包括有关安装和运行 ostress 的说明、用于驱动工作负荷的工具以及运行演示工作负荷本身的说明
先决条件
SQL Server 2014 RTM - 评估版、开发人员版或企业版
用于性能测试时,选择一台规格与生产环境相似的服务器。 对于此特定示例,应至少有 16GB 内存可供 SQL Server 使用。 有关 In-Memory OLTP 硬件的一般准则,请参阅以下博客文章:https://cloudblogs.microsoft.com/sqlserver/2013/08/01/hardware-considerations-for-in-memory-oltp-in-sql-server-2014/
安装基于 AdventureWorks 的 In-Memory OLTP 示例
按照以下步骤安装示例:
下载 AdventureWorks2014 数据库的完整备份的存档:
打开以下内容: https://msftdbprodsamples.codeplex.com/downloads/get/880661
当系统提示将文件保存到本地文件夹时。
将 AdventureWorks2014.bak 文件解压缩到本地文件夹,例如“c:\temp”。
使用 Transact-SQL 或 SQL Server Management Studio 还原数据库备份:
标识数据文件的目标文件夹和文件名,例如
“h:\DATA\AdventureWorks2014_Data.mdf”
标识日志文件的目标文件夹和文件名,例如
'i:\DATA\AdventureWorks2014_log.ldf'
- 日志文件应放置在与数据文件不同的驱动器上,理想情况下是低延迟驱动器(例如 SSD 或 PCIe 存储),以获得最佳性能。
T-SQL 脚本示例:
RESTORE DATABASE [AdventureWorks2014] FROM DISK = N'C:\temp\AdventureWorks2014.bak' WITH FILE = 1, MOVE N'AdventureWorks2014_Data' TO N'h:\DATA\AdventureWorks2014_Data.mdf', MOVE N'AdventureWorks2014_Log' TO N'i:\DATA\AdventureWorks2014_log.ldf' GO通过在 SQL Server Management Studio 的查询窗口中运行以下命令,将数据库所有者更改为服务器上的登录名:
ALTER AUTHORIZATION ON DATABASE::AdventureWorks2014 TO [<NewLogin>]将示例脚本“SQL Server 2014 RTM In-Memory OLTP Sample.sql”从 SQL Server 2014 RTM In-Memory OLTP 示例 下载到本地文件夹。
更新脚本“SQL Server 2014 RTM In-Memory OLTP Sample.sql”中变量“checkpoint_files_location”的值,以指向 In-Memory OLTP 检查点文件的目标位置。 检查点文件应放置在具有良好顺序 IO 性能的驱动器上。
将变量“database_name”的值更新为指向 AdventureWorks2014 数据库。
请务必在路径名称中包含反斜杠“”
示例:
:setvar checkpoint_files_location "d:\DBData\" ... :setvar database_name "AdventureWorks2014"
通过以下两种方式之一执行示例脚本:
使用 sqlcmd 命令行实用工具。 例如,通过从包含脚本的文件夹中的命令行提示符运行以下命令,
sqlcmd -S . -E -i "ssSQL14 RTM hek_2 Sample.sql"使用 Management Studio:
在查询窗口中打开脚本“SQL Server 2014 RTM In-Memory OLTP Sample.sql”
连接到包含数据库 AdventureWorks2014 的目标服务器
通过单击“查询 -> SQLCMD 模式”启用 SQLCMD 模式
单击按钮“执行”以运行脚本
示例表和过程的说明
该示例基于 AdventureWorks 中的现有表为产品和销售订单创建新表。 新表的架构与现有表类似,但有一些差异,如下所述。
新的内存优化表带有后缀“_inmem”。 此示例还包括包含后缀“_ondisk”的对应表 - 这些表可用于在内存优化表的性能与系统上的基于磁盘的表之间进行一对一比较。
请注意,工作负荷中用于性能比较的内存优化表是完全持久且完全记录的。 它们不会牺牲持久性或可靠性来实现性能提升。
此示例的目标工作负荷是销售订单处理,其中我们还考虑了有关产品和折扣的信息。 为此,表格包含 SalesOrderHeader、SalesOrderDetail、Product、SpecialOffer 和 SpecialOfferProduct。
两个新的存储过程,Sales.usp_InsertSalesOrder_inmem和Sales.usp_UpdateSalesOrderShipInfo_inmem,用于插入销售订单和更新给定销售订单的发货信息。
新的架构“演示”包含用于执行演示工作负荷的帮助程序表和存储过程。
具体地说,In-Memory OLTP 示例将以下对象添加到 AdventureWorks:
由示例添加的表
新表
销售.销售订单头_inmem
- 有关销售订单的标头信息。 每个销售订单在此表中都有一行。
销售.销售订单详情_内存
- 销售订单的详细信息。 销售订单的每个行项在此表中都有一行。
Sales.SpecialOffer_inmem
- 有关特价产品/服务的信息,包括与每个特价产品/服务关联的折扣百分比。
销售.特别优惠产品_inmem
- 特惠与产品之间的参考表。 每个特价可以包括零个或多个产品,并且每个产品可以包含在零个或多个特价中。
Production.Product_inmem
- 有关产品的信息,包括其标价。
Demo.DemoSalesOrderDetailSeed
- 在演示工作负荷中用于构造示例销售订单。
基于磁盘的表格变体:
Sales.SalesOrderHeader_ondisk
Sales.SalesOrderDetail_ondisk
Sales.SpecialOffer_ondisk
Sales.SpecialOfferProduct_ondisk
Production.Product_ondisk
基于磁盘的原始表和新内存优化表之间的差异
在大多数情况下,此示例引入的新表使用与原始表相同的列和相同的数据类型。 但二者之间存在一些区别。 我们列出了以下差异,以及更改的理由。
销售.销售订单头_inmem
内存优化表支持默认约束,我们按原样迁移了大多数默认约束。 但是,原始表 Sales.SalesOrderHeader 包含两个默认约束,用于检索 OrderDate 和 ModifiedDate 列的当前日期。 在具有大量并发的高吞吐量顺序处理工作负荷中,任何全局资源都可以成为争用点。 系统时间是这样的全局资源,我们观察到,当运行插入销售订单的 In-Memory OLTP 工作负荷时,它可能会成为瓶颈,尤其是在需要检索销售订单标头中多个列的系统时间以及销售订单详细信息时。 此示例中通过检索插入的每个销售订单的系统时间一次,并在存储过程Sales.usp_InsertSalesOrder_inmem中对SalesOrderHeader_inmem和SalesOrderDetail_inmem中的日期/时间列使用该值来解决该问题。
别名 UDT - 原始表使用了两种别名用户定义数据类型(UDT),即 dbo.OrderNumber 和 dbo.AccountNumber,分别对应于 PurchaseOrderNumber 列和 AccountNumber 列。 SQL Server 2014 不支持内存优化表的别名 UDT,因此新表分别使用系统数据类型 nvarchar(25) 和 nvarchar(15)。
索引键中的可为 Null 列 - 在原始表中,SalesPersonID 列为 null,而在新表中,该列不可为 null,并且具有具有值的默认约束(-1)。 这是因为内存优化表上的索引在索引键中不能有可为 null 的列;在本例中,-1 是 NULL 的代理项。
计算列 - 省略计算列 SalesOrderNumber 和 TotalDue,因为 SQL Server 2014 不支持内存优化表中的计算列。 新的视图 Sales.vSalesOrderHeader_extended_inmem 反映了列 SalesOrderNumber 和 TotalDue。 因此,如果需要这些列,则可以使用此视图。
SQL Server 2014 中的内存优化表不支持外键约束。 此外,SalesOrderHeader_inmem 是示例工作负荷中的热表,外键约束要求对所有 DML 操作进行额外的处理步骤,因为需要在这些约束中引用的所有其他表中进行查找。 因此,假设应用确保引用完整性,并且在插入行时不验证引用完整性。 可以使用存储过程dbo.usp_ValidateIntegrity使用以下脚本验证此表中数据的引用完整性:
DECLARE @o int = object_id(N'Sales.SalesOrderHeader_inmem') EXEC dbo.usp_ValidateIntegrity @oSQ Server 2014 中的内存优化表不支持检查约束。 使用此脚本验证域完整性以及引用完整性:
DECLARE @o int = object_id(N'Sales.SalesOrderHeader_inmem') EXEC dbo.usp_ValidateIntegrity @oRowguid - rowguid 列已被省略。 虽然 uniqueidentifier 支持内存优化表,但 SQL Server 2014 不支持 ROWGUIDCOL 选项。 此类列通常用于合并复制或者用于包含文件流列的表。 此示例不包含这两个示例。
Sales.SalesOrderDetail
默认约束 - 类似于 SalesOrderHeader,不迁移需要系统日期/时间的默认约束,而是插入销售订单的存储过程负责在首次插入时插入当前系统日期/时间。
计算列 - 计算列 LineTotal 未迁移,因为 SQL Server 2014 中的内存优化表不支持计算列。 若要访问此列,请使用视图Sales.vSalesOrderDetail_extended_inmem。
Rowguid - 省略 rowguid 列。 有关详细信息,请参阅表 SalesOrderHeader 的说明。
有关 检查 和 外键 约束,请参阅 SalesOrderHeader 的说明。 以下脚本可用于验证此表的域和引用完整性:
DECLARE @o int = object_id(N'Sales.SalesOrderHeader_inmem') EXEC dbo.usp_ValidateIntegrity @o
生产.产品
别名 UDT - 原始表使用用户定义的数据类型 dbo.Flag,它等效于系统数据类型 bit。 迁移的表改用位数据类型。
BIN2 排序规则 - 列 Name 和 ProductNumber 包含在索引键中,因此必须在 SQL Server 2014 中包含 BIN2 排序规则。 在此,假设应用不依赖于排序规则细节,例如不区分大小写。
Rowguid - 省略 rowguid 列。 有关详细信息,请参阅表 SalesOrderHeader 的说明。
唯一的 检查 和 外键约束 分为两种方式:存储过程Product.usp_InsertProduct_inmem和Product.usp_DeleteProduct_inmem可用于插入和删除产品;这些过程验证域和引用完整性,如果违反完整性,将失败。 此外,以下脚本可用于验证域和引用完整性,如下所示:
DECLARE @o int = object_id(N'Production.Product') EXEC dbo.usp_ValidateIntegrity @o- 请注意,存储过程usp_InsertProduct_inmem和usp_DeleteProduct_inmem只考虑迁移表之间的外键。 不考虑对其他表 ProductModel、ProductSubcategory 和 UnitMeasure 的引用。
销售.特别优惠
检查 和 外键约束 有两种处理方式:可以使用存储过程Sales.usp_InsertSpecialOffer_inmem和Sales.usp_DeleteSpecialOffer_inmem来插入和删除特别优惠。这些过程会验证域和引用完整性,如果完整性被违反,操作将失败。 此外,以下脚本可用于验证域和引用完整性,如下所示:
DECLARE @o int = object_id(N'Sales.SpecialOffer_inmem') EXEC dbo.usp_ValidateIntegrity @oRowguid - 省略 rowguid 列。 有关详细信息,请参阅表 SalesOrderHeader 的说明。
销售.特价产品
外键约束 分为两种方式:存储过程Sales.usp_InsertSpecialOfferProduct_inmem可用于插入特殊产品/服务之间的关系;此过程验证引用完整性,如果违反完整性,则失败。 此外,以下脚本可用于验证引用完整性,如下所示:
DECLARE @o int = object_id(N'Sales.SpecialOfferProduct_inmem') EXEC dbo.usp_ValidateIntegrity @oRowguid - 省略 rowguid 列。 有关详细信息,请参阅表 SalesOrderHeader 的说明。
内存优化表上的索引注意事项
内存优化表的基线索引是 NONCLUSTERED 索引,它支持点查找(相等谓词上的索引查找)、范围扫描(在不等谓词中查找索引)、完整索引扫描和有序扫描。 此外,NONCLUSTERED 索引还支持搜索索引键的前导列。 事实上,内存优化的非聚集索引支持所有基于磁盘的非聚集索引所支持的操作,唯一的例外是反向扫描。 因此,对于你的索引来说,使用非聚集索引是一个安全的选择。
哈希索引可用于进一步优化工作负荷。 它们特别针对点查找和行插入进行优化。 但是,必须考虑到它们不支持范围扫描、有序扫描或搜索主索引键列。 因此,在使用这些索引时需要小心。 此外,还需要在创建时指定bucket_count。 它通常应设置为索引键值的一到两倍,但过度估计通常不是问题。
有关索引指南和选择正确bucket_count的指南的更多详细信息,请参阅在线文档。
迁移表上的索引已针对演示的销售订单处理工作负载进行了优化。 工作负荷依赖于表 Sales.SalesOrderHeader_inmem 和 Sales.SalesOrderDetail_inmem 中的数据插入和点查询,同时依赖于表 Production.Product_inmem 和 Sales.SpecialOffer_inmem 中主键列的点查询。
Sales.SalesOrderHeader_inmem有三个索引,这些索引都是出于性能原因,并且工作负荷不需要有序扫描或范围扫描。
(SalesOrderID)的 HASH 指数:bucket_count大小为 1000 万(舍入至 1600 万),因为预期销售订单数为 1000 万
(SalesPersonID):bucket_count为 100 万。 提供的数据集没有大量的销售人员,但这允许将来的增长,此外,如果bucket_count过大,则不会为点查找支付性能处罚。
(CustomerID):bucket_count的 HASH 索引为 100 万。 提供的数据集没有大量客户,但这允许将来增长。
Sales.SalesOrderDetail_inmem 有三个索引,这些索引全部为 HASH 索引,这是出于性能考量,并且由于工作负荷不需要有序或范围扫描。
哈希索引 (SalesOrderID, SalesOrderDetailID):这是主键索引,即使对 (SalesOrderID, SalesOrderDetailID) 的查找并不频繁,但使用键的哈希索引加快行插入速度。 bucket_count的大小为5000万(舍入为6700万):预期销售订单数量为1000万,并且每个订单的平均商品数量为5项。
(SalesOrderID):按销售订单查找的哈希索引很频繁:需要查找与单个订单对应的所有行项。 bucket_count大小为 1000 万(舍入到 1600 万),因为预期销售订单数为 1000 万
在 (ProductID) 上的 HASH 索引:bucket_count 为 100 万。 提供的数据集没有大量的产品,但这允许将来的增长。
Production.Product_inmem有三个索引
关于 (ProductID) 的哈希索引:因为演示工作负荷中对 ProductID 的查找位于关键路径中,所以这是一个哈希索引。
非聚集索引(名称):这将允许对产品名称进行排序扫描
NONCLUSTERED 索引(产品编号):这将允许对产品编号进行有序扫描
Sales.SpecialOffer_inmem 有一个关于 (SpecialOfferID) 的 HASH 索引:特殊优惠的点查询是演示工作负载中的关键部分。 bucket_count的大小为 100 万,以允许未来的增长。
Sales.SpecialOfferProduct_inmem未在演示工作负荷中引用,因此无需在此表上使用哈希索引来优化此工作负荷。此外,(SpecialOfferID、ProductID)和(ProductID)上的索引为NONCLUSTERED。
请注意,在上述有些bucket_counts的大小过大,但SalesOrderHeader_inmem和SalesOrderDetail_inmem索引的bucket_counts不是这样:它们的大小仅适合 1000 万个销售订单。 这样做是为了允许在内存不足的系统上安装示例,但在这种情况下,演示工作负荷会因内存不足而失败。 如果想把销售订单扩展到远超 1000 万个,可以随意相应地增加桶的数量。
内存利用率注意事项
示例数据库中的内存利用率(运行演示工作负荷之前和之后)在 内存优化表的“内存利用率”部分讨论。
示例添加的存储过程
插入销售订单和更新发货详细信息的两个关键存储过程如下所示:
Sales.usp_InsertSalesOrder_inmem
在数据库中插入新的销售订单,并输出该销售订单的 SalesOrderID。 作为输入参数,它将获取销售订单标头的详细信息以及订单中的行项。
输出参数:
- @SalesOrderID int - 刚插入的销售订单的SalesOrderID
输入参数(必需):
@DueDate datetime2
@CustomerID int
@BillToAddressID [int]
@ShipToAddressID [int]
@ShipMethodID [int]
@SalesOrderDetails Sales.SalesOrderDetailType_inmem - 包含订单行项的 TVP
输入参数(可选):
@Status [tinyint]
@OnlineOrderFlag [bit]
@PurchaseOrderNumber [nvarchar](25)
@AccountNumber [nvarchar](15)
@SalesPersonID [int]
@TerritoryID [int]
@CreditCardID [int]
@CreditCardApprovalCode [varchar](15)
@CurrencyRateID [int]
@Comment nvarchar(128)
Sales.usp_UpdateSalesOrderShipInfo_inmem
更新给定销售订单的发货信息。 这还将更新销售订单的所有行项的发货信息。
这是一个用于本机编译存储过程Sales.usp_UpdateSalesOrderShipInfo_native的包装过程,包含重试逻辑,以处理与更新同一订单的并发事务可能引发的意外冲突。 有关重试逻辑的详细信息,请参阅 此处的在线文档主题。
Sales.usp_UpdateSalesOrderShipInfo_native
- 这是本机编译的存储过程,它实际处理对发货信息的更新。 从包装器存储过程Sales.usp_UpdateSalesOrderShipInfo_inmem调用它。 如果客户端可以处理失败并实现重试逻辑,则可以直接调用此过程,而不是使用包装存储过程。
以下存储过程用于演示工作负荷。
Demo.usp_DemoReset
- 通过清空和重新设置 SalesOrderHeader 和 SalesOrderDetail 表来重置演示。
以下存储过程用于插入和删除内存优化表,同时保证域和引用完整性。
Production.usp_InsertProduct_inmem
Production.usp_DeleteProduct_inmem
Sales.usp_InsertSpecialOffer_inmem
Sales.usp_DeleteSpecialOffer_inmem
Sales.usp_InsertSpecialOfferProduct_内存中
最后,以下存储过程用于验证域和引用完整性。
dbo.usp_ValidateIntegrity
可选参数: @object_id - 要验证其完整性的对象 ID
此过程依赖于表 dbo。DomainIntegrity,dbo。ReferentialIntegrity 和 dbo。需要验证的完整性规则的唯一Integrity - 该示例基于 AdventureWorks 数据库中原始表存在的检查、外键和唯一约束填充这些表。
它依赖于帮助程序过程dbo.usp_GenerateCKCheck、dbo.usp_GenerateFKCheck和 dbo。GenerateUQCheck 生成执行完整性检查所需的 T-SQL。
使用演示工作负荷进行性能度量
Ostress 是由 Microsoft CSS SQL Server 支持团队开发的命令行工具。 此工具可用于并行执行查询或运行存储过程。 可以将线程数配置为并行运行给定的 T-SQL 语句,并且可以指定应在此线程上执行该语句的次数;ostress 将启动线程并在所有线程上并行执行语句。 执行完成所有线程后,ostress 将报告所有线程完成执行所花费的时间。
安装 ostress
Ostress 作为 RML 实用工具的一部分安装;ostress 没有独立安装。
安装步骤:
从以下页面下载并运行 RML 实用工具的 x64 安装包: https://blogs.msdn.com/b/psssql/archive/2013/10/29/cumulative-update-2-to-the-rml-utilities-for-microsoft-sql-server-released.aspx
如果有一个对话框指示某些文件正在使用,请单击“继续”
运行 ostress
Ostress 从命令行界面运行。 从“RML Cmd 提示符”(作为 RML 实用工具的一部分安装)运行该工具最为方便。
若要打开 RML Cmd 提示符,请按照以下说明作:
在 Windows Server 2012 [R2] 和 Windows 8 和 8.1 中,单击 Windows 键打开开始菜单,然后键入“rml”。 单击“RML Cmd 提示”,该提示将出现在搜索结果列表中。
确保命令提示符位于 RML 实用工具安装文件夹中。 例如:

只需在没有任何命令行选项的情况下运行 ostress.exe 即可查看 ostress 的命令行选项。 使用此示例运行 ostress 的主要选项包括:
要连接到的 MicrosoftSQL Server 实例的 S 名称
-E 使用 Windows 身份验证进行连接(默认值):如果使用 SQL Server 身份验证,请使用选项 -U 和 -P 分别指定用户名和密码
-d 数据库的名称,本示例 AdventureWorks2014
-Q 要执行的 T-SQL 语句
每个输入文件/查询的处理连接数为 -n 个
-是执行每个输入文件/查询的每个连接的迭代次数
演示工作负荷
演示工作负荷中使用的主存储过程是Sales.usp_InsertSalesOrder_inmem/ondisk。 下面的脚本使用示例数据构造表值参数(TVP),并调用该过程以插入包含 5 行项的销售订单。
ostress 工具用于并行执行存储过程调用,模拟多个客户端同时插入销售订单的情景。
在每次压力运行中执行 Demo.usp_DemoReset 后重置演示。 此过程删除内存优化表中的行,截断基于磁盘的表,并执行数据库检查点。
将同时执行以下脚本来模拟销售订单处理工作负荷:
DECLARE
@i int = 0,
@od Sales.SalesOrderDetailType_inmem,
@SalesOrderID int,
@DueDate datetime2 = sysdatetime(),
@CustomerID int = rand() * 8000,
@BillToAddressID int = rand() * 10000,
@ShipToAddressID int = rand() * 10000,
@ShipMethodID int = (rand() * 5) + 1;
INSERT INTO @od
SELECT OrderQty, ProductID, SpecialOfferID
FROM Demo.DemoSalesOrderDetailSeed
WHERE OrderID= cast((rand()*106) + 1 as int);
WHILE (@i < 20)
BEGIN;
EXEC Sales.usp_InsertSalesOrder_inmem @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od;
SET @i += 1
END
使用此脚本,构建的每个样本订单将通过在 WHILE 循环中执行的 20 个存储过程插入 20 次。 循环用于说明数据库用于构造示例顺序的事实。 在典型的生产环境中,中间层应用程序将构造要插入的销售订单。
上述脚本将销售订单插入内存优化表中。 将销售订单插入到基于磁盘的表中的脚本是通过将“_inmem”的两个匹配项替换为“_ondisk”来派生的。
我们将使用 ostress 工具通过多个并发连接执行脚本。 我们将使用参数“-n”来控制连接数,并使用参数“r”来控制脚本在每个连接上执行的次数。
工作负荷的功能验证
若要验证一切是否正常工作,我们将从示例测试开始,使用 10 个并发连接和 5 次迭代,总共插入 10 * 5 * 20 = 1000 个销售订单。
使用以下命令,我们假定在本地计算机上使用默认实例。 如果使用命名实例或使用远程服务器,请使用参数 -S 相应地更改服务器名称。
在内存优化表中插入 1000 个销售订单,请在 RML Cmd 提示符中使用以下命令:
单击“复制”按钮复制命令,然后将其粘贴到 RML 实用工具命令提示符中。
ostress.exe -n10 -r5 -S. -E -dAdventureWorks2014 -q -Q"DECLARE @i int = 0, @od Sales.SalesOrderDetailType_inmem, @SalesOrderID int, @DueDate datetime2 = sysdatetime(), @CustomerID int = rand() * 8000, @BillToAddressID int = rand() * 10000, @ShipToAddressID int = rand() * 10000, @ShipMethodID int = (rand() * 5) + 1; INSERT INTO @od SELECT OrderQty, ProductID, SpecialOfferID FROM Demo.DemoSalesOrderDetailSeed WHERE OrderID= cast((rand()*106) + 1 as int); while (@i < 20) begin; EXEC Sales.usp_InsertSalesOrder_inmem @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od; set @i += 1 end"
如果一切按预期工作,则命令窗口将如下所示。 不应出现错误消息。

确认工作负荷在基于磁盘的表中也能按预期运行,方法是:在 RML 命令提示符中运行以下命令:
单击“复制”按钮复制命令,然后将其粘贴到 RML 实用工具命令提示符中。
ostress.exe -n10 -r5 -S. -E -dAdventureWorks2014 -q -Q"DECLARE @i int = 0, @od Sales.SalesOrderDetailType_ondisk, @SalesOrderID int, @DueDate datetime2 = sysdatetime(), @CustomerID int = rand() * 8000, @BillToAddressID int = rand() * 10000, @ShipToAddressID int = rand() * 10000, @ShipMethodID int = (rand() * 5) + 1; INSERT INTO @od SELECT OrderQty, ProductID, SpecialOfferID FROM Demo.DemoSalesOrderDetailSeed WHERE OrderID= cast((rand()*106) + 1 as int); while (@i < 20) begin; EXEC Sales.usp_InsertSalesOrder_ondisk @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od; set @i += 1 end"
运行工作负荷
为了进行大规模测试,我们使用100个连接插入1000万个销售订单。 此测试在适度的服务器(例如 8 个物理核心、16 个逻辑核心)和日志的基本 SSD 存储上合理执行。 如果测试在硬件上表现不佳,请查看 “排查运行缓慢的测试问题”部分。如果要降低此测试的压力级别,请通过更改参数“-n”来降低连接数。 例如,若要将连接计数降低到 40,请将参数“-n100”更改为“-n40”。
作为工作负荷的性能度量值,我们使用运行工作负荷后 ostress.exe 报告的已用时间。
内存优化表
首先,在内存优化表上运行工作负荷。 以下命令将打开 100 个线程,每个线程运行 5,000 次迭代。 每次迭代都会在单独的交易中插入 20 个销售订单。 每次迭代有 20 次插入来补偿数据库用于生成要插入的数据的事实。 这总共产生 20 * 5,000 * 100 = 10,000,000 个销售订单条目。
打开 RML Cmd 提示符,并执行以下命令:
单击“复制”按钮复制命令,然后将其粘贴到 RML 实用工具命令提示符中。
ostress.exe -n100 -r5000 -S. -E -dAdventureWorks2014 -q -Q"DECLARE @i int = 0, @od Sales.SalesOrderDetailType_inmem, @SalesOrderID int, @DueDate datetime2 = sysdatetime(), @CustomerID int = rand() * 8000, @BillToAddressID int = rand() * 10000, @ShipToAddressID int = rand() * 10000, @ShipMethodID int = (rand() * 5) + 1; INSERT INTO @od SELECT OrderQty, ProductID, SpecialOfferID FROM Demo.DemoSalesOrderDetailSeed WHERE OrderID= cast((rand()*106) + 1 as int); while (@i < 20) begin; EXEC Sales.usp_InsertSalesOrder_inmem @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od; set @i += 1 end"
在总共有 8 个物理(16 个逻辑)核心的测试服务器上,这需要 2 分 5 秒。 在具有 24 个物理(48 个逻辑)核心的第二个测试服务器上,这需要 1 分 0 秒。
在工作负荷运行时观察 CPU 利用率,例如使用任务管理器。 你将看到 CPU 使用率接近 100%。 如果不是这种情况,那么您遇到了日志 IO 瓶颈,另请参见 “排查运行缓慢的测试”。
基于磁盘的表
以下命令将在基于磁盘的表上运行工作负荷。 请注意,此工作负载的执行可能会花费一些时间,这主要是由于系统中存在闩锁争用。 内存优化表无闩锁,因此不会遇到此问题。
打开 RML Cmd 提示符,并执行以下命令:
单击“复制”按钮复制命令,然后将其粘贴到 RML 实用工具命令提示符中。
ostress.exe -n100 -r5000 -S. -E -dAdventureWorks2014 -q -Q"DECLARE @i int = 0, @od Sales.SalesOrderDetailType_ondisk, @SalesOrderID int, @DueDate datetime2 = sysdatetime(), @CustomerID int = rand() * 8000, @BillToAddressID int = rand() * 10000, @ShipToAddressID int = rand() * 10000, @ShipMethodID int = (rand() * 5) + 1; INSERT INTO @od SELECT OrderQty, ProductID, SpecialOfferID FROM Demo.DemoSalesOrderDetailSeed WHERE OrderID= cast((rand()*106) + 1 as int); while (@i < 20) begin; EXEC Sales.usp_InsertSalesOrder_ondisk @SalesOrderID OUTPUT, @DueDate, @CustomerID, @BillToAddressID, @ShipToAddressID, @ShipMethodID, @od; set @i += 1 end"
在总共有 8 个物理(16 个逻辑)核心的测试服务器上,这需要 41 分 25 秒。 在具有 24 个物理(48 个逻辑)核心的第二个测试服务器上,这需要 52 分 16 秒。
此测试中内存优化表与基于磁盘的表之间的性能差异的主要原因是,使用基于磁盘的表时,SQL Server 无法完全利用 CPU。 原因是闩锁争用:多个并发事务尝试写入同一数据页,闩锁用于确保在任一时刻只有一个事务能写入该页面。 OLTP 引擎 In-Memory 采用无锁设计,数据行不按页组织。 因此,并发事务不会阻止彼此的插入,从而使 SQL Server 能够充分利用 CPU。
可以在工作负荷运行时观察 CPU 使用率,例如使用任务管理器。 你会发现,基于磁盘的表,CPU 利用率远远低于 100%。 在具有 16 个逻辑处理器的测试配置上,利用率将徘徊在 24%左右。
(可选)可以使用性能监视器查看每秒闩锁等待数,其性能计数器为“\SQL Server:Latches\Latch Waits/sec”。
重置演示
若要重置演示,请打开 RML Cmd 提示符并执行以下命令:
ostress.exe -S. -E -dAdventureWorks2014 -Q"EXEC Demo.usp_DemoReset"
根据硬件的不同,这可能需要几分钟才能运行。
建议在每次运行演示后重置。 由于此工作负荷仅插入,因此每次运行都将消耗更多内存,因此需要重置以防止内存不足。 运行工作负荷后,在分区 内存利用率中讨论运行后消耗的内存量。
对运行缓慢的测试进行故障排除
测试结果通常因硬件而异,以及测试运行中使用的并发级别。 如果结果不如预期,请注意以下几点:
并发事务数:在单个线程上运行工作负荷时,In-Memory OLTP 的性能提升可能小于 2X。 并发级别较高时,闩锁争用才是一个大问题。
SQL Server 可用的处理器核心数量较少:这意味着系统中的并发性水平较低,因为并发执行的事务数量只能与 SQL Server 可用的核心数量一致。
- 症状:如果在基于磁盘的表上运行工作负荷时 CPU 占用率很高,这意味着不存在大量竞争,这表明并发性不足。
日志驱动器的速度:如果日志驱动器无法跟上系统中的事务吞吐量级别,则工作负荷在日志 IO 上变得瓶颈。 尽管使用 In-Memory OLTP 进行日志记录更高效,但如果日志 IO 是瓶颈,则潜在的性能提升会受到限制。
- 症状:如果 CPU 利用率不接近 100%,或者在内存优化表上运行工作负荷时波动很大,则可能存在日志 IO 瓶颈问题。 可以通过打开资源监视器并查看日志驱动器的队列长度来确认这一点。
示例中的内存和磁盘空间利用率
在下面,我们将介绍示例数据库的内存和磁盘空间利用率方面的预期。 我们还显示了我们在具有 16 个逻辑核心的测试服务器上看到的结果。
内存优化表的内存利用率
数据库的总体利用率
以下查询可用于获取系统中 In-Memory OLTP 的总内存利用率。
SELECT type
, name
, pages_kb/1024 AS pages_MB
FROM sys.dm_os_memory_clerks WHERE type LIKE '%xtp%'
创建数据库后快照:
| 类型 | 名字 | pages_MB |
| MEMORYCLERK_XTP | 违约 | 94 |
| MEMORYCLERK_XTP | DB_ID_5 | 877 |
| MEMORYCLERK_XTP | 违约 | 0 |
| MEMORYCLERK_XTP | 违约 | 0 |
默认内存管理器包含系统范围内存结构,并且相对较小。 用户数据库的内存分配器(在本例中为 ID 5 的数据库)约为 900MB。
每个表的内存利用率
以下查询可用于向下钻取各个表及其索引的内存利用率:
SELECT object_name(t.object_id) AS [Table Name]
, memory_allocated_for_table_kb
, memory_allocated_for_indexes_kb
FROM sys.dm_db_xtp_table_memory_stats dms JOIN sys.tables t
ON dms.object_id=t.object_id
WHERE t.type='U'
下面显示了此查询的结果,用于全新安装示例:
| 表名 | 为表分配的内存(KB) | memory_allocated_for_indexes_kb |
| SpecialOfferProduct_inmem | 64 | 3840 |
| 演示销售订单头部种子 | 1984 | 5504 |
| SalesOrderDetail_inmem | 15316 | 663552 |
| 销售订单明细示例种子 | 64 | 10432 |
| SpecialOffer_inmem | 3 | 8192 |
| SalesOrderHeader_inmem | 7168 | 147456 |
| Product_inmem | 124 | 12352 |
正如你所看到的表相当小:SalesOrderHeader_inmem大约为 7MB,SalesOrderDetail_inmem大小约为 15MB。
此处令人吃惊的是,与表数据大小相比,为索引分配的内存大小之大。 这是因为示例中的哈希索引已针对较大的数据大小预先调整大小。 请注意,哈希索引的大小固定,因此其大小不会随表中的数据大小而增长。
运行工作负荷后的内存利用率
插入 1000 万个销售订单后,所有内存利用率如下所示:
SELECT type
, name
, pages_kb/1024 AS pages_MB
FROM sys.dm_os_memory_clerks WHERE type LIKE '%xtp%'
| 类型 | 名字 | pages_MB |
| MEMORYCLERK_XTP | 违约 | 146 |
| MEMORYCLERK_XTP | DB_ID_5 | 7374 |
| MEMORYCLERK_XTP | 违约 | 0 |
| MEMORYCLERK_XTP | 违约 | 0 |
可以看到,SQL Server 在示例数据库中的内存优化表和索引上使用了略低于 8GB 的内存。
在运行一个示例后查看每个表的详细内存使用情况:
SELECT object_name(t.object_id) AS [Table Name]
, memory_allocated_for_table_kb
, memory_allocated_for_indexes_kb
FROM sys.dm_db_xtp_table_memory_stats dms JOIN sys.tables t
ON dms.object_id=t.object_id
WHERE t.type='U'
| 表名 | 为表分配的内存_kb | 为索引分配的内存_kb |
| 销售订单详情_inmem | 5113761 | 663552 |
| DemoSalesOrderDetailSeed | 64 | 10368 |
| SpecialOffer_inmem | 2 | 8192 |
| SalesOrderHeader_inmem | 1575679 | 147456 |
| Product_inmem | 111 | 12032 |
| 特价产品_内存 | 64 | 3712 |
| DemoSalesOrderHeaderSeed | 1984 | 5504 |
我们总共可以看到大约 6.5GB 的数据。 请注意,表上的索引大小SalesOrderHeader_inmem和SalesOrderDetail_inmem与插入销售订单之前索引的大小相同。 索引大小没有更改,因为两个表都使用哈希索引,哈希索引是静态的。
演示版本重置后
存储过程Demo.usp_DemoReset可用于重置演示。 它将删除表中的数据SalesOrderHeader_inmem和SalesOrderDetail_inmem,并从原始表 SalesOrderHeader 和 SalesOrderDetail 中重新设置数据种子。
现在,即使表中的行已删除,这并不意味着立即回收内存。 根据需要,SQL Server 从后台内存优化表中已删除的行回收内存。 你将看到,在演示重置后立即看到,系统上没有事务工作负荷,已删除行中的内存尚未回收:
SELECT type
, name
, pages_kb/1024 AS pages_MB
FROM sys.dm_os_memory_clerks WHERE type LIKE '%xtp%'
| 类型 | 名字 | pages_MB |
| MEMORYCLERK_XTP | 违约 | 2261 |
| MEMORYCLERK_XTP | DB_ID_5 | 7396 |
| MEMORYCLERK_XTP | 违约 | 0 |
| MEMORYCLERK_XTP | 违约 | 0 |
这是预期的:在事务工作负荷运行时,将回收内存。
如果再次启动演示工作负载,您会发现内存利用率最初会下降,因为清理了先前删除的行。 在某些时候,内存大小将再次增加,直到工作负荷完成。 在演示重置后插入 1000 万行后,内存利用率将非常类似于首次运行后的利用率。 例如:
SELECT type
, name
, pages_kb/1024 AS pages_MB
FROM sys.dm_os_memory_clerks WHERE type LIKE '%xtp%'
| 类型 | 名字 | pages_MB |
| MEMORYCLERK_XTP | 违约 | 1,863 |
| MEMORYCLERK_XTP | DB_ID_5 | 7390 |
| MEMORYCLERK_XTP | 违约 | 0 |
| MEMORYCLERK_XTP | 违约 | 0 |
内存优化表的磁盘利用率
可以使用查询在给定时间找到数据库的检查点文件的总体磁盘大小:
SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups f JOIN sys.database_files df
ON f.data_space_id=df.data_space_id
WHERE f.type=N'FX'
初始状态
最初创建示例文件组和示例内存优化表时,会预先创建多个检查点文件,系统开始填充文件 - 预先创建的检查点文件数取决于系统中的逻辑处理器数。 由于示例最初很小,因此在初始创建后,预创建的文件大多为空。
下面显示了具有 16 个逻辑处理器的计算机上的示例的初始磁盘大小:
SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups f JOIN sys.database_files df
ON f.data_space_id=df.data_space_id
WHERE f.type=N'FX'
| 磁盘上的大小(以 MB 为单位) |
| 2312 |
可以看到,检查点文件的磁盘大小(2.3GB)与实际数据大小(接近 30MB)之间存在很大的差异。
仔细查看磁盘空间利用率的来源,可以使用以下查询。 此查询返回的磁盘大小仅为估算值,适用于文件状态为 5(备份/HA 所需)、6(过渡至墓碑状态)或 7(墓碑状态)。
SELECT state_desc
, file_type_desc
, COUNT(*) AS [count]
, SUM(CASE
WHEN state = 5 AND file_type=0 THEN 128*1024*1024
WHEN state = 5 AND file_type=1 THEN 8*1024*1024
WHEN state IN (6,7) THEN 68*1024*1024
ELSE file_size_in_bytes
END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type
对于示例的初始状态,结果类似于具有 16 个逻辑处理器的服务器:
| state_desc | file_type_desc | 计数 | 磁盘大小 MB |
| 预先创建 | 数据 | 16 | 2048 |
| 预先创建 | 德尔塔 | 16 | 128 |
| 正在施工中 | 数据 | 1 | 128 |
| 正在施工中 | 德尔塔 | 1 | 8 |
正如你所看到的,预先创建的数据和增量文件占用了大部分空间。 SQL Server 为每个逻辑处理器预先创建了一对(数据、增量)文件。 此外,数据文件的预大小为 128MB,增量文件为 8MB,以便将数据插入这些文件更高效。
内存优化表中的实际数据位于单个数据文件中。
运行工作负荷后
在单次测试运行中插入 1000 万个销售订单后,对于 16 核心测试服务器,磁盘上的总体大小如下所示:
SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups f JOIN sys.database_files df
ON f.data_space_id=df.data_space_id
WHERE f.type=N'FX'
| 磁盘上的大小(以 MB 为单位) |
| 8828 |
磁盘空间大小接近 9GB,与数据在内存中的大小接近。
更仔细地查看跨各种状态的检查点文件的大小:
SELECT state_desc
, file_type_desc
, COUNT(*) AS [count]
, SUM(CASE
WHEN state = 5 AND file_type=0 THEN 128*1024*1024
WHEN state = 5 AND file_type=1 THEN 8*1024*1024
WHEN state IN (6,7) THEN 68*1024*1024
ELSE file_size_in_bytes
END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type
| state_desc | 文件类型描述 | 计数 | 磁盘大小 MB |
| 预先创建 | 数据 | 16 | 2048 |
| 预先创建 | 德尔塔 | 16 | 128 |
| 正在施工中 | 数据 | 1 | 128 |
| 正在施工中 | 德尔塔 | 1 | 8 |
我们仍然有 16 对预先创建的文件,准备在检查点关闭时使用。
当前有一对正在构建中,将在当前检查点关闭之前使用。 除了活动检查点文件之外,这为内存中 6.5GB 的数据提供了大约 6.5GB 的磁盘利用率。 回想一下,索引不会保留在磁盘上,因此磁盘上的总体大小小于内存中的大小在这种情况下。
演示重置后
在演示重置后,如果系统上没有事务工作负荷,并且没有数据库检查点,则不会立即回收磁盘空间。 要使检查点文件在各个阶段移动并最终被丢弃,需要发生多个检查点和日志截断事件,以便启动检查点文件的合并和垃圾回收。 如果在系统中具有事务性工作负荷(如果使用的是 FULL 恢复模式),则会自动执行这些作,但当系统处于空闲状态时(如演示方案中所示)。
在此示例中,在演示重置后,你可能会看到类似于
SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups f JOIN sys.database_files df
ON f.data_space_id=df.data_space_id
WHERE f.type=N'FX'
| 磁盘上的大小(以 MB 为单位) |
| 11839 |
接近 12GB,这显著多于我们在演示重置前拥有的 9GB。 这是因为已启动一些检查点文件合并,但某些合并目标尚未安装,并且某些合并源文件尚未清理,如下所示:
SELECT state_desc
, file_type_desc
, COUNT(*) AS [count]
, SUM(CASE
WHEN state = 5 AND file_type=0 THEN 128*1024*1024
WHEN state = 5 AND file_type=1 THEN 8*1024*1024
WHEN state IN (6,7) THEN 68*1024*1024
ELSE file_size_in_bytes
END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type
| state_desc | 文件类型描述 | 计数 | 磁盘大小 MB |
| 预先创建 | 数据 | 16 | 2048 |
| 预先创建 | 德尔塔 | 16 | 128 |
| 积极 | 数据 | 三十八 | 5152 |
| 积极 | 德尔塔 | 三十八 | 1331 |
| 合并目标 | 数据 | 7 | 896 |
| 合并目标 | 德尔塔 | 7 | 56 |
| 合并来源 | 数据 | 13 | 1772 |
| 合并来源 | 德尔塔 | 13 | 455 |
合并目标对象已安装,并且在系统中发生事务操作时合并源会进行清理。
在演示工作负荷的第二次运行后,在演示重置后插入 1000 万个销售订单,你将看到在第一次运行工作负荷期间构造的文件已被清理。 如果在工作负荷运行时多次运行上述查询,则可以看到检查点文件通过各个阶段流转。
在第二次运行工作负荷后,将插入 1000 万个销售订单后,会看到磁盘利用率与首次运行后的情况非常相似,因为系统本质上是动态的。 例如:
SELECT state_desc
, file_type_desc
, COUNT(*) AS [count]
, SUM(CASE
WHEN state = 5 AND file_type=0 THEN 128*1024*1024
WHEN state = 5 AND file_type=1 THEN 8*1024*1024
WHEN state IN (6,7) THEN 68*1024*1024
ELSE file_size_in_bytes
END) / 1024 / 1024 AS [on-disk size MB]
FROM sys.dm_db_xtp_checkpoint_files
GROUP BY state, state_desc, file_type, file_type_desc
ORDER BY state, file_type
| state_desc | 文件类型描述 | 计数 | 磁盘大小 MB |
| 预先创建 | 数据 | 16 | 2048 |
| 预先创建 | 德尔塔 | 16 | 128 |
| 正在施工中 | 数据 | 2 | 268 |
| 正在施工中 | 德尔塔 | 2 | 16 |
| 积极 | 数据 | 41 | 5608 |
| 积极 | 德尔塔 | 41 | 328 |
在这种情况下,有两个检查点文件对处于“正在构建”状态,这意味着由于工作负荷中的高并发性,多个文件对被移至“正在构建”状态。 多个并发线程同时需要一个新文件对,从而将一对从“预创建”移动到“正在构造中”。