1. Introduction
Many enterprise systems need fast, real-time analytics on transactional data. Examples include:
Daily sales summaries
Inventory snapshots
Customer activity aggregates
Performance dashboards
Monthly revenue reports
When queries involve large tables, multiple joins, or expensive aggregates, API response time becomes slow. Index tuning sometimes helps, but often it is not enough.
This is where materialized views (also known as indexed views) in SQL Server provide a huge advantage. They precompute and store aggregated results physically, giving near-instant reads.
This article explains how to use SQL Server indexed views for complex aggregates using a practical, step-by-step approach.
2. What is a Materialized View?
A materialized view in SQL Server is simply an indexed view. It stores the result of the view physically on disk, unlike a normal view which is just a SELECT query.
Key properties
Data is precomputed and stored.
Automatically updated when base tables change.
Querying the view is extremely fast.
SQL Server may rewrite queries automatically to use the indexed view.
Materialized views are perfect for slow aggregate queries in OLTP workloads.
3. Example Scenario
Assume you have an Orders table with millions of rows:
CREATE TABLE dbo.Orders (
OrderId BIGINT PRIMARY KEY,
CustomerId INT,
OrderDate DATETIME2,
Amount DECIMAL(18,2),
Status VARCHAR(20)
);
Your application frequently runs this expensive query:
SELECT
CustomerId,
COUNT(*) AS TotalOrders,
SUM(Amount) AS TotalAmount
FROM dbo.Orders
WHERE Status = 'Completed'GROUP BY CustomerId;
This scan can take several seconds when the table becomes large.
A materialized view makes this instant.
4. Requirements for Indexed Views
SQL Server enforces strict rules for creating indexed views:
4.1 Mandatory Requirements
Schema binding
Deterministic expressions
Two-part naming for tables
NO COUNT(*), use COUNT_BIG
NO TOP, DISTINCT, UNION, OUTER JOIN
Grouping allowed but only on explicit columns
4.2 Recommended Settings
Execute these at database level:
ALTER DATABASE YourDB SET ANSI_NULLS ON;
ALTER DATABASE YourDB SET QUOTED_IDENTIFIER ON;
ALTER DATABASE YourDB SET ANSI_PADDING ON;
ALTER DATABASE YourDB SET ANSI_WARNINGS ON;
ALTER DATABASE YourDB SET CONCAT_NULL_YIELDS_NULL ON;
Without this, SQL Server will not allow an indexed view.
5. Creating a Materialized View (Step-by-Step)
Step 1: Create the View
CREATE VIEW dbo.vwCustomerOrderSummary
WITH SCHEMABINDING
ASSELECT
o.CustomerId,
COUNT_BIG(*) AS TotalOrders,
SUM(o.Amount) AS TotalAmount
FROM dbo.Orders AS o
WHERE o.Status = 'Completed'GROUP BY o.CustomerId;
Note the use of:
SCHEMABINDING
COUNT_BIG
Step 2: Create a Unique Clustered Index
CREATE UNIQUE CLUSTERED INDEX IX_vwCustomerOrderSummary
ON dbo.vwCustomerOrderSummary (CustomerId);
This makes the view materialized.
Now SQL Server stores aggregated results physically.
6. How SQL Server Maintains the View
Whenever a row is inserted, updated, or deleted in the Orders table:
SQL Server recalculates affected aggregates.
The indexed view is updated transactionally.
Reads become extremely fast since the work is precomputed.
Trade-off
Writes become slightly slower (because of view updates).
Reads become much faster.
This makes indexed views ideal for read-heavy systems.
7. Querying the Materialized View
Your API can query the view directly:
SELECT *
FROM dbo.vwCustomerOrderSummary
WHERE CustomerId = 10;
This returns results instantly because the aggregates are already computed.
Automatic Query Rewrite
Even if you do not query the view directly, SQL Server may rewrite queries internally to use the indexed view—if:
The session uses
WITH (NOEXPAND)in Standard EditionEnterprise Edition automatically uses indexed views
Example
SELECT
CustomerId,
COUNT(*) AS TotalOrders
FROM dbo.Orders
WHERE Status = 'Completed'GROUP BY CustomerId
OPTION (NOEXPAND);
This forces SQL Server to use the materialized view.
8. Materialized Views for Complex Aggregates
Let’s explore advanced use cases.
8.1 Daily Summary (Partition-Aware Aggregates)
CREATE VIEW dbo.vwDailySalesSummary
WITH SCHEMABINDING
ASSELECT
CAST(OrderDate AS DATE) AS OrderDay,
COUNT_BIG(*) AS TotalOrders,
SUM(Amount) AS TotalAmount
FROM dbo.Orders
GROUP BY CAST(OrderDate AS DATE);
Index
CREATE UNIQUE CLUSTERED INDEX IX_vwDailySalesSummary
ON dbo.vwDailySalesSummary(OrderDay);
Ideal for dashboards showing:
Today’s sales
Yesterday’s sales
Trend charts
8.2 Multi-Level Aggregates
Materialized views support multiple grouping levels inside a single view.
Example: Customer + Month aggregates:

Join the conversation! Your thoughts help the community grow.