What is In-Memory OLTP?
In-Memory OLTP (Hekaton) is SQL Server's technology for storing tables entirely in RAM instead of on disk. This eliminates disk I/O and enables:
- Sub-millisecond latency: Queries and transactions measured in microseconds, not milliseconds
- High throughput: Thousands of transactions per second from a single server
- Lock-free concurrency: Optimistic locking instead of blocking
Introduced in SQL Server 2014, it's been refined through 2016, 2017, 2019, and beyond. But it's not a magic bullet — it has tradeoffs.
How It Works
The Architecture
Memory-optimized tables live entirely in RAM:
Traditional Table Memory-Optimized Table
├─ Buffer Pool (RAM) ├─ In-Memory Storage
├─ Disk (SSD/HDD) └─ Transaction Log (disk for durability)
└─ Transaction Log
Access path:
1. Request arrives
2. Check buffer pool (cache hit/miss)
3. If miss, read from disk
4. Execute query
5. Write to log
With memory-optimized:
1. Request arrives
2. Read from RAM (always in memory)
3. Execute query
4. Write to log
No disk reads except on startup/recovery!
Durability: How Data Survives Server Crashes
Even though data lives in RAM, it's durable. SQL Server writes transaction logs to disk (checkpoint files):
- Data file: Snapshot of table state (checkpoint)
- Delta file: Incremental updates since last checkpoint
- Transaction log: Every change (like traditional tables)
On server restart, SQL Server rebuilds the table in RAM from these files. The process is fast (seconds, not hours) but not instant.
Creating Memory-Optimized Tables
Basic Syntax
-- Step 1: Create a filegroup for memory-optimized data
ALTER DATABASE MyDatabase
ADD FILEGROUP MemOptFileGroup CONTAINS MEMORY_OPTIMIZED_DATA;
ALTER DATABASE MyDatabase
ADD FILE (
NAME = MemOptData,
FILENAME = 'C:\SQLData\MemOptData'
) TO FILEGROUP MemOptFileGroup;
-- Step 2: Create memory-optimized table
CREATE TABLE OrdersOptimized (
OrderID INT NOT NULL PRIMARY KEY NONCLUSTERED,
CustomerID INT NOT NULL,
OrderDate DATETIME2 NOT NULL,
Amount DECIMAL(10,2)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
-- Durability options:
-- SCHEMA_AND_DATA: Durable (default)
-- SCHEMA_ONLY: Data lost on restart (for temporary working sets)
Indexing Memory-Optimized Tables
Memory-optimized tables support two index types:
- HASH: For equality searches (very fast)
- RANGE: For range queries (>/<, between)
CREATE TABLE OrdersOptimized (
OrderID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 100000),
CustomerID INT NOT NULL,
OrderDate DATETIME2 NOT NULL,
INDEX idx_customer HASH (CustomerID) WITH (BUCKET_COUNT = 100000),
INDEX idx_date RANGE (OrderDate)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
Limitations: The Catch
Significant Restrictions
Memory-optimized tables have strict limitations (especially in earlier versions, relaxed in SQL Server 2019+):
| Feature | Traditional Table | Memory-Optimized Table |
|---|---|---|
| Data Types | All supported | Limited (no TEXT, BLOB; VARCHAR max 8000 bytes) |
| Triggers | AFTER/INSTEAD OF | AFTER only, limited functionality |
| Foreign Keys | Yes | Partial support |
| Constraints | All types | PRIMARY KEY, UNIQUE only |
| Transactions | ACID with locking | ACID with optimistic concurrency |
| Schema Changes | Online (mostly) | Limited (often requires table recreation) |
No Blocking, But Conflicts
Memory-optimized tables use optimistic concurrency instead of locking:
- Multiple threads read/write without blocking
- On commit, SQL Server detects conflicts
- If conflict detected, transaction retries (automatic)
Result: No deadlocks, but transient failures possible if high contention.
Performance: The Reality
Typical Gains
Performance depends on your workload:
- High-frequency OLTP (thousands of writes/second): 10-100x faster
- Point lookups (single row by ID): 5-20x faster
- Complex analytics (full table scans): 1-2x faster (RAM is fast, but not magic)
- Mixed workloads: 2-5x faster
Real Example: Trading System
-- Traditional table
INSERT INTO Trades (TradeID, SecurityID, Quantity, Price)
VALUES (@ID, @Security, @Qty, @Price);
-- Time: 5-10 ms per insert (disk I/O)
-- Throughput: 100-200 inserts/second
-- Memory-optimized table
INSERT INTO TradesOptimized (TradeID, SecurityID, Quantity, Price)
VALUES (@ID, @Security, @Qty, @Price);
-- Time: 0.05-0.1 ms per insert (no disk I/O)
-- Throughput: 10,000+ inserts/second
-- 50-100x faster!
When to Use Memory-Optimized Tables
Good Candidates
- High-frequency transactions: Trading systems, real-time analytics, click tracking
- Session tables: User sessions, shopping carts (temporary, small)
- Event queues: Message processing, event logs (fast insert/consume)
- Cache tables: Materialized views, reference data (fits in RAM)
- Latency-sensitive operations: Where milliseconds matter
Poor Candidates
- Large tables: If data is larger than available RAM, memory-optimized won't help
- Complex queries: Analytical queries with joins, GROUP BY (limited optimization)
- Unpredictable sizes: If table grows unbounded, you'll run out of memory
- Data warehouse workloads: Memory-optimized is for OLTP, not OLAP
- Tables requiring extensive schema changes: ALTER is limited
Common Pitfalls
Pitfall 1: Ignoring Memory Requirements
-- Table size calculation:
-- Each row uses: (column sizes) + 24 bytes overhead + index overhead
-- For 1 million rows with 100 bytes/row:
-- Minimum RAM: 1,000,000 * (100 + 24) + index overhead ≈ 150 MB
-- But SQL Server reserves 2x for GC overhead and concurrency:
-- Actual RAM consumed: ~300 MB
-- If you have multiple memory-optimized tables and other workloads,
-- RAM can fill up quickly. Plan accordingly.
Pitfall 2: Retrying Logic Missing
Memory-optimized tables use optimistic concurrency. Under contention, transactions can fail:
-- Without retry logic, transactions fail occasionally
INSERT INTO OrdersOptimized VALUES (...); -- May fail with conflict
-- Correct: Implement retry logic
DECLARE @RetryCount INT = 0;
WHILE @RetryCount < 5
BEGIN
BEGIN TRY
INSERT INTO OrdersOptimized VALUES (...);
BREAK; -- Success
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 41302 -- Conflict detected
SET @RetryCount = @RetryCount + 1;
ELSE
THROW;
END CATCH;
END;
Pitfall 3: Not Monitoring Memory Usage
If memory-optimized tables consume all server RAM, other workloads suffer:
-- Monitor memory consumption
SELECT
object_name,
memory_allocated_for_table_kb,
memory_used_by_table_kb
FROM sys.dm_db_xtp_table_memory_stats
ORDER BY memory_allocated_for_table_kb DESC;
Migration Strategy
Hybrid Approach
You don't need to migrate entire tables. Run hot data in memory-optimized, cold data in traditional tables:
-- Memory-optimized: Recent orders (last 7 days)
CREATE TABLE OrdersHot (...)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
-- Traditional: Archive (older than 7 days)
CREATE TABLE OrdersArchive (...);
-- Nightly job moves old orders from Hot to Archive
DELETE FROM OrdersHot WHERE OrderDate < DATEADD(DAY, -7, GETDATE());
INSERT INTO OrdersArchive SELECT * FROM OrdersHot WHERE OrderDate < ...;
Steps to Migrate
- Identify hotspots (high contention, latency-critical tables)
- Test memory-optimized version on staging with production-like load
- Implement retry logic in application code
- Monitor memory consumption during pilot
- Gradually migrate, measuring performance gains
- Keep traditional tables for compatibility if needed
Monitoring Memory-Optimized Tables
-- Check memory usage
SELECT
OBJECT_NAME(object_id) AS TableName,
memory_allocated_for_table_kb / 1024 AS AllocatedMB,
memory_used_by_table_kb / 1024 AS UsedMB
FROM sys.dm_db_xtp_table_memory_stats
ORDER BY memory_used_by_table_kb DESC;
-- Check transaction conflicts
SELECT
object_name,
total_access_count,
total_conflict_count,
CAST(100.0 * total_conflict_count / NULLIF(total_access_count, 0) AS DECIMAL(5,2)) AS conflict_pct
FROM sys.dm_db_xtp_nonclustered_index_stats
WHERE total_conflict_count > 0;
The Verdict
Memory-optimized tables are powerful for OLTP workloads with extreme performance requirements. But they're not a universal solution — they have strict limitations and require application-level retry logic. Use them surgically for hotspots, not wholesale.
Consider memory-optimized tables if:
- Latency is critical (sub-millisecond)
- Throughput is high (thousands of ops/second)
- Data fits in RAM
- You can implement retry logic
- Your schema is stable
Skip memory-optimized if:
- Table is large (bigger than available RAM)
- Performance is already acceptable
- Schema changes frequently
- You need complex transactions