Business Professionals
Power BI | Power Pivot | Power Query | DAX
Cloud Flows | RPA | AI Builder | Copilot
60+ Formulas | Data Stories | Advanced Reporting & Modeling
VB Programming | Report Automation |
MS-Office Automation
Techno-Business Professionals
Power BI | Power Query | Advanced DAX | SQL - Query &
Programming
Microsoft Fabric | Power BI | Power Query | Advanced DAX |
SQL - Query & Programming
Power BI | Power Apps | Power Automate | Copilot Studio | Power Pages | Dataverse
Microsoft Power Apps | Microsoft Power Automate
Power BI | Adv. DAX | SQL (Query & Programming) |
VBA | Python | Web Scrapping | API Integration
Power BI | Power Apps | Power Automate |
SQL (Query & Programming)
Power BI | Adv. DAX | Power Apps | Power Automate |
SQL (Query & Programming) | VBA | Python | Web Scrapping | API Integration
Power Apps | Power Automate | SQL | VBA | Python |
Web Scraping | RPA | API Integration
Technology Professionals
Power BI | DAX | SQL | ETL with SSIS | SSAS | VBA | Python
Power BI | SQL | Azure Data Lake | Synapse Analytics |
Data Factory | Databricks | Power Apps | Power Automate |
Azure Analysis Services
Microsoft Fabric | Power BI | SQL | Lakehouse |
Data Factory (Pipelines) | Dataflows Gen2 | KQL | Delta Tables | Power Apps | Power Automate
Power BI | Power Apps | Power Automate | SQL | VBA | Python | API Integration
Power BI | Advanced DAX | Databricks | SQL | Lakehouse Architecture
Business Professionals
Power BI | Power Pivot | Power Query | DAX
Cloud Flows | RPA | AI Builder | Copilot
60+ Formulas | Data Stories | Advanced Reporting & Modeling
VB Programming | Report Automation |
MS-Office Automation
Techno-Business Professionals
Power BI | Power Query | Advanced DAX | SQL - Query &
Programming
Microsoft Fabric | Power BI | Power Query | Advanced DAX |
SQL - Query & Programming
Power BI | Power Apps | Power Automate | Copilot Studio | Power Pages | Dataverse
Microsoft Power Apps | Microsoft Power Automate
Power BI | Adv. DAX | SQL (Query & Programming) |
VBA | Web Scrapping | API Integration
Power BI | Power Apps | Power Automate |
SQL (Query & Programming)
Power BI | Adv. DAX | Power Apps | Power Automate |
SQL (Query & Programming) | VBA | Web Scrapping | API Integration
Power Apps | Power Automate | SQL | VBA |
Web Scraping | RPA | API Integration
Technology Professionals
Power BI | DAX | SQL | ETL with SSIS | SSAS | VBA
Power BI | SQL | Azure Data Lake | Synapse Analytics |
Data Factory | Azure Analysis Services
Microsoft Fabric | Power BI | SQL | Lakehouse |
Data Factory (Pipelines) | Dataflows Gen2 | KQL | Delta Tables
Power BI | Power Apps | Power Automate | SQL | VBA | API Integration
Power BI | Advanced DAX | Databricks | SQL | Lakehouse Architecture
If your SQL Server database serves both transactional users and reporting tools, index design becomes a balancing act. This article walks through practical patterns and anti-patterns for designing MS-SQL indexes for mixed OLTP and reporting workloads, with concrete examples you can adapt in production. If you’re pushing SQL data into Power BI, you’ll get even more value by combining these ideas with solid SQL-powered reporting models.
Mixed workloads typically have:
OLTP characteristics
Reporting characteristics
Indexing for one can easily hurt the other. The goal is not a perfect compromise, but a deliberate split:
Start by explicitly classifying indexes:
Transaction-focused indexes
Reporting-focused indexes
Assume a typical Orders table:
CREATE TABLE dbo.Orders
(
OrderID INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
CustomerID INT NOT NULL,
OrderDate DATETIME2(0) NOT NULL,
Status VARCHAR(20) NOT NULL,
TotalAmount DECIMAL(18,2) NOT NULL,
CreatedAt DATETIME2(0) NOT NULL,
ModifiedAt DATETIME2(0) NULL
);
Transaction index (OLTP) – fast lookup by OrderID and CustomerID:
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_OrderID
ON dbo.Orders (CustomerID, OrderID);
Reporting index – common reporting query:
SELECT CustomerID,
YEAR(OrderDate) AS OrderYear,
SUM(TotalAmount) AS TotalSales
FROM dbo.Orders
WHERE OrderDate >= DATEADD(YEAR, -1, SYSUTCDATETIME())
GROUP BY CustomerID, YEAR(OrderDate);
Matching index:
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_CustomerID
ON dbo.Orders (OrderDate, CustomerID)
INCLUDE (TotalAmount);
OrderDate first: supports range filterCustomerID second: supports groupingTotalAmount as INCLUDE: avoids key lookupsPattern: create explicitly named reporting indexes and document their target queries. If you can’t name the query, don’t create the index.
Covering indexes (using INCLUDE) are ideal for mixed workloads when:
Dashboard query:
SELECT o.CustomerID,
c.CustomerName,
o.Status,
o.OrderDate,
o.TotalAmount
FROM dbo.Orders o
JOIN dbo.Customers c ON c.CustomerID = o.CustomerID
WHERE o.Status = 'Completed'
AND o.OrderDate >= DATEADD(DAY, -30, SYSUTCDATETIME());
Possible index:
CREATE NONCLUSTERED INDEX IX_Orders_Status_OrderDate
ON dbo.Orders (Status, OrderDate)
INCLUDE (CustomerID, TotalAmount);
This reduces IO for the reporting query but adds write overhead on Orders. Before adding it, check:
Status and OrderDate.Orders.Pattern: cover your top N reporting queries (the ones that matter to users) and leave the rest to scan/seek combinations.
Clustered index choice is critical for mixed workloads.
Monotonically increasing keys
Business keys that match most joins
CustomerID, clustering on it can be beneficial.For a high-volume OrderLines table:
CREATE TABLE dbo.OrderLines
(
OrderLineID INT IDENTITY(1,1) NOT NULL,
OrderID INT NOT NULL,
ProductID INT NOT NULL,
Quantity INT NOT NULL,
UnitPrice DECIMAL(18,2) NOT NULL,
LineTotal AS Quantity * UnitPrice PERSISTED
);
CREATE CLUSTERED INDEX CX_OrderLines_OrderLineID
ON dbo.OrderLines (OrderLineID);
Then add nonclustered indexes for reporting:
CREATE NONCLUSTERED INDEX IX_OrderLines_OrderID_ProductID
ON dbo.OrderLines (OrderID, ProductID)
INCLUDE (Quantity, UnitPrice, LineTotal);
Pattern: choose a clustered index that minimises fragmentation and supports the most critical access path. Use nonclustered indexes for reporting needs.
Filtered indexes can give reporting queries a big boost without indexing the entire table.
Status = 'Active').Reporting query:
SELECT CustomerID,
COUNT(*) AS RecentOrders
FROM dbo.Orders
WHERE OrderDate >= DATEADD(DAY, -90, SYSUTCDATETIME())
GROUP BY CustomerID;
Filtered index:
CREATE NONCLUSTERED INDEX IX_Orders_Recent_OrderDate_CustomerID
ON dbo.Orders (OrderDate, CustomerID)
WHERE OrderDate >= DATEADD(DAY, -90, SYSUTCDATETIME());
Notes:
GETDATE() inside the definition; use a static boundary and adjust periodically if needed).Pattern: use filtered indexes for hot reporting slices instead of indexing the full history.
Reporting queries often aggregate over large ranges. A good index can:
Query:
SELECT CAST(OrderDate AS DATE) AS OrderDay,
SUM(TotalAmount) AS TotalSales
FROM dbo.Orders
GROUP BY CAST(OrderDate AS DATE);
Helpful index:
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_TotalAmount
ON dbo.Orders (OrderDate)
INCLUDE (TotalAmount);
If you regularly aggregate by CustomerID and day:
SELECT CustomerID,
CAST(OrderDate AS DATE) AS OrderDay,
SUM(TotalAmount) AS TotalSales
FROM dbo.Orders
GROUP BY CustomerID, CAST(OrderDate AS DATE);
Then consider:
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_OrderDate
ON dbo.Orders (CustomerID, OrderDate)
INCLUDE (TotalAmount);
Pattern: align index key order with GROUP BY and WHERE clauses, not just the columns involved.
Common symptoms:
Why it hurts:
Use DMVs to check usage:
SELECT DB_NAME(database_id) AS DatabaseName,
OBJECT_NAME(i.object_id) AS TableName,
i.name AS IndexName,
s.user_seeks,
s.user_scans,
s.user_lookups,
s.user_updates
FROM sys.dm_db_index_usage_stats s
JOIN sys.indexes i
ON s.object_id = i.object_id
AND s.index_id = i.index_id
WHERE s.database_id = DB_ID();
Look for indexes with high user_updates and very low user_seeks/scans.
A reporting index that speeds up one dashboard but slows down all OLTP writes is a bad trade.
Watch out for:
Anti-pattern: adding a new index for every slow report instead of redesigning the report or consolidating indexes.
Indexes are often created with the right columns but wrong order.
Typical mistakes:
Status) with a highly selective column second.Query:
SELECT *
FROM dbo.Orders
WHERE CustomerID = @CustomerID
AND OrderDate >= @FromDate
AND OrderDate < @ToDate;
Bad index:
CREATE NONCLUSTERED INDEX IX_Orders_Status_CustomerID_OrderDate
ON dbo.Orders (Status, CustomerID, OrderDate);
Better index:
CREATE NONCLUSTERED INDEX IX_Orders_CustomerID_OrderDate
ON dbo.Orders (CustomerID, OrderDate);
Anti-pattern: designing indexes around “business meaning” of columns instead of how queries actually filter and join.
A simple workflow that works well in real projects:
Identify critical OLTP queries
Identify top reporting queries
For each query, map to an index
Validate with actual execution plans
Monitor and iterate
Pick one heavily used reporting query today, capture its actual execution plan, and design exactly one index that supports both that query and at least one OLTP query on the same table. Deploy it, measure the impact on both read and write performance, and only then decide if you truly need more indexes for that table.
MS-SQL
New
Next Batches Now Live
Power BI
SQL
Power Apps
Power Automate
Microsoft Fabrics
Azure Data Engineering