Skip to content
Back to Blog
1 min read

Intelligent Query Processing in Azure SQL: Automatic Performance Optimization

I wrote “Intelligent Query Processing in Azure SQL: Automatic Performance Optimization” to share practical, production-minded guidance on this topic.

IQP Feature Overview

The IQP family includes:

  • Adaptive Query Processing (Adaptive Joins, Memory Grant Feedback)
  • Table Variable Deferred Compilation
  • Batch Mode on Rowstore
  • Scalar UDF Inlining
  • Approximate Query Processing

Enabling IQP Features

-- Enable all IQP features by setting compatibility level
ALTER DATABASE MyDatabase SET COMPATIBILITY_LEVEL = 150;

-- Verify compatibility level
SELECT name, compatibility_level
FROM sys.databases
WHERE name = 'MyDatabase';

Adaptive Joins in Action

Adaptive joins choose between hash and nested loop joins at runtime based on actual row counts:

-- Query that benefits from adaptive joins
SELECT
    o.OrderID,
    o.OrderDate,
    c.CustomerName,
    p.ProductName
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
INNER JOIN Products p ON o.ProductID = p.ProductID
WHERE o.OrderDate >= '2021-01-01';

-- Check if adaptive join was used
SELECT
    qp.query_plan,
    qs.execution_count,
    qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qp.query_plan.exist('//RelOp[@PhysicalOp="Adaptive Join"]') = 1;

Memory Grant Feedback

Memory grant feedback adjusts memory allocations based on actual usage:

-- Query with potential memory grant issues
SELECT
    CustomerID,
    STRING_AGG(OrderID, ',') AS Orders,
    SUM(TotalAmount) AS TotalSpent
FROM Orders
WHERE OrderDate >= DATEADD(YEAR, -1, GETDATE())
GROUP BY CustomerID
ORDER BY TotalSpent DESC;

-- Monitor memory grant feedback
SELECT
    q.query_id,
    p.plan_id,
    p.query_plan_hash,
    rs.avg_query_max_used_memory,
    rs.last_query_max_used_memory,
    p.is_memory_grant_feedback_adjusted
FROM sys.query_store_query q
JOIN sys.query_store_plan p ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
WHERE p.is_memory_grant_feedback_adjusted = 1;

Table Variable Deferred Compilation

Prior to IQP, table variables always estimated 1 row. Deferred compilation uses actual cardinality:

-- Create procedure using table variable
CREATE OR ALTER PROCEDURE GetOrderSummary
    @StartDate DATE,
    @EndDate DATE
AS
BEGIN
    DECLARE @TempOrders TABLE (
        OrderID INT,
        CustomerID INT,
        TotalAmount DECIMAL(18,2)
    );

    -- Insert with actual row count now used for cardinality
    INSERT INTO @TempOrders
    SELECT OrderID, CustomerID, TotalAmount
    FROM Orders
    WHERE OrderDate BETWEEN @StartDate AND @EndDate;

    -- Join now uses accurate row estimate
    SELECT
        t.OrderID,
        c.CustomerName,
        t.TotalAmount
    FROM @TempOrders t
    INNER JOIN Customers c ON t.CustomerID = c.CustomerID;
END;

Batch Mode on Rowstore

Batch mode processing, previously only for columnstore, now works on rowstore tables:

-- Analytical query benefiting from batch mode on rowstore
SELECT
    YEAR(OrderDate) AS OrderYear,
    MONTH(OrderDate) AS OrderMonth,
    CategoryID,
    COUNT(*) AS OrderCount,
    SUM(Quantity) AS TotalQuantity,
    AVG(UnitPrice) AS AvgPrice
FROM Orders o
JOIN OrderDetails od ON o.OrderID = od.OrderID
GROUP BY YEAR(OrderDate), MONTH(OrderDate), CategoryID
ORDER BY OrderYear, OrderMonth;

-- Verify batch mode usage
SELECT
    qs.execution_count,
    qs.total_worker_time,
    qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
WHERE qp.query_plan.exist('//RelOp[@EstimatedExecutionMode="Batch"]') = 1;

Scalar UDF Inlining

Scalar UDFs are inlined into the calling query for better performance:

-- Create an inlineable scalar UDF
CREATE OR ALTER FUNCTION dbo.CalculateDiscount(
    @Price DECIMAL(18,2),
    @Quantity INT
)
RETURNS DECIMAL(18,2)
WITH INLINE = ON
AS
BEGIN
    RETURN CASE
        WHEN @Quantity >= 100 THEN @Price * 0.20
        WHEN @Quantity >= 50 THEN @Price * 0.10
        WHEN @Quantity >= 10 THEN @Price * 0.05
        ELSE 0
    END;
END;

-- Query using the inlined function
SELECT
    ProductID,
    UnitPrice,
    Quantity,
    dbo.CalculateDiscount(UnitPrice, Quantity) AS Discount,
    UnitPrice * Quantity - dbo.CalculateDiscount(UnitPrice, Quantity) AS FinalPrice
FROM OrderDetails;

Approximate Query Processing

For large datasets where approximate results are acceptable:

-- Use APPROX_COUNT_DISTINCT for faster cardinality estimation
SELECT
    YEAR(OrderDate) AS OrderYear,
    APPROX_COUNT_DISTINCT(CustomerID) AS ApproxUniqueCustomers
FROM Orders
GROUP BY YEAR(OrderDate);

-- Compare with exact count
SELECT
    YEAR(OrderDate) AS OrderYear,
    COUNT(DISTINCT CustomerID) AS ExactUniqueCustomers
FROM Orders
GROUP BY YEAR(OrderDate);

Monitoring IQP Effectiveness

-- Check IQP feature usage across queries
SELECT
    query_hash,
    COUNT(*) AS execution_count,
    AVG(total_elapsed_time) AS avg_duration,
    SUM(CASE WHEN adaptive_join_applied = 1 THEN 1 ELSE 0 END) AS adaptive_join_count,
    SUM(CASE WHEN memory_grant_feedback_applied = 1 THEN 1 ELSE 0 END) AS memory_feedback_count
FROM sys.dm_exec_query_stats_history
GROUP BY query_hash
ORDER BY execution_count DESC;

IQP represents a significant step toward self-tuning databases, reducing the manual effort required to optimize query performance while automatically adapting to changing data distributions and workload patterns.\n\n## Takeaways\n\nAdd a concise, personal takeaway and recommended next steps here.\n

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.