SQL Server Memory Optimized Tables – Der Leitfaden für In-Memory OLTP

Ultra-schnelle Tabellen, latch-freie Architektur, native Kompilierung – wann sie sinnvoll sind und wie Sie sie richtig einsetzen

📌 Was sind Memory Optimized Tables?

GRUNDLAGEN
Memory Optimized Tables sind ein zentraler Bestandteil der In-Memory OLTP-Funktionalität in SQL Server. Im Gegensatz zu klassischen, auf Festplatten basierenden Tabellen werden alle Datenzeilen primär im Arbeitsspeicher gehalten. Gleichzeitig gibt es eine persistente Version auf der Festplatte – bei jedem Commit wird ein Eintrag ins Transaktionsprotokoll geschrieben. Die Architektur ist latch-frei und optimistisch, was zu massiven Performancegewinnen bei hoher Parallelität und geringer Latenz führt. Eingeführt wurden sie mit SQL Server 2014.
🎯 Hauptvorteile: Latch‑Konflikte entfallen komplett, Sperrverhalten optimistisch (keine Blockierketten), native Kompilierung von gespeicherten Prozeduren möglich. Ideale Einsatzbereiche sind Hochlast-OLTP, Session-State, Caching, ETL-Puffer und TempDB-Auslagerungen.

⚠️ Voraussetzungen & wichtige Einschränkungen

KOMPATIBILITÄT
Bevor Sie Memory Optimized Tables einsetzen, sollten Sie die folgenden Rahmenbedingungen prüfen:
  • Edition: Enterprise, Developer oder Standard (ab SQL Server 2016 SP1 ist In-Memory OLTP in allen Editionen enthalten, aber die Größe ist in der Standard Edition auf 32 GB pro Datenbank begrenzt).
  • Speicher: Die aktive Tabelle muss vollständig in den Arbeitsspeicher passen, plus ausreichend Puffer für Checkpoint- und Hintergrundprozesse.
  • Datenbank: Muss eine spezielle MEMORY_OPTIMIZED_DATA-Dateigruppe enthalten.
  • Nicht unterstützte Features: Keine FOREIGN KEY-Constraints (außer selbstreferenzierend?), keine Triggern (nur DDL-Trigger, aber keine DML-Trigger), keine IDENTITY-Spalten mit CACHE (nur NO CACHE), keine Änderung des Schemas mit ALTER TABLE – umfassende Änderungen erfordern ein DROP/CREATE.
  • Datentypen: LOB-Typen wie XML, TEXT, IMAGE, FILESTREAM sind nicht erlaubt. VARCHAR(MAX) etc. sind möglich, aber In-Memory speichert diese in separaten Spalten, die weiterhin auf der Festplatte liegen können – das reduziert den Performancegewinn.
🔧 Wichtigste Einschränkung: Sie können die Tabellendefinition nicht einfach über ALTER TABLE ändern. Stattdessen müssen Sie eine neue Tabelle mit dem gewünschten Schema erstellen, die Daten umkopieren, die alte Tabelle löschen und die neue umbenennen. Planen Sie Änderungen daher sorgfältig.

🔨 Erste Schritte: Dateigruppe und Tabelle anlegen

IMPLEMENTIERUNG
Die Erstellung einer Memory Optimized Table erfordert eine spezielle Dateigruppe mit einem oder mehreren Containern (Verzeichnissen). Diese Container speichern die Checkpoint-Dateien für die Persistenz. Eine vollständige Anlage sieht wie folgt aus:
-- 1. Memory-optimierte Dateigruppe hinzufügen 
ALTER DATABASE InMemoryDemo 
ADD FILEGROUP InMemoryFG CONTAINS MEMORY_OPTIMIZED_DATA; 
 
-- 2. Einen Container (Ordner) hinzufügen 
ALTER DATABASE InMemoryDemo 
ADD FILE (NAME = N'InMemory_Container', FILENAME = N'D:\Data\InMemory_Container') 
TO FILEGROUP InMemoryFG; 
 
-- 3. Memory-Optimized Table erstellen 
CREATE TABLE dbo.SessionState ( 
SessionId UNIQUEIDENTIFIER NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000), 
UserName NVARCHAR(128) NOT NULL, 
LastAccess DATETIME2 NOT NULL, 
Data VARBINARY(8000) 
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); 
 
-- Alternative: Nur Schemadauerhaftigkeit (Verlust bei Neustart) 
CREATE TABLE dbo.CacheTemp ( 
Id INT PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 10000), 
Value NVARCHAR(255) 
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_ONLY);
💡 Hinweis zur Dauerhaftigkeit: SCHEMA_AND_DATA bedeutet, dass die Tabelle nach einem Neustart wiederhergestellt wird (aus Checkpoint- und Transaktionslog-Dateien). SCHEMA_ONLY ist ähnlich wie #tempdb – die Daten gehen bei einem Neustart verloren, aber das Schema bleibt. Letzteres eignet sich für Caches oder Session-Daten, die nicht über einen Serverneustart hinaus benötigt werden.

🗂️ Indizes – HASH und NONCLUSTERED im Vergleich

INDEX DESIGN
Memory Optimized Tables unterstützen zwei Index-Typen:
  • HASH Index: Ideal für exakte Gleichheitssuchen (WHERE Column = @Value). Die Bucket-Anzahl muss sorgfältig gewählt werden – zu wenige Buckets führen zu Konflikten, zu viele verschwenden Speicher.
  • NONCLUSTERED Index (B-Baum-ähnlich): Unterstützt Bereichssuchen (WHERE Column BETWEEN ...), Sortierung und teilweise Prädikate. Er ist speicher- und wartungsintensiver als ein HASH Index.
Regel: Verwenden Sie HASH Indizes ausschließlich für exakte Punktzugriffe. Bei allen anderen Abfragen (besonders mit ORDER BY, >, <, LIKE) ist ein NONCLUSTERED Index die bessere Wahl.
-- HASH Index für Primärschlüssel (Punktzugriff) 
CREATE TABLE dbo.ShoppingCart ( 
CartId INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000), 
UserId INT NOT NULL, 
CreatedDate DATETIME2 NOT NULL 
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); 
 
-- NONCLUSTERED Index für Bereichszugriffe 
CREATE NONCLUSTERED INDEX ix_Cart_UserId ON dbo.ShoppingCart (UserId); 
CREATE NONCLUSTERED INDEX ix_Cart_CreatedDate ON dbo.ShoppingCart (CreatedDate); 
 
-- Empfehlung: Bucket-Count = 2x oder 3x geschätzte eindeutige Schlüssel 
ALTER TABLE dbo.ShoppingCart 
ADD INDEX [IX_HASH_CartId] NONCLUSTERED HASH (CartId) WITH (BUCKET_COUNT = 2000000);
📌 Bucket-Count Tipp: Eine zu niedrige Bucket-Anzahl führt zu Hash-Konflikten (mehrere Werte pro Bucket) und verschlechtert die Lese- und Schreibperformance. Verwenden Sie (BUCKET_COUNT = 2 * erwartete_Anzahl_eindeutiger_Werte). Nachträgliches Ändern der Bucket-Anzahl ist nur durch DROP/CREATE möglich.

⚙️ Native Kompilierung – Das Turbo-Feature

PERFORMANCE
Nativ kompilierte gespeicherte Prozeduren (Natively Compiled Stored Procedures) werden in eine DLL kompiliert und direkt vom Prozessor ausgeführt – ohne Interpretation, ohne Latchs, mit minimalem Overhead. Sie sind der Schlüssel zu extrem hohen Transaktionsraten (Hunderttausende Batches pro Sekunde). Solche Prozeduren arbeiten ausschließlich auf Memory Optimized Tables und müssen in einer spezierten Syntax verfasst werden.
CREATE PROCEDURE dbo.InsertOrder 
@OrderId INT, 
@CustomerId INT, 
@OrderDate DATETIME2 
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER 
AS BEGIN ATOMIC WITH ( 
TRANSACTION ISOLATION LEVEL = SNAPSHOT, 
LANGUAGE = N'Deutsch' 
) 
INSERT INTO dbo.MemOrders (OrderId, CustomerId, OrderDate) 
VALUES (@OrderId, @CustomerId, @OrderDate); 
 
UPDATE dbo.MemCustomers 
SET LastOrderDate = @OrderDate 
WHERE CustomerId = @CustomerId; 
END; 
 
-- Ausführung wie gewohnt 
EXEC dbo.InsertOrder 1001, 42, GETUTCDATE();
💡 Einschränkungen: Nativ kompilierte Prozeduren unterstützen nur eine Teilmenge von T-SQL (z. B. keine CURSOR, keine temporären Tabellen, keine dynamischen Abfragen). Sie sind jedoch extrem schnell und sollten für alle kritischen Pfade verwendet werden, die auf Memory Optimized Tables zugreifen.

📊 Überwachung – Wie viel Speicher belegen meine In-Memory Tabellen?

MONITORING
SQL Server stellt mehrere DMVs bereit, um die Größe und Auslastung von Memory Optimized Tables zu überwachen. Die wichtigsten sind:
  • sys.dm_db_xtp_table_memory_stats – Speicher pro Tabelle (Daten + Indizes)
  • sys.dm_db_xtp_hash_index_stats – Informationen zu Hash-Konflikten (chain_length, empty_buckets)
  • sys.dm_db_xtp_checkpoint_stats – Status der Persistenz
  • sys.dm_db_xtp_transactions – Aktive Transaktionen auf In-Memory Objekten
-- Speicherverbrauch pro Memory Optimized Table 
SELECT  
OBJECT_NAME(object_id) AS TableName, 
SUM(memory_allocated_for_table_kb) AS AllocatedKB, 
SUM(memory_used_by_table_kb) AS UsedKB 
FROM sys.dm_db_xtp_table_memory_stats 
GROUP BY object_id 
ORDER BY AllocatedKB DESC; 
 
-- Prüfung auf Hash-Konflikte (durchschnittliche Kettenlänge > 1 = zu wenig Buckets) 
SELECT  
OBJECT_NAME(h.object_id) AS TableName, 
i.name AS IndexName, 
h.avg_chain_length, 
h.total_bucket_count, 
h.empty_bucket_count 
FROM sys.dm_db_xtp_hash_index_stats h 
JOIN sys.indexes i ON h.object_id = i.object_id AND h.index_id = i.index_id 
WHERE h.avg_chain_length > 1 
ORDER BY h.avg_chain_length DESC;
🔍 Interpretation: Eine durchschnittliche Kettenlänge (avg_chain_length) von 1,0 bis 1,5 ist optimal. Werte über 2 deuten auf zu wenige Buckets hin. Dann sollten Sie den Index neu erstellen (DROP/CREATE) mit einer höheren BUCKET_COUNT.

🔄 Migration – Schritt für Schritt

UMSETZUNG
Die Umstellung einer bestehenden, diskbasierten Tabelle auf eine Memory Optimized Table erfordert einen geplanten Prozess, da ein direktes ALTER TABLE ... SET MEMORY_OPTIMIZED = ON nicht existiert. Empfohlenes Vorgehen:
  • Analyse: Prüfen Sie Datentypen, Indizes und Constraints (FOREIGN KEYS sind nicht erlaubt). Entfernen Sie nicht unterstützte Features.
  • Neue Tabelle erstellen: Erzeugen Sie eine Memory Optimized Table mit dem gewünschten Schema, HASH/NONCLUSTERED Indizes.
  • Daten kopieren: In einer Transaktion mit INSERT INTO ... SELECT FROM.
  • Anwendungen umstellen: Lassen Sie Ihre Anwendungen auf die neue Tabelle zeigen.
  • Alte Tabelle löschen: Nach erfolgreichem Test.
-- Schritt 1: Neue Memory-Optimized Tabelle erstellen 
CREATE TABLE dbo.NewOrders_Mem ( 
OrderId INT PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 500000), 
CustomerId INT NOT NULL, 
OrderDate DATETIME2 NOT NULL, 
INDEX ix_Customer NONCLUSTERED (CustomerId) 
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA); 
 
-- Schritt 2: Daten kopieren (ggf. in Batches, um Blockaden zu vermeiden) 
INSERT INTO dbo.NewOrders_Mem (OrderId, CustomerId, OrderDate) 
SELECT OrderId, CustomerId, OrderDate 
FROM dbo.Orders_Old 
WHERE OrderDate >= '2024-01-01'; 
 
-- Schritt 3: Umbenennung (nach Test) 
EXEC sp_rename 'dbo.Orders_Old', 'Orders_Old_Bak'; 
EXEC sp_rename 'dbo.NewOrders_Mem', 'Orders';
🚨 Wichtig: Testen Sie die Migration zuerst in einer nicht-produktiven Umgebung. Überprüfen Sie insbesondere die Latenz unter Last – Memory Optimized Tables können bei sehr großen Scan-Operationen (vollständige Tabellenscans) nicht schneller sein als traditionelle Tabellen, da Daten zwar im Speicher sind, aber die MVCC-Architektur Lesevorgänge etwas aufwendiger machen kann.

✅ Best Practices & Checkliste für In-Memory OLTP

BEST PRACTICES
  • Bucket-Count richtig wählen: 2- bis 4-fache der erwarteten eindeutigen Schlüsselwerte. Keine perfekte Potenz erzwingen.
  • Indexwahl prüfen: Punktzugriffe → HASH Index; Bereichszugriffe/Sortierung → NONCLUSTERED Index.
  • Transaktionen kurz halten: Lange Transaktionen können die alte Versionen im Speicher halten und den Speicherverbrauch erhöhen.
  • Speicher ausreichend dimensionieren: Planen Sie 20–30 % zusätzlichen Speicher für Zeilenversionierung (MVCC) ein.
  • Backup und Recovery testen: Memory Optimized Tables sind in normalen Backups enthalten – testen Sie die Wiederherstellung.
  • Monitoring einrichten: Beobachten Sie sys.dm_xtp_* DMVs regelmäßig.
  • Nativ kompilierte Prozeduren bevorzugen: Für alle kritischen Pfade – sie sind um Größenordnungen schneller als interpretierte Prozeduren.
  • Datenmigration planen: Schemänderungen sind aufwendig – entwerfen Sie Tabellen möglichst stabil.
📌 Zusatztipp: In SQL Server 2022 und Azure SQL Database können Memory Optimized Tables auch mit Always On Availability Groups und intelligentem Query Processing betrieben werden. Azure SQL Database bietet zudem eine vollständig verwaltete In-Memory OLTP-Erfahrung ohne manuelle Dateigruppen.

⚠️ Häufige Fehler – Was Sie unbedingt vermeiden sollten

IRRTÜMER
  • ❌ „Ich kann jetzt alle Tabellen umstellen“ – Falsch. Memory Optimized Tables sind kein Allheilmittel. Sie lohnen sich bei hohen Transaktionsraten, kurzen Schreibzugriffen und Punktzugriffen. Vollständige Scans über große Tabellen sind oft sogar langsamer als auf einer traditionellen Tabelle mit gutem Pufferpool.
  • ❌ „HASH Index ist immer besser“ – Nein. Bei Bereichssuchen ist ein NONCLUSTERED Index überlegen. HASH bringt nur bei exakter Gleichheit Vorteile.
  • ❌ „Bucket-Count egal, SQL Server passt an“ – Nein, der Bucket-Count ist fix. Falsche Wahl führt zu Performance-Einbußen.
  • ❌ „Keine Persistenz nötig – einfach SCHEMA_ONLY“ – Das ist akzeptabel für Caches, aber bei einem ungeplanten Neustart sind alle Daten weg. Planen Sie entsprechend.
  • ❌ „Kann einfach ALTER TABLE ausführen“ – Nicht möglich. Änderungen erfordern DROP/CREATE oder die Verwendung eines Offline-Migrationsskripts.
🚀 Letzter Hinweis: Testen Sie die Performance mit echten Lastprofilen. Oft reicht bereits eine Optimierung der Indizes oder eine bessere Partitionierung auf traditionellen Tabellen. In-Memory OLTP ist ein mächtiges Werkzeug, aber kein Ersatz für gutes Datenbankdesign.