Modern SQL Server Features: JSON, Temporal Tables, and In-Memory OLTP

Modern SQL Server Features: JSON, Temporal Tables, and In-Memory OLTP

Introduction

Modern SQL Server provides advanced features for diverse workloads. This guide covers JSON and XML support for semi-structured data, temporal tables for automatic history tracking, in-memory OLTP for extreme performance, columnstore indexes for analytics, and graph databases for relationship modeling.

JSON Support

Storing and Querying JSON

-- Create table with JSON column
CREATE TABLE dbo.Products (
    ProductID INT PRIMARY KEY IDENTITY,
    ProductName NVARCHAR(100),
    Price DECIMAL(10,2),
    Attributes NVARCHAR(MAX)  -- JSON data
);

-- Insert JSON data
INSERT INTO dbo.Products (ProductName, Price, Attributes)
VALUES 
    ('Laptop', 1299.99, '{"brand":"Dell","processor":"Intel i7","ram":"16GB","storage":"512GB SSD"}'),
    ('Mouse', 29.99, '{"brand":"Logitech","wireless":true,"dpi":1600}'),
    ('Monitor', 349.99, '{"brand":"Samsung","size":"27 inch","resolution":"2560x1440","refresh_rate":144}');

Extracting JSON Values

-- JSON_VALUE: Extract scalar values
SELECT 
    ProductID,
    ProductName,
    JSON_VALUE(Attributes, '$.brand') AS Brand,
    JSON_VALUE(Attributes, '$.processor') AS Processor,
    JSON_VALUE(Attributes, '$.ram') AS RAM
FROM dbo.Products
WHERE JSON_VALUE(Attributes, '$.brand') = 'Dell';

-- JSON_QUERY: Extract objects or arrays
SELECT 
    ProductID,
    ProductName,
    JSON_QUERY(Attributes, '$') AS FullAttributes
FROM dbo.Products;

Modifying JSON Data

-- JSON_MODIFY: Update JSON properties
UPDATE dbo.Products
SET Attributes = JSON_MODIFY(Attributes, '$.price_updated', CAST(GETDATE() AS VARCHAR(50)))
WHERE ProductID = 1;

-- Add new property
UPDATE dbo.Products
SET Attributes = JSON_MODIFY(Attributes, '$.warranty', '2 years')
WHERE JSON_VALUE(Attributes, '$.brand') = 'Dell';

-- Remove property (set to NULL)
UPDATE dbo.Products
SET Attributes = JSON_MODIFY(Attributes, '$.temp_field', NULL)
WHERE ProductID = 1;

Converting JSON to Relational

-- OPENJSON: Parse JSON into table format
DECLARE @json NVARCHAR(MAX) = N'[
    {"id":1,"name":"John","age":30},
    {"id":2,"name":"Jane","age":25},
    {"id":3,"name":"Bob","age":35}
]';

SELECT *
FROM OPENJSON(@json)
WITH (
    id INT '$.id',
    name NVARCHAR(50) '$.name',
    age INT '$.age'
);

-- Parse nested JSON
DECLARE @nestedJson NVARCHAR(MAX) = N'{
    "customer": {
        "id": 1001,
        "name": "Contoso",
        "address": {
            "street": "123 Main St",
            "city": "Seattle",
            "zip": "98101"
        }
    }
}';

SELECT 
    JSON_VALUE(@nestedJson, '$.customer.id') AS CustomerID,
    JSON_VALUE(@nestedJson, '$.customer.name') AS CustomerName,
    JSON_VALUE(@nestedJson, '$.customer.address.city') AS City;

FOR JSON: Convert to JSON

-- FOR JSON AUTO: Automatic structure
SELECT 
    c.CustomerID,
    c.CustomerName,
    o.OrderID,
    o.OrderDate,
    o.TotalAmount
FROM dbo.Customers c
INNER JOIN dbo.Orders o ON c.CustomerID = o.CustomerID
WHERE c.CustomerID = 1001
FOR JSON AUTO;

-- Result: [{"CustomerID":1001,"CustomerName":"Contoso","o":[{"OrderID":100,"OrderDate":"2025-01-15","TotalAmount":299.99}]}]

-- FOR JSON PATH: Custom structure
SELECT 
    CustomerID AS 'customer.id',
    CustomerName AS 'customer.name',
    Email AS 'customer.email',
    OrderID AS 'orders.id',
    OrderDate AS 'orders.date',
    TotalAmount AS 'orders.total'
FROM dbo.Customers c
INNER JOIN dbo.Orders o ON c.CustomerID = o.CustomerID
WHERE c.CustomerID = 1001
FOR JSON PATH, ROOT('data');

-- Result: {"data":[{"customer":{"id":1001,"name":"Contoso","email":"contact@contoso.com"},"orders":{"id":100,"date":"2025-01-15","total":299.99}}]}

Indexing JSON Data

-- Create computed column for JSON property
ALTER TABLE dbo.Products
ADD Brand AS JSON_VALUE(Attributes, '$.brand') PERSISTED;

-- Create index on computed column
CREATE INDEX IX_Products_Brand ON dbo.Products(Brand);

-- Query uses index
SELECT ProductID, ProductName, Brand
FROM dbo.Products
WHERE Brand = 'Dell';

REST API Integration Example

-- Stored procedure to return JSON for REST API
CREATE PROCEDURE dbo.GetProductsJSON
    @CategoryID INT = NULL
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        ProductID AS id,
        ProductName AS name,
        Price AS price,
        JSON_QUERY(Attributes) AS attributes
    FROM dbo.Products
    WHERE @CategoryID IS NULL OR CategoryID = @CategoryID
    FOR JSON PATH;
END;
GO

-- Call from application
-- HTTP GET /api/products?categoryId=5
-- Returns: [{"id":1,"name":"Laptop","price":1299.99,"attributes":{"brand":"Dell",...}}]

XML Support

Storing and Querying XML

-- Create table with XML column
CREATE TABLE dbo.ProductsXML (
    ProductID INT PRIMARY KEY IDENTITY,
    ProductName NVARCHAR(100),
    ProductData XML
);

-- Insert XML data
INSERT INTO dbo.ProductsXML (ProductName, ProductData)
VALUES 
    ('Laptop', '<Product><Brand>Dell</Brand><Processor>Intel i7</Processor><RAM>16GB</RAM></Product>'),
    ('Mouse', '<Product><Brand>Logitech</Brand><Wireless>true</Wireless><DPI>1600</DPI></Product>');

-- XQuery: Extract values
SELECT 
    ProductID,
    ProductName,
    ProductData.value('(/Product/Brand)[1]', 'NVARCHAR(50)') AS Brand,
    ProductData.value('(/Product/Processor)[1]', 'NVARCHAR(50)') AS Processor
FROM dbo.ProductsXML;

-- XQuery: Check existence
SELECT *
FROM dbo.ProductsXML
WHERE ProductData.exist('/Product/Wireless[. = "true"]') = 1;

FOR XML

-- FOR XML RAW
SELECT TOP 3 ProductID, ProductName, Price
FROM dbo.Products
FOR XML RAW('Product'), ROOT('Products'), ELEMENTS;

-- Result:
-- <Products>
--   <Product><ProductID>1</ProductID><ProductName>Laptop</ProductName><Price>1299.99</Price></Product>
--   <Product><ProductID>2</ProductID><ProductName>Mouse</ProductName><Price>29.99</Price></Product>
-- </Products>

-- FOR XML PATH (custom structure)
SELECT 
    CustomerID AS '@id',
    CustomerName AS 'Name',
    Email AS 'Contact/Email',
    Phone AS 'Contact/Phone'
FROM dbo.Customers
WHERE CustomerID = 1001
FOR XML PATH('Customer'), ROOT('Customers');

Temporal Tables

Creating System-Versioned Temporal Table

-- Create temporal table
CREATE TABLE dbo.Employees (
    EmployeeID INT PRIMARY KEY NOT NULL,
    FirstName NVARCHAR(50) NOT NULL,
    LastName NVARCHAR(50) NOT NULL,
    Department NVARCHAR(50) NOT NULL,
    Salary DECIMAL(10,2) NOT NULL,
    
    -- Period columns (auto-managed by SQL Server)
    SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL,
    SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL,
    PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.EmployeesHistory));

-- Insert data
INSERT INTO dbo.Employees (EmployeeID, FirstName, LastName, Department, Salary)
VALUES 
    (1, 'John', 'Doe', 'IT', 75000),
    (2, 'Jane', 'Smith', 'Sales', 65000);

-- Update employee (creates history record)
UPDATE dbo.Employees
SET Salary = 80000, Department = 'Engineering'
WHERE EmployeeID = 1;

-- Delete employee (moves to history)
DELETE FROM dbo.Employees
WHERE EmployeeID = 2;

Querying Temporal Data

-- Current data (default)
SELECT * FROM dbo.Employees;

-- All historical versions
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME ALL
ORDER BY EmployeeID, SysStartTime;

-- Point-in-time query (as of specific date)
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME AS OF '2025-01-01 12:00:00'
WHERE EmployeeID = 1;

-- Changes between two dates
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME BETWEEN '2025-01-01' AND '2025-12-31'
WHERE EmployeeID = 1
ORDER BY SysStartTime;

-- Changes from specific time to now
SELECT * FROM dbo.Employees
FOR SYSTEM_TIME FROM '2025-06-01' TO '9999-12-31 23:59:59.9999999'
WHERE EmployeeID = 1;

Audit Trail Example

-- Find who changed salary and when
SELECT 
    EmployeeID,
    FirstName,
    LastName,
    Salary,
    Department,
    SysStartTime AS ChangeDate,
    SysEndTime AS ValidUntil,
    CASE 
        WHEN SysEndTime = '9999-12-31 23:59:59.9999999' THEN 'Current'
        ELSE 'Historical'
    END AS Status
FROM dbo.Employees
FOR SYSTEM_TIME ALL
WHERE EmployeeID = 1
ORDER BY SysStartTime;

-- Salary history report
SELECT 
    e.EmployeeID,
    e.FirstName + ' ' + e.LastName AS FullName,
    e.Salary AS OldSalary,
    LEAD(e.Salary) OVER (PARTITION BY e.EmployeeID ORDER BY e.SysStartTime) AS NewSalary,
    e.SysStartTime AS ChangeDate,
    DATEDIFF(DAY, e.SysStartTime, e.SysEndTime) AS DaysActive
FROM dbo.Employees FOR SYSTEM_TIME ALL e
WHERE e.EmployeeID = 1
ORDER BY e.SysStartTime;

Retention Policy

-- Enable retention policy (keep 6 months of history)
ALTER TABLE dbo.Employees
SET (SYSTEM_VERSIONING = OFF);

ALTER TABLE dbo.EmployeesHistory
SET (DATA_DELETION = ON (FILTER_COLUMN = SysEndTime, RETENTION_PERIOD = 6 MONTHS));

ALTER TABLE dbo.Employees
SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.EmployeesHistory, DATA_CONSISTENCY_CHECK = ON));

In-Memory OLTP

Creating Memory-Optimized Tables

-- Add memory-optimized filegroup (one-time setup)
ALTER DATABASE AdventureWorks
ADD FILEGROUP MemoryOptimized_FG CONTAINS MEMORY_OPTIMIZED_DATA;

ALTER DATABASE AdventureWorks
ADD FILE (
    NAME = 'MemoryOptimized_File',
    FILENAME = 'C:\Data\AdventureWorks_MemoryOptimized'
) TO FILEGROUP MemoryOptimized_FG;

-- Create memory-optimized table
CREATE TABLE dbo.OrdersMemoryOptimized (
    OrderID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 1000000),
    CustomerID INT NOT NULL,
    OrderDate DATETIME2 NOT NULL,
    TotalAmount DECIMAL(10,2) NOT NULL,
    Status NVARCHAR(20) NOT NULL,
    
    INDEX IX_CustomerID NONCLUSTERED (CustomerID),
    INDEX IX_OrderDate NONCLUSTERED (OrderDate)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

-- DURABILITY options:
-- SCHEMA_AND_DATA: Full durability (logged)
-- SCHEMA_ONLY: Data lost on restart (faster)

Natively Compiled Stored Procedures

-- Create natively compiled procedure
CREATE PROCEDURE dbo.InsertOrder_Native
    @CustomerID INT,
    @OrderDate DATETIME2,
    @TotalAmount DECIMAL(10,2),
    @Status NVARCHAR(20)
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
AS
BEGIN ATOMIC WITH (
    TRANSACTION ISOLATION LEVEL = SNAPSHOT,
    LANGUAGE = N'us_english'
)
    INSERT INTO dbo.OrdersMemoryOptimized (OrderID, CustomerID, OrderDate, TotalAmount, Status)
    VALUES (NEXT VALUE FOR dbo.OrderIDSequence, @CustomerID, @OrderDate, @TotalAmount, @Status);
END;
GO

-- Create sequence for OrderID
CREATE SEQUENCE dbo.OrderIDSequence
START WITH 1
INCREMENT BY 1;

Hash vs Range Indexes

-- Hash index: Best for equality searches
-- BUCKET_COUNT should be 1-2x row count
CREATE TABLE dbo.CustomersMemoryOptimized (
    CustomerID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH WITH (BUCKET_COUNT = 100000),
    CustomerName NVARCHAR(100),
    Email NVARCHAR(100)
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

-- Range index: Best for range queries, sorting
CREATE TABLE dbo.ProductsMemoryOptimized (
    ProductID INT NOT NULL PRIMARY KEY NONCLUSTERED,  -- Range index
    ProductName NVARCHAR(100),
    Price DECIMAL(10,2),
    
    INDEX IX_Price NONCLUSTERED (Price)  -- Range queries
)
WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);

Monitoring Memory Usage

-- Memory usage by table
SELECT 
    OBJECT_NAME(object_id) AS TableName,
    memory_allocated_for_table_kb / 1024 AS table_memory_mb,
    memory_allocated_for_indexes_kb / 1024 AS index_memory_mb,
    (memory_allocated_for_table_kb + memory_allocated_for_indexes_kb) / 1024 AS total_memory_mb
FROM sys.dm_db_xtp_table_memory_stats
ORDER BY total_memory_mb DESC;

-- Total memory usage
SELECT 
    SUM(allocated_bytes) / 1048576 AS allocated_mb,
    SUM(used_bytes) / 1048576 AS used_mb
FROM sys.dm_db_xtp_memory_consumers;

Columnstore Indexes

Clustered Columnstore Index

-- Create fact table with clustered columnstore
CREATE TABLE dbo.FactSales (
    OrderID INT NOT NULL,
    ProductID INT NOT NULL,
    CustomerID INT NOT NULL,
    OrderDate DATE NOT NULL,
    Quantity INT NOT NULL,
    UnitPrice DECIMAL(10,2) NOT NULL,
    TotalAmount DECIMAL(10,2) NOT NULL
);

-- Create clustered columnstore index
CREATE CLUSTERED COLUMNSTORE INDEX CCI_FactSales ON dbo.FactSales;

-- Insert data (batch mode execution)
INSERT INTO dbo.FactSales
SELECT 
    OrderID,
    ProductID,
    CustomerID,
    CAST(OrderDate AS DATE),
    Quantity,
    UnitPrice,
    TotalAmount
FROM dbo.Orders o
INNER JOIN dbo.OrderDetails od ON o.OrderID = od.OrderID;

-- Query benefits from batch mode and compression
SELECT 
    YEAR(OrderDate) AS OrderYear,
    MONTH(OrderDate) AS OrderMonth,
    SUM(TotalAmount) AS Revenue,
    COUNT(*) AS OrderCount
FROM dbo.FactSales
WHERE OrderDate >= '2024-01-01'
GROUP BY YEAR(OrderDate), MONTH(OrderDate)
ORDER BY OrderYear, OrderMonth;

Nonclustered Columnstore Index

-- Add columnstore to existing OLTP table
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Orders_Analytics
ON dbo.Orders (OrderDate, CustomerID, TotalAmount, Status)
WHERE OrderDate >= '2024-01-01';  -- Filtered

-- Real-time operational analytics
SELECT 
    CustomerID,
    COUNT(*) AS OrderCount,
    SUM(TotalAmount) AS TotalRevenue
FROM dbo.Orders
WHERE OrderDate >= '2024-01-01'
GROUP BY CustomerID
ORDER BY TotalRevenue DESC;

Columnstore Performance

-- View columnstore segments and compression
SELECT 
    OBJECT_NAME(i.object_id) AS TableName,
    i.name AS IndexName,
    p.partition_number,
    p.rows AS row_count,
    p.data_compression_desc,
    css.segment_id,
    css.column_id,
    css.row_count AS segment_rows,
    css.on_disk_size / 1024 AS on_disk_kb,
    css.on_disk_size * 100.0 / NULLIF(SUM(css.on_disk_size) OVER(PARTITION BY i.object_id), 0) AS pct_of_index
FROM sys.indexes i
INNER JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id
INNER JOIN sys.column_store_segments css ON p.partition_id = css.partition_id
WHERE i.type IN (5, 6)  -- Columnstore indexes
ORDER BY TableName, segment_id, column_id;

Graph Databases

Creating Graph Tables

-- Create node tables
CREATE TABLE dbo.Person (
    PersonID INT PRIMARY KEY,
    FirstName NVARCHAR(50),
    LastName NVARCHAR(50),
    Email NVARCHAR(100)
) AS NODE;

CREATE TABLE dbo.Restaurant (
    RestaurantID INT PRIMARY KEY,
    Name NVARCHAR(100),
    City NVARCHAR(50),
    Rating DECIMAL(2,1)
) AS NODE;

-- Create edge table
CREATE TABLE dbo.Likes AS EDGE;

-- Insert nodes
INSERT INTO dbo.Person (PersonID, FirstName, LastName, Email)
VALUES 
    (1, 'John', 'Doe', 'john@example.com'),
    (2, 'Jane', 'Smith', 'jane@example.com'),
    (3, 'Bob', 'Johnson', 'bob@example.com');

INSERT INTO dbo.Restaurant (RestaurantID, Name, City, Rating)
VALUES 
    (100, 'Pizza Palace', 'Seattle', 4.5),
    (101, 'Burger Barn', 'Seattle', 4.0),
    (102, 'Sushi Studio', 'Portland', 4.8);

-- Insert edges (relationships)
INSERT INTO dbo.Likes ($from_id, $to_id)
VALUES 
    ((SELECT $node_id FROM dbo.Person WHERE PersonID = 1), 
     (SELECT $node_id FROM dbo.Restaurant WHERE RestaurantID = 100)),
    ((SELECT $node_id FROM dbo.Person WHERE PersonID = 1), 
     (SELECT $node_id FROM dbo.Restaurant WHERE RestaurantID = 102)),
    ((SELECT $node_id FROM dbo.Person WHERE PersonID = 2), 
     (SELECT $node_id FROM dbo.Restaurant WHERE RestaurantID = 100));

Querying Graph Data

-- MATCH: Find relationships
SELECT 
    p.FirstName + ' ' + p.LastName AS Person,
    r.Name AS Restaurant,
    r.Rating
FROM dbo.Person p, dbo.Likes, dbo.Restaurant r
WHERE MATCH(p-(Likes)->r)
    AND p.PersonID = 1;

-- Find common likes (friends who like same restaurant)
SELECT 
    p1.FirstName + ' ' + p1.LastName AS Person1,
    p2.FirstName + ' ' + p2.LastName AS Person2,
    r.Name AS CommonRestaurant
FROM dbo.Person p1, dbo.Likes l1, dbo.Restaurant r, dbo.Likes l2, dbo.Person p2
WHERE MATCH(p1-(l1)->r<-(l2)-p2)
    AND p1.PersonID < p2.PersonID;  -- Avoid duplicates

-- Shortest path (requires SQL Server 2019+)
SELECT PersonName, RestaurantName
FROM (
    SELECT 
        p.FirstName + ' ' + p.LastName AS PersonName,
        STRING_AGG(r.Name, ' -> ') WITHIN GROUP (GRAPH PATH) AS RestaurantName,
        LAST_VALUE(r.Name) WITHIN GROUP (GRAPH PATH) AS LastRestaurant
    FROM dbo.Person p, dbo.Likes FOR PATH, dbo.Restaurant r FOR PATH
    WHERE MATCH(SHORTEST_PATH(p(-(Likes)->r)+))
        AND p.PersonID = 1
) AS Results;

Social Network Example

-- Create friendship edge
CREATE TABLE dbo.FriendOf AS EDGE;

-- Insert friendships
INSERT INTO dbo.FriendOf ($from_id, $to_id)
VALUES 
    ((SELECT $node_id FROM dbo.Person WHERE PersonID = 1), 
     (SELECT $node_id FROM dbo.Person WHERE PersonID = 2)),
    ((SELECT $node_id FROM dbo.Person WHERE PersonID = 2), 
     (SELECT $node_id FROM dbo.Person WHERE PersonID = 3));

-- Find friends-of-friends
SELECT 
    p1.FirstName + ' ' + p1.LastName AS Person,
    p3.FirstName + ' ' + p3.LastName AS FriendOfFriend
FROM dbo.Person p1, dbo.FriendOf f1, dbo.Person p2, dbo.FriendOf f2, dbo.Person p3
WHERE MATCH(p1-(f1)->p2-(f2)->p3)
    AND p1.PersonID = 1
    AND p1.PersonID <> p3.PersonID;  -- Exclude self

Key Takeaways

  • JSON support enables semi-structured data storage and REST APIs
  • XML provides enterprise data exchange capabilities
  • Temporal tables automatically track data history for auditing
  • In-memory OLTP delivers extreme performance for high-concurrency workloads
  • Columnstore indexes optimize analytics on large datasets
  • Graph databases model complex relationships efficiently
  • Use computed columns to index JSON/XML properties
  • Choose SCHEMA_AND_DATA durability for mission-critical in-memory tables
  • Columnstore compression can reduce storage by 10x
  • Graph queries simplify relationship traversal

Next Steps

  • Implement JSON APIs for modern applications
  • Enable temporal tables for audit-critical tables
  • Evaluate in-memory OLTP for high-throughput scenarios
  • Add columnstore indexes to fact tables
  • Model relationships with graph tables
  • Monitor memory usage for in-memory objects

Additional Resources


Innovate. Optimize. Scale. Transform.