在今年的8月份,我寫了篇文章,介紹了我還不推薦使用者使用記憶體OLTP的各個理由。近日很多人告訴我,他們有一些效能的問題,並考慮使用記憶體OLTP來解決它們。

眾所皆知,在SQL Server裡記憶體OLTP是個非常特別的技術,在很多情況下並不適用,但這是在SQL Server 2014裡的首次實現,它有很多限制,我在這篇文章裡已經介紹

感謝上帝——現在事情已經改變了!幾個星期前,SQL Server 2016的CTP 3版本已經可以公開下載了。在記憶體OLTP領域,微軟做出了巨大的改進。我們來詳細看下。

首先我們建立測試的資料庫:

  1. USE master
  2. GO
  3.  
  4. -- Create new database
  5. CREATE DATABASE InMemoryOLTP
  6. GO
  7.  
  8. --Add MEMORY_OPTIMIZED_DATA filegroup to the database.
  9. ALTER DATABASE InMemoryOLTP
  10. ADD FILEGROUP InMemoryOLTPFileGroup CONTAINS MEMORY_OPTIMIZED_DATA
  11. GO
  12.  
  13. USE InMemoryOLTP
  14. GO
  15.  
  16. -- Add a new file to the previous created file group
  17. ALTER DATABASE InMemoryOLTP ADD FILE
  18. (
  19. NAME = N'InMemoryOLTPContainer',
  20. FILENAME = N'C:\Program Files\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQL\DATA\InMemoryOLTPContainer'
  21. )
  22. TO FILEGROUP [InMemoryOLTPFileGroup]
  23. GO

外來鍵約束(Foreign-Key Constraints)

一個最重要的改變,或者我應該說是提升——是支援外來鍵約束!是的,你沒有聽錯:記憶體OLTP現在支援外來鍵約束。在最初的實現裡通常你會期望是支援外來鍵約束,因為這是涉及OLTP情景,但在SQL Server 2014上微軟並不支援。我們來看下面的程式碼:

  1. -- Create a parent table
  2. CREATE TABLE Parent
  3. (
  4. ParentID INT IDENTITY(1, 1) NOT NULL,
  5. Col1 CHAR(100) NOT NULL,
  6. Col2 CHAR(100) NOT NULL,
  7. Col3 CHAR(100) NOT NULL,
  8. CONSTRAINT chk_PrimaryKey_Parent PRIMARY KEY NONCLUSTERED HASH (ParentID) WITH (BUCKET_COUNT = 1024)
  9. )
  10. WITH (MEMORY_OPTIMIZED = ON)
  11. GO
  12.  
  13. -- Create a child table
  14. CREATE TABLE Child
  15. (
  16. ChildID INT IDENTITY(1, 1) NOT NULL,
  17. ParentID INT NOT NULL,
  18. Col1 CHAR(100) NOT NULL,
  19. Col2 CHAR(100) NOT NULL,
  20. Col3 CHAR(100) NOT NULL,
  21.  
  22. -- Create a FK constraint between both tables
  23. CONSTRAINT FK_Child_Parent FOREIGN KEY (ParentID)
  24. REFERENCES Parent (ParentID),
  25.  
  26. CONSTRAINT chk_PrimaryKey_Child PRIMARY KEY NONCLUSTERED HASH (ChildID) WITH (BUCKET_COUNT = 1024)
  27. )
  28. WITH (MEMORY_OPTIMIZED = ON)
  29. GO
  30.  
  31. -- Insert some records into both tables
  32. INSERT INTO Parent VALUES ('a', 'a', 'a'), ('b', 'b', 'b'), ('c', 'c', 'c')
  33. INSERT INTO Child VALUES (1, 'a', 'a', 'a'), (1, 'b', 'b', 'b'), (1, 'c', 'c', 'c')
  34. GO

這段程式碼在2個表(parent和child表)之間建立了一個簡單的外來鍵約束。另外我也在2個表裡也插入了些測試資料。現在我們對這2個表進行簡單的查詢:

  1. -- The unnecessary join is removed in the execution plan.
  2. SELECT c.* FROM Parent p
  3. JOIN Child c ON c.ParentID = p.ParentID
  4. GO

當你看查詢本身時,你會看見我只想返回child表的內容。基於外來鍵約束,查詢優化器知道在parent表裡肯定有記錄存在。因此查詢優化器通過移除不需要的表連線來簡化查詢。當你看執行計劃時,你會看到這個簡化真的發生了——非常棒:

在SQL Server的記憶體OLTP裡,這是其中一個最大的改進——支援外來鍵約束。對於外來鍵約束的支援同樣支援本地編譯的儲存過程,如下程式碼所示:

  1. -- Create a natively compiled Stored Procedure
  2. CREATE PROCEDURE InMemoryOLTPProcedure
  3. WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
  4. AS
  5. BEGIN
  6. ATOMIC WITH
  7. (
  8. TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = 'us_english'
  9. )
  10.  
  11. SELECT c.ChildID, c.ParentID, c.Col1, c.Col2, c.Col3 FROM dbo.Parent p
  12. JOIN dbo.Child c ON c.ParentID = p.ParentID
  13. END
  14. GO

當你執行本地編譯儲存過程時,同樣你沒有可用的實際執行計劃。你能檢視的只有估計執行計劃……

檢查約束(Check Constraints)

另外非常棒的提升是現在我們支援檢查約束。檢查約束非常重要,因為它告訴查詢優化器你資料長相的更多資訊。基於這些資訊,查詢優化器可以給你更好效能的執行計劃。下面這段程式碼給你展示了一個使用記憶體OLTP如何定義檢查約束的簡單例子。

  1. -- You can't create a CHECK constraint on a Memory Optimized Table
  2. CREATE TABLE CheckConstraint
  3. (
  4. ID INT IDENTITY(1, 1) PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
  5. Value INT NOT NULL DEFAULT 1 CONSTRAINT ck_Value CHECK (Value = 1)
  6. )
  7. WITH
  8. (
  9. MEMORY_OPTIMIZED = ON,
  10. DURABILITY = SCHEMA_AND_DATA
  11. )
  12. GO

從這個表定義你可以看到,這裡我建立了一個簡單的檢查約束,告訴SQL Server在列值裡只儲存1的值。甚至否定檢查在與約束檢查組合也是支援的。下面這個查詢會導致在執行計劃裡有常數掃描運算子。

  1. -- Contradiction detection works with In-Memory OLTP.
  2. SELECT * FROM CheckConstraint
  3. WHERE Value = 0
  4. GO

在字元列上的索引

回到使用SQL Server 2014的舊時光裡,記憶體OLTP裡不支援字元列上的索引,因為你必須使用BIN2排序。對於大多數人來說這是個專案障礙,因為當你在字元列上進行比較或排序時,使用另一個排序會影響結果。

使用SQL Server 2016,微軟現在已經最終移除了這個限制,現在你可以在字元列上直接建立雜湊或範圍索引,不需要使用BIN2排序。我們來看下面的例子,在SQL Server 2016裡現在是正常執行的。

  1. -- Creates a table with an index on a character column.
  2. -- This works now without any problems in SQL Server 2016.
  3. CREATE TABLE TestTable1
  4. (
  5. Col1 CHAR(10) NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
  6. Col2 VARCHAR(100) NOT NULL,
  7. Col3 VARCHAR(100) NOT NULL
  8. ) WITH
  9. (
  10. MEMORY_OPTIMIZED = ON,
  11. DURABILITY = SCHEMA_AND_DATA
  12. )
  13. GO

非常炫——太棒了!

架構和桶數改變

在SQL Server 2014裡這個真的是很糟也很爛:在記憶體優化表上架構修改是不完全不支援的。你需要刪掉並重建你的表,即使你只想增加一個索引或修改現存的索引。抱歉,這是我絕不推薦使用者使用記憶體OLTP的一個最主要原因。即使對於雜湊索引修改桶數,你也要刪除並重建你的表。

使用SQL Server 2016,生活現在好多了,一切變得簡單了。首先你可以使用簡單的ALTER INDEX REBUILD語句來修改現存索引的桶數。要留意的是你需要有表大小的2倍記憶體。下列程式碼顯示這個提升:

  1. -- We can change now the bucket count without dropping the table
  2. ALTER TABLE Parent
  3. ALTER INDEX chk_PrimaryKey_Parent
  4. REBUILD WITH (BUCKET_COUNT = 1048576)
  5. GO

另外在你建立記憶體優化表後,現在你可以修改你的表,甚至建立索引。我還沒嘗試所有的可能修改,但下面的程式碼可以給你一個你期望SQL Server 2016的一個大致想法。

  1. -- Creates a table with an index on a character column.
  2. -- This works now without any problems in SQL Server 2016.
  3. CREATE TABLE TestTable1
  4. (
  5. Col1 CHAR(10) NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
  6. Col2 VARCHAR(100) NOT NULL,
  7. Col3 VARCHAR(100) NOT NULL
  8. ) WITH
  9. (
  10. MEMORY_OPTIMIZED = ON,
  11. DURABILITY = SCHEMA_AND_DATA
  12. )
  13. GO
  14.  
  15. -- Create a new system-versioned table
  16. CREATE TABLE Persons
  17. (
  18. ID INT IDENTITY(1, 1) PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
  19. FirstName VARCHAR(100) NOT NULL,
  20. LastName VARCHAR(100) NOT NULL,
  21. City VARCHAR(100) NOT NULL,
  22.  
  23. -- Needed for System-Versioned Tables
  24. StartDate DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
  25. EndDate DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
  26. PERIOD FOR SYSTEM_TIME (StartDate, EndDate)
  27. )
  28. WITH
  29. (
  30. -- Needed for System-Versioned Tables
  31. SYSTEM_VERSIONING = ON
  32. (
  33. -- Name of the history table (optional)
  34. HISTORY_TABLE = dbo.PersonHistory
  35. ),
  36. MEMORY_OPTIMIZED = ON,
  37. DURABILITY = SCHEMA_AND_DATA
  38. )
  39. GO
  40.  
  41. -- The current table is a Memory-Optimized Table, the history table
  42. -- is a traditional Disk-Based Table.
  43. SELECT is_memory_optimized, * FROM sys.tables
  44. WHERE object_id IN(OBJECT_ID('Persons'), OBJECT_ID('PersonHistory'))
  45. GO
  46.  
  47. -- Create a parent table
  48. CREATE TABLE LetsTrySchemaChanges
  49. (
  50. ID INT IDENTITY(1, 1) NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1024),
  51. Col1 CHAR(100) NOT NULL,
  52. Col2 CHAR(100) NOT NULL,
  53. Col3 CHAR(100) NOT NULL
  54. )
  55. WITH (MEMORY_OPTIMIZED = ON)
  56. GO
  57.  
  58. -- Schema Changes are now also supported on Memory-Optimized tables.
  59. ALTER TABLE LetsTrySchemaChanges ADD Test CHAR(100) NULL
  60. GO
  61.  
  62. -- The creation of an index after the table creation is not supported
  63. CREATE NONCLUSTERED HASH INDEX idx_Test ON LetsTrySchemaChanges(Col3)
  64. WITH (BUCKET_COUNT = 1024)
  65. GO

小結

從這個文章裡,你可以看到在SQL Server 2016裡,記憶體OLTP已經徹底翻新了。現在如果有人問我它們是否應該使用記憶體中OLTP,我會說是的——如要你有對應的問題,而且這些問題使用SQL Server的傳統關係引擎不能解決的。

除了我在這篇文章裡提到的提升外,SQL Server 2016裡的記憶體OLTP可以給你的其他的提升,我會在接下來的文章裡談到。

對這些提升你有啥想法?請盡情留言。

參考文章:

http://www.sqlpassion.at/archive/2015/11/16/why-i-now-eventually-recommend-in-memory-oltp/