概觀
此範例示範記憶體內部 OLTP 功能。 它顯示記憶體最佳化資料表和原生編譯的預存程式,並可用來示範記憶體內 OLTP 的效能優勢。
注意
若要檢視 SQL Server 2014 (12.x) 的這篇文章,請參閱 AdventureWorks 的延伸模組以示範 In-Memory OLTP。
此範例會將 AdventureWorks2025 資料庫中的五個資料表移轉至經記憶體最佳化的資料表,並包含銷售訂單處理的工作負載示範。 您可以使用此示範工作負載來查看在伺服器上使用記憶體內部 OLTP 的效能優勢。
在範例的描述中,我們討論在將資料表移轉至記憶體內部 OLTP 的過程中,為了考慮記憶體最佳化資料表尚不支援的功能所做的取捨。
此範例的文件集結構如下:
安裝範例及執行工作負載示範的必要條件。
範例資料表和程式的描述 - 包括記憶體內部 OLTP 範例所新增
AdventureWorks2025的資料表和程式的描述,以及移轉某些原始AdventureWorks2025資料表以進行記憶體最佳化的考慮。執行使用示範工作負載進行效能測量的指示 - 其中包括安裝和運行用於驅動工作負載的工具ostress,以及執行示範工作負載本身的指示。
必要條件
-
SQL Server 2016 (13.x)
基於效能測試考量,伺服器的規格必須與您的實際執行環境類似。 針對此特定範例,您應該準備至少 16 GB 的記憶體供 SQL Server 使用。 如需記憶體內部 OLTP 硬體的一般指導方針,請參閱下列部落格文章: SQL Server 中 In-Memory OLTP 的硬體考慮
安裝以 AdventureWorks 為基礎的記憶體內部 OLTP 範例
請遵循下列步驟來安裝範例:
從
AdventureWorks2016_EXT.bak將SQLServer2016Samples.zip和 https://github.com/microsoft/sql-server-samples/releases/tag/adventureworks 下載至本機資料夾,例如C:\Temp。使用 Transact-SQL 或 SQL Server Management Studio 還原資料庫備份:
識別資料檔案的目標資料夾和檔案名稱,例如:
H:\DATA\AdventureWorks2022_Data.mdf識別記錄檔的目標資料夾和檔名,例如:
I:\DATA\AdventureWorks2022_log.ldf- 放置記錄檔的磁碟機應該與放置資料檔案的磁碟機不同,最好使用低度延遲的磁碟機以取得最高效能,例如 SSD 或 PCIe 儲存體。
範例 T-SQL 指令碼:
RESTORE DATABASE [AdventureWorks2022] FROM DISK = N'C:\temp\AdventureWorks2022.bak' WITH FILE = 1, MOVE N'AdventureWorks2022_Data' TO N'h:\DATA\AdventureWorks2022_Data.mdf', MOVE N'AdventureWorks2022_Log' TO N'i:\DATA\AdventureWorks2022_log.ldf', MOVE N'AdventureWorks2022_mod' TO N'h:\data\AdventureWorks2022_mod' GO若要檢視範例指令碼和工作負載,請將檔案
SQLServer2016Samples.zip解壓縮至本機資料夾。 請參閱檔案In-Memory OLTP\readme.txt,以取得執行工作負載的指示。
範例資料表和程序描述
此範例以 AdventureWorks2025 中的現有資料表為基礎,為產品和銷售訂單建立新資料表。 新資料表的結構描述與現有資料表類似,但有一些差異,如本節稍後所述。
新的記憶體最佳化資料表帶有後綴 _inmem。 此範例也包含帶有尾碼 _ondisk 的對應資料表 - 這些資料表可用來在系統上記憶體最佳化資料表與磁碟型資料表的效能之間進行一對一比較。
工作負載中用於比較效能的經記憶體最佳化的資料表是完全持久且完整記錄的。 他們不會為了獲得性能提升而犧牲耐用性或可靠性。
此範例的目標工作負載是銷售訂單處理,同時也會考量產品和折扣的相關資訊。 為此,我們使用資料表 SalesOrderHeader、SalesOrderDetail、Product、SpecialOffer 和 SpecialOfferProduct。
兩個新的預存程序 Sales.usp_InsertSalesOrder_inmem 和 Sales.usp_UpdateSalesOrderShipInfo_inmem 用於插入銷售訂單和更新指定銷售訂單的發貨資訊。
新的架構 Demo 包含輔助資料表和預存程序,用於執行示範工作負載。
具體而言,記憶體內部 OLTP 範例會將下列物件加入 AdventureWorks2025:
由範例添加的資料表
新資料表
Sales.SalesOrderHeader_inmem
- 銷售訂單的相關標頭資訊。 每個銷售訂單在此資料表中有一個資料列。
Sales.SalesOrderDetail_inmem
- 銷售訂單的詳細資料。 銷售訂單的每個明細項目在此表格中都有一列。
Sales.SpecialOffer_inmem
- 特殊供應項目的相關資訊,包括與每個特殊供應項目相關聯的折扣百分比。
Sales.SpecialOfferProduct_inmem
- 特殊優惠與產品之間的參考表。 每個特殊供應項目可能適用於零個或多個產品,而每個產品也可適用於零個或多個特殊供應項目。
Production.Product_inmem
- 產品的相關資訊,包括產品的標價。
Demo.DemoSalesOrderDetailSeed
- 在範例工作負載中用於建構銷售訂單範本。
以磁碟為基礎的資料表的變異:
Sales.SalesOrderHeader_ondiskSales.SalesOrderDetail_ondiskSales.SpecialOffer_ondiskSales.SpecialOfferProduct_ondiskProduction.Product_ondisk
原始磁碟資料表與新的記憶體最佳化資料表之間的差異
通常,此範例引進的新資料表會使用與原始資料表相同的資料行和相同的資料類型。 但是仍有一些差異。 我們列出本節中的差異,以及變更的理由。
Sales.SalesOrderHeader_inmem
記憶體最佳化資料表支援「預設條件約束」 ,且大部分的預設條件約束在移轉後會保持原狀。 不過,原始資料表
Sales.SalesOrderHeader包含兩個預設條件約束,會擷取資料行OrderDate和ModifiedDate的目前日期。 在具備大量並行性的高吞吐量訂單處理工作負載中,任何全域資源都可能成為爭奪焦點。 系統時間是一種全球性資源,我們觀察到它在執行 In-Memory OLTP 工作負載並插入銷售訂單時,可能會成為瓶頸,特別是當需要擷取銷售訂單標頭中多個資料行的系統時間,以及銷售訂單詳細資料時。 這個問題已在此範例中獲得解決,只對每個插入的銷售訂單擷取一次系統時間,然後在預存程序SalesOrderHeader_inmem中,將此值做為SalesOrderDetail_inmem和Sales.usp_InsertSalesOrder_inmem的日期時間資料行使用。別名使用者定義資料類型(UDT)——原始資料表使用兩個別名 UDT 和
dbo.OrderNumber,分別用於資料行dbo.AccountNumber和PurchaseOrderNumber。 SQL Server 2016 (13.x) 不支援記憶體最佳化資料表的別名 UDT,因此新資料表會分別使用系統資料類型 nvarchar(25) 和 nvarchar(15)。索引鍵中的可 Null 資料行 - 在原始資料表中,資料行
SalesPersonID可為 Null,而在新資料表中,資料行不可為 Null,並且設有預設值為 -1 的約束。 這種情況是因為記憶體最佳化資料表上的索引在索引鍵中不能包含可為 Null 的資料行。在這種情況下,使用 -1 作為 NULL 的替代值。計算資料行 - 省略計算資料行
SalesOrderNumber和TotalDue,因為 SQL Server 2016 (13.x) 不支援記憶體最佳化資料表中的計算資料行。 新的檢視Sales.vSalesOrderHeader_extended_inmem顯示資料行SalesOrderNumber和TotalDue。 因此,如果需要這些資料行,您可以使用此檢視。- 適用於: SQL Server 2017 (14.x)。 從 SQL Server 2017 (14.x) 開始,記憶體最佳化的資料表與索引支援計算資料行。
外部索引鍵條件約束獲得 SQL Server 2016 (13.x) 中的記憶體最佳化資料表支援,但僅限於參考的資料表也處於記憶體最佳化的情況。 參考移轉至記憶體最佳化之資料表的外部索引鍵會保留在移轉的資料表中,而其他外部索引鍵會被省略。 此外,
SalesOrderHeader_inmem是範例工作負載中的熱表,而外鍵約束需要對所有 DML 作業進行額外處理,因為它需要在這些約束中參照的所有其他表格中進行查詢。 因此,假設應用程式可確保資料表的Sales.SalesOrderHeader_inmem參考完整性,而且插入資料列時不會驗證參考完整性。Rowguid - 已省略 Rowguid 欄。 雖然記憶體最佳化資料表支援 uniqueidentifier,但 SQL Server 2016 (13.x) 不支援 ROWGUIDCOL 選項。 這類資料行通常用於合併複寫或具有 filestream 資料行的資料表。 此範例不包含這兩者。
銷售.訂單明細
預設條件約束 - 類似
SalesOrderHeader於 ,不會移轉需要系統日期/時間的預設條件約束。 相反地,插入銷售訂單的預存程序會在第一次插入時負責插入目前的系統日期/時間。計算資料行 - 未移轉計算資料行
LineTotal,因為 SQL Server 2016 (13.x) 中的記憶體最佳化資料表不支援計算資料行。 若要存取此欄,請使用Sales.vSalesOrderDetail_extended_inmem檢視。Rowguid -
rowguid資料欄位已被省略。 如需詳細資訊,請參閱SalesOrderHeader資料表的描述。
生產.產品
別名 UDTs - 原始資料表使用使用者定義的資料類型 ,相當於系統資料類型 bit。 移轉的資料表會改用 bit 資料類型。
Rowguid -
rowguid資料欄位已被省略。 如需詳細資訊,請參閱SalesOrderHeader資料表的描述。
銷售.特別優惠
-
Rowguid -
rowguid資料欄位已被省略。 如需詳細資訊,請參閱SalesOrderHeader資料表的描述。
Sales.SpecialOfferProduct
-
Rowguid -
rowguid資料欄位已被省略。 如需詳細資訊,請參閱SalesOrderHeader資料表的描述。
記憶體最佳化資料表上索引的注意事項
記憶體最佳化資料表的基準索引為 NONCLUSTERED 索引,支援點查閱 (等號比較述詞的索引搜尋)、範圍掃描 (不等號比較述詞的索引搜尋)、完整索引掃描及依序掃描。 此外,NONCLUSTERED 索引支援使用索引鍵的前置資料行進行搜尋。 事實上,記憶體最佳化的 NONCLUSTERED 索引支援以磁碟為基礎之 NONCLUSTERED 索引支援的所有作業,唯一的例外狀況是回溯掃描。 因此,使用 NONCLUSTERED 索引對您的索引而言是安全的選擇。
HASH 索引可用來進一步最佳化工作負載。 對於點查詢和資料列插入,這些操作已進行最佳化。 不過,必須注意它們不支援範圍掃描、排序掃描,或在前導索引鍵列上搜尋。 因此,您需要謹慎地使用這些索引。 此外,必須在建立時指定bucket_count。 此計數通常應該設定為索引鍵值數量的一到兩倍之間,不過通常高估並不會造成問題。
其他資訊:
已移轉資料表上的索引已針對示範銷售訂單處理工作負載進行調整。 工作負載依賴資料表 Sales.SalesOrderHeader_inmem 和 Sales.SalesOrderDetail_inmem 中的插入和點查詢,也依賴資料表 Production.Product_inmem 和 Sales.SpecialOffer_inmem 中對於主索引鍵資料行的點查詢。
Sales.SalesOrderHeader_inmem 具有三個索引,基於效能的考量,以及由於此工作負載不需要依序或範圍掃描,這三個索引都是 HASH 索引。
(
SalesOrderID) 上的 HASH 索引:「bucket_count」的大小調整為 1,000 萬(向上取整到 1,600 萬),因為預期的銷售訂單數目為 1,000 萬(
SalesPersonID) 上的 HASH 索引:bucket_count 為 100 萬。 提供的資料集沒有很多銷售人員。 但這個高容量的 bucket_count 允許未來的成長。 此外,即使 bucket_count 過大,您在進行點查詢時也不會承擔效能損失。(
CustomerID) 上的 HASH 索引:bucket_count 為 100 萬。 提供的數據集沒有很多客戶,但這允許未來的增長。
Sales.SalesOrderDetail_inmem 具有三個索引,基於效能的考量,以及由於此工作負載不需要依序或範圍掃描,這三個索引都是 HASH 索引。
(,
SalesOrderID)SalesOrderDetailID上的HASH索引:這是主鍵索引,即使對(SalesOrderID,SalesOrderDetailID)的查找不頻繁,但對鍵使用雜湊索引可以加快行插入速度。 bucket_count 的大小設置為 5,000 萬(進位至 6,700 萬):預期的銷售訂單數目為 1,000 萬,此大小設定是為了使每個訂單平均包含五個項目。HASH 索引(
SalesOrderID):經常需要按銷售訂單查找:您希望找到與單一訂單相對應的所有明細項目。 bucket_count 的大小調整為 1,000 萬 (無條件進位到 1,600 萬),因為預期的銷售訂單數目為 1,000 萬(
ProductID) 上的 HASH 索引:bucket_count 為 100 萬。 提供的數據集沒有很多產品,但這允許未來的增長。
Production.Product_inmem 有三個索引
(
ProductID) 上的 HASH 索引:ProductID上的查詢處於示範工作負載的關鍵路徑中,因此這是雜湊索引非叢集索引(
Name):這允許對產品名稱進行有序掃描非聚集索引(
ProductNumber):這允許對產品編號進行有序掃描
Sales.SpecialOffer_inmem 在 (SpecialOfferID) 上有一個 HASH 索引:特價優惠的點查詢是示範工作負載中的關鍵部分。
bucket_count 的大小設置為一百萬,以便於未來的擴展。
Sales.SpecialOfferProduct_inmem 不會在示範工作負載中參考,因此顯然不需要在此資料表上使用雜湊索引來優化工作負載 - (SpecialOfferID、 ProductID) 和 (ProductID) 上的索引是非叢集的。
在之前的範例中,某些桶的計數過大,但在 SalesOrderHeader_inmem 和 SalesOrderDetail_inmem 上的索引的桶容量並未過大:它們僅適用於1,000萬個銷售訂單。 這樣做是為了允許在記憶體可用性較低的系統上安裝範例,儘管在這些情況下,演示工作負載會失敗並出現記憶體不足錯誤。 如果您想要擴展到超過 1,000 萬個銷售訂單,可以根據需求隨時增加桶數。
記憶體使用量的考量
範例資料庫中的記憶體使用情形在工作負載示範前後的變化,都會在 記憶體最佳化資料表的記憶體使用量一節中說明。
範例新增的預存程序
以下是用於插入銷售訂單及更新出貨詳細資料的兩個主要預存程序:
Sales.usp_InsertSalesOrder_inmem在資料庫中插入新的銷售訂單,並輸出該銷售訂單的
SalesOrderID。 作為輸入參數,它會取得銷售訂單標題的詳細資料,以及訂單中的明細項目。輸出參數:
- @SalesOrderID int - 剛插入的銷售訂單標識符
輸入參數 (必要):
- @DueDatedatetime2
- @CustomerIDint
- @BillToAddressIDint
- @ShipToAddressIDint
- @ShipMethodIDint
-
@SalesOrderDetails
Sales.SalesOrderDetailType_inmem- 包含訂單明細項目的表格值引數 (TVP)
輸入參數 (選擇性):
- @Statustinyint
- @OnlineOrderFlag位
- @PurchaseOrderNumbernvarchar(25)
- @AccountNumbernvarchar(15)
- @SalesPersonIDint
- @TerritoryIDint
- @CreditCardIDint
- @CreditCardApprovalCodevarchar(15)
- @CurrencyRateIDint
- @Commentnvarchar(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_inmemProduction.usp_DeleteProduct_inmemSales.usp_InsertSpecialOffer_inmemSales.usp_DeleteSpecialOffer_inmemSales.usp_InsertSpecialOfferProduct_inmem
最後,下列預存程序可用來驗證網域和參考完整性。
dbo.usp_ValidateIntegrity選用參數: @object_id - 要驗證完整性的物件識別碼
此程序依賴資料表
dbo.DomainIntegrity、dbo.ReferentialIntegrity和dbo.UniqueIntegrity的完整性規則,這些規則需要檢驗。此範例根據AdventureWorks2025資料庫中原始資料表的檢查、外部索引鍵及唯一條件約束,來填入這些資料表。這依賴協助程式程序
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 安裝套件:下載適用於 SQL Server 的 RML
如果出現對話框指出某些檔案正在使用中,請選擇「繼續」
跑奧特雷斯
Ostress 會從命令列介面執行。 從 RML 實用程序的一部分安裝的 RML Cmd 提示符運行該工具是最方便的。
若要開啟 RML CMD 命令提示字元,請遵循下列指示:
在 Windows 中,選取 Windows 鍵開啟 [開始] 功能表,然後輸入 rml。 選取 RML Cmd Prompt,它位於搜尋結果清單中。
確定命令提示字元位於 RML 公用程式安裝資料夾中。
在沒有任何命令列選項的情況下簡單執行 ostress 時,即可看到命令列選項。 使用此範例運行 壓力 時要考慮的主要選項如下:
| Option | Description |
|---|---|
-S |
要連線的 SQL Server 執行個體名稱。 |
-E |
使用 Windows 驗證進行連線 (預設);如果您使用 SQL Server 驗證,請使用 和 -U 選項-P分別指定使用者名稱和密碼。 |
-d |
資料庫的名稱,在此範例 AdventureWorks2025中。 |
-Q |
要執行的 T-SQL 陳述式。 |
-n |
處理每個輸入檔案/查詢的連線數。 |
-r |
每個連線執行每個輸入檔案/查詢的反覆運算次數。 |
示範工作量
示範工作負載中使用的主要預存程序是 Sales.usp_InsertSalesOrder_inmem/ondisk。 下列範例中的指令碼會建構具有範例資料的資料表值參數 (TVP),並呼叫程序以插入具有五個明細項目的銷售訂單。
ostress 工具用於並行執行預存程序調用,以模擬客戶同時插入銷售訂單。
每次壓力執行 Demo.usp_DemoReset 之後,請重設示範。 此程序會刪除記憶體最佳化資料表中的資料列、截斷磁碟資料表,並執行資料庫檢查點。
下列指令碼會同時執行,以模擬銷售訂單處理工作負載:
DECLARE @i AS INT = 0, @od AS Sales.SalesOrderDetailType_inmem, @SalesOrderID AS INT, @DueDate AS DATETIME2 = sysdatetime(), @CustomerID AS INT = RAND() * 8000, @BillToAddressID AS INT = RAND() * 10000, @ShipToAddressID AS INT = RAND() * 10000, @ShipMethodID AS 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
EXECUTE Sales.usp_InsertSalesOrder_inmem
@SalesOrderID OUTPUT,
@DueDate,
@CustomerID,
@BillToAddressID,
@ShipToAddressID,
@ShipMethodID,
@od;
SET @i + = 1;
END
利用此指令碼,每個建構的範例訂單會透過以 WHILE 迴圈執行的 20 個預存程序被插入 20 次。 此迴圈可用來說明使用資料庫建構範例訂單的情況。 在典型的生產環境中,中間層應用程式會建構要插入的銷售訂單。
上一個指令碼將銷售訂單插入記憶體最佳化資料表。 將銷售訂單插入磁碟型表格的腳本是透過將 的 _inmem 兩個出現位置取代為 _ondisk來衍生的。
我們使用 ostress 工具使用多個並發連接來執行腳本。 我們使用參數 -n 來控制連接的數量,並使用參數 r 來控制腳本在每個連接上執行的次數。
執行工作負載
為了進行大規模測試,我們利用 100 個連接插入 1,000 萬筆銷售訂單。 此測試會在適合的伺服器 (例如 8 個實體、16 個邏輯核心) 上適當地執行,並使用基本 SSD 儲存體儲存記錄檔。 如果測試在您的硬體上執行不佳,請參考排除執行緩慢的測試問題一節。 如果您想要降低此測試的應力等級,請變更參數 -n來降低連接數。 例如,若要將連線計數降低至 40,請將參數 -n100 變更為 -n40。
作為衡量工作負載效能的方法,我們使用執行工作負載後由 ostress.exe 所報告的經過時間。
下列指示和測量使用插入一千萬個銷售訂單的工作負載。 如需執行插入一百萬個銷售訂單的縮減工作負載的指示,請參閱封存中的In-Memory OLTP\readme.txt指示SQLServer2016Samples.zip。
記憶體最佳化的資料表
我們首先在記憶體最佳化資料表上執行工作負載。 下列命令會開啟 100 個執行緒,每個執行緒執行 5,000 次反覆運算。 每次迭代會將 20 個銷售訂單插入到個別交易中。 每次反覆運算會插入 20 個資料,以彌補由資料庫產生要插入之資料的情況。 總計產生 20 * 5,000 * 100 = 10,000,000 個銷售訂單插頁。
開啟 RML Cmd 提示字元,然後執行下列命令:
選取 [複製] 按鈕複製命令,然後將其貼入 RML 公用程式命令提示字元。
ostress.exe -n100 -r5000 -S. -E -dAdventureWorks2022 -q -Q"DECLARE @i AS INT = 0, @od AS Sales.SalesOrderDetailType_inmem, @SalesOrderID AS INT, @DueDate AS DATETIME2 = SYSDATETIME(), @CustomerID AS INT = RAND() * 8000, @BillToAddressID AS INT = RAND() * 10000, @ShipToAddressID AS INT = RAND() * 10000, @ShipMethodID AS 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 EXECUTE 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 -dAdventureWorks2022 -q -Q"DECLARE @i AS INT = 0, @od AS Sales.SalesOrderDetailType_ondisk, @SalesOrderID AS INT, @DueDate AS DATETIME2 = sysdatetime(), @CustomerID AS INT = RAND() * 8000, @BillToAddressID AS INT = RAND() * 10000, @ShipToAddressID AS INT = RAND() * 10000, @ShipMethodID AS 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 EXECUTE 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。 原因在於鎖存器競爭:並行交易嘗試寫入相同的資料頁面;鎖存器用來確保每次只有一筆交易能夠寫入頁面。 In-Memory OLTP 引擎是無鎖的,而且資料列不會組織在頁中。 因此,並行交易不會封鎖彼此的插入,因此 SQL Server 能夠充分利用 CPU。
您可以觀察執行工作負載時的 CPU 使用率,例如使用工作管理員。 您會看到,使用基於磁碟的表,CPU 使用率遠非 100%。 在具有 16 個邏輯處理器的測試組態中,使用率保持在 24% 左右。
選擇性地,您可以使用效能監視器搭配其效能計數器 \SQL Server:Latches\Latch Waits/sec,檢視每秒的閂鎖等候次數。
重設示範
若要重設示範,請開啟 RML CMD 命令提示字元,然後執行下列命令:
ostress.exe -S. -E -dAdventureWorks2022 -Q"EXEC Demo.usp_DemoReset"
視硬體而定,這可能需要幾分鐘才能執行。
建議每次執行示範之後將其重設。 因為此工作負載是僅限插入的,所以每次執行都會耗用更多記憶體,因此需要重設以防止記憶體不足。 執行工作負載之後的記憶體使用量章節中會討論執行後的記憶體耗用量。
排除測試執行緩慢的問題
測試結果通常會因硬體而異,以及測試執行中使用的並行層級而有所不同。 如果結果不符合預期,需要注意以下幾點:
並行交易數目:在單一執行緒上執行工作負載時,In-Memory OLTP 的效能增益可能小於 2 倍。 閂鎖爭用只有在並行層級很高時才會發生重大問題。
SQL Server 可用的核心數目較少:這表示系統中的並行層級較低,因為並行執行的交易數量只能與 SQL 可用的核心數目相同。
- 症狀:如果在磁碟型資料表上執行工作負載時 CPU 使用率很高,這表示沒有太多爭用,指出缺乏並行。
日誌磁碟機的速度:如果日誌磁碟機無法支持系統中的交易處理量,則工作負載就會成為日誌 IO 上的瓶頸。 雖然記憶體內部 OLTP 的記錄效率較高,但一旦記錄 IO 成為瓶頸,將會限制可能提升的效能。
- 徵兆:如果 CPU 使用率未接近 100%,或者在記憶體最佳化資料表上執行工作負載時出現大幅波動,那麼可能存在日誌 IO 瓶頸。 這可以透過開啟資源監視器並查看記錄磁碟機的佇列長度來確認。
範例中的記憶體和磁碟空間使用率
在下列範例中,我們會說明範例資料庫的記憶體和磁碟空間使用率。 我們還顯示了在具有 16 個邏輯內核的測試服務器上的結果。
記憶體最佳化資料表的記憶體使用量
資料庫的整體使用情況
下列查詢可用來取得系統中記憶體內部 OLTP 的總記憶體使用量。
SELECT type,
name,
pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';
以下是剛建立資料庫之後的快照集:
| 型別 | 名稱 | 頁面_MB |
|---|---|---|
| MEMORYCLERK_XTP | 預設 | 94 |
| MEMORYCLERK_XTP | DB_ID_5 | 877 |
| MEMORYCLERK_XTP | 預設 | 0 |
| MEMORYCLERK_XTP | 預設 | 0 |
預設記憶體 Clerk 包含全系統記憶體結構,且規模相對較小。 使用者資料庫的記憶體管理員,在此案例中為 ID 為 5 的資料庫(在您的database_id執行個體中可能有所不同),約為 900 MB。
每個資料表的記憶體使用量
下列查詢可用來深入分析個別資料表及其索引的記憶體使用量:
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 AS dms
INNER JOIN sys.tables AS t
ON dms.object_id = t.object_id
WHERE t.type = 'U';
下表顯示範例全新安裝的查詢結果:
| 數據表名稱 | memory_allocated_for_table_kb |
memory_allocated_for_indexes_kb |
|---|---|---|
SpecialOfferProduct_inmem |
64 | 3840 |
DemoSalesOrderHeaderSeed |
1984 | 5504 |
SalesOrderDetail_inmem |
15316 | 663552 |
DemoSalesOrderDetailSeed |
64 | 10432 |
SpecialOffer_inmem |
3 | 8192 |
SalesOrderHeader_inmem |
7168 | 147456 |
Product_inmem |
124 | 12352 |
如您所見,這些表格相當小: SalesOrderHeader_inmem 大約 7 MB, SalesOrderDetail_inmem 大小約為 15 MB。
這裡引人注目的是,配置給索引的記憶體大小與資料表資料大小相比。 這是因為範例中的雜湊索引已針對較大的資料大小進行預設調整。 雜湊索引具有固定大小,因此其大小不會隨著資料表中資料的大小而成長。
執行工作負載之後的記憶體使用量
插入一千萬個銷售訂單之後,全部記憶體使用率看起來類似於下列查詢:
SELECT type,
name,
pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';
結果如下。
type |
name |
pages_MB |
|---|---|---|
| MEMORYCLERK_XTP | 預設 | 146 |
| MEMORYCLERK_XTP | DB_ID_5 | 7374 |
| MEMORYCLERK_XTP | 預設 | 0 |
| MEMORYCLERK_XTP | 預設 | 0 |
如您所見,SQL Server 針對範例資料庫中的經記憶體最佳化的資料表和索引使用小於 8 GB 的記憶體。
請在執行一個範例之後,查看每個資料表的詳細記憶體使用量:
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 AS dms
INNER JOIN sys.tables AS t
ON dms.object_id = t.object_id
WHERE t.type = 'U';
結果如下。
Table name |
memory_allocated_for_table_kb |
memory_allocated_for_indexes_kb |
|---|---|---|
| SalesOrderDetail_inmem | 5113761 | 663552 |
| 示範銷售訂單詳情種子 | 64 | 10368 |
| 特別優惠_inmem | 2 | 8192 |
| SalesOrderHeader_inmem | 1575679 | 147456 |
| Product_inmem | 111 | 12032 |
| SpecialOfferProduct_inmem | 64 | 3712 |
| DemoSalesOrderHeaderSeed | 1984 | 5504 |
我們會看到總計約 6.5 GB 的資料。 表格 SalesOrderHeader_inmem 和 SalesOrderDetail_inmem 上的索引大小與插入銷售訂單之前的索引大小相同。 索引大小沒有變更,因為兩個資料表都使用雜湊索引,而雜湊索引是靜態的。
示範重設後
可使用預存程序 Demo.usp_DemoReset 重設示範。 它會刪除表格 SalesOrderHeader_inmem 和 SalesOrderDetail_inmem中的資料,並從原始表格SalesOrderHeaderSalesOrderDetail和 重新植入資料。
現在,即使刪除了表格中的列,這並不意味著會立即回收記憶體。 SQL Server 會視需要在背景回收記憶體最佳化資料表中已刪除資料列的記憶體。 您會看到,在範例重置後,系統上沒有交易式工作負載,但已刪除資料列的記憶體尚未回收:
SELECT type,
name,
pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';
結果如下。
type |
name |
pages_MB |
|---|---|---|
| MEMORYCLERK_XTP | 預設 | 2261 |
| MEMORYCLERK_XTP | DB_ID_5 | 7396 |
| MEMORYCLERK_XTP | 預設 | 0 |
| MEMORYCLERK_XTP | 預設 | 0 |
這是預期的:當交易式工作負載執行時,會回收記憶體。
如果您開始第二次執行示範工作負載,您會看到記憶體使用率一開始會降低,因為先前刪除的資料列會遭到清除。 在某個時間點,記憶體大小會再次增加,直到工作負載完成為止。 在示範重設後插入一千萬列之後,記憶體使用率與第一次執行後的使用率非常相似。 例如:
SELECT type,
name,
pages_kb / 1024 AS pages_MB
FROM sys.dm_os_memory_clerks
WHERE type LIKE '%xtp%';
結果如下。
type |
name |
pages_MB |
|---|---|---|
| MEMORYCLERK_XTP | 預設 | 1863 |
| 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 AS f
INNER JOIN sys.database_files AS 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 AS f
INNER JOIN sys.database_files AS df
ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';
結果如下。
| 磁碟大小以MB計 |
|---|
| 2312 |
如您所見,檢查點檔案的磁碟大小(2.3 GB)與實際資料大小(接近 30 MB)之間存在很大差異。
您可以使用下列查詢,更仔細地查看磁碟空間的使用來源。 這個查詢傳回的磁碟大小近似於狀態 5 (REQUIRED FOR BACKUP/HA)、6 (IN TRANSITION TO TOMBSTONE) 或 7 (TOMBSTONE) 的檔案。
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 個邏輯處理器的伺服器:
| 狀態描述 | 檔案類型描述 | 計數 | 磁碟空間大小 MB |
|---|---|---|---|
| 已預先建立 | 數據 | 16 | 2048 |
| 已預先建立 | DELTA | 16 | 128 |
| 施工中 | 數據 | 1 | 128 |
| 施工中 | DELTA | 1 | 8 |
如您所見,大部分空間都被已預先創建的資料和差異檔案佔用。 SQL Server 事先預先為每個邏輯處理器建立一對(資料、差異)檔案。 此外,資料檔案的大小預設為 128 MB,增量檔案的大小為 8 MB,以便更有效率地將資料插入這些檔案。
記憶體最佳化資料表中的實際資料會包含在單一資料檔案中。
執行工作負載之後
在一次插入 1,000 萬個銷售訂單的測試執行後,磁碟上的整體大小如下所示(以 16 核心的測試伺服器為例):
SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups AS f
INNER JOIN sys.database_files AS df
ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';
結果如下。
| 磁碟大小以MB計 |
|---|
| 8828 |
在磁碟上的大小接近 9 GB,與記憶體中的資料大小更接近。
更仔細地查看不同狀態下檢查點檔案的大小:
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 |
file_type_desc |
count |
on-disk size MB |
|---|---|---|---|
| 已預先建立 | 數據 | 16 | 2048 |
| 已預先建立 | DELTA | 16 | 128 |
| 施工中 | 數據 | 1 | 128 |
| 施工中 | DELTA | 1 | 8 |
關閉檢查點時,我們仍然有 16 對預先建立的文件,已經準備好使用。
有一對正在建置中,將會使用到當前檢查點關閉為止。 連同使用中的檢查點檔案,記憶體中 6.5 GB 資料的磁碟空間使用量約為 6.5 GB。 回想一下,索引不會保存在磁碟上,因此在此情況下,磁碟上的整體大小小於記憶體中的大小。
示範重設後
示範重設之後,如果系統上沒有交易式工作負載,而且沒有資料庫檢查點,則不會立即回收磁碟空間。 若要讓檢查點檔案在各個階段中移動並最終捨棄,需要發生數個檢查點及日誌截斷事件,以起始檢查點檔案的合併,以及起始記憶體回收。 如果您的系統中有交易式工作負載 (並定期進行記錄備份,以防您使用 FULL 復原模型),則會自動發生這些事件,但不會在系統閒置時發生,就像在示範案例中一樣。
在此範例中,在示範重設之後,您可能會看到類似以下內容:
SELECT SUM(df.size) * 8 / 1024 AS [On-disk size in MB]
FROM sys.filegroups AS f
INNER JOIN sys.database_files AS df
ON f.data_space_id = df.data_space_id
WHERE f.type = N'FX';
結果如下。
| 磁碟大小以MB計 |
|---|
| 11839 |
接近 12 GB 的檔案大小,這比我們在示範重設前的 9 GB 大很多。 這是因為一些檢查點檔案合併已啟動,但某些合併目標尚未安裝,部分合併來源檔案尚未清理,從以下範例中可以看出:
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 |
file_type_desc |
count |
on-disk size MB |
|---|---|---|---|
| 已預先建立 | 數據 | 16 | 2048 |
| 已預先建立 | DELTA | 16 | 128 |
| 啟用中 | 數據 | 38 | 5152 |
| 啟用中 | DELTA | 38 | 1331 |
| 合併目標 | 數據 | 7 | 896 |
| 合併目標 | DELTA | 7 | 56 |
| 已合併的來源 | 數據 | 13 | 1772 |
| 已合併的來源 | DELTA | 13 | 455 |
隨著系統中發生交易活動,合併目標被安裝,合併的來源被清除。
在第二次執行示範工作負載後,重設示範並插入一千萬個銷售訂單後,您可以看到在第一次執行工作負載期間建構的檔案已被清除。 如果您在工作負載執行時多次執行前一個查詢,則可以看到檢查點檔案通過各個階段。
在第二次執行工作量插入一千萬個銷售訂單之後,您會看到磁碟使用率與第一次執行之後非常相似,但不一定相同,因為系統本質上是動態的。 例如:
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 |
file_type_desc |
count |
on-disk size MB |
|---|---|---|---|
| 已預先建立 | 數據 | 16 | 2048 |
| 已預先建立 | DELTA | 16 | 128 |
| 施工中 | 數據 | 2 | 268 |
| 施工中 | DELTA | 2 | 16 |
| 啟用中 | 數據 | 41 | 5608 |
| 啟用中 | DELTA | 41 | 328 |
在這種情況下,有兩對檢查點檔案處於UNDER CONSTRUCTION狀態,這表示多對檔案已移至UNDER CONSTRUCTION狀態,可能是由於工作負載中的高並發層級所致。 多個並行線程需要同時獲取一對新的檔案,因而將一對從 PRECREATED 移動到 UNDER CONSTRUCTION。