Вы находитесь на странице: 1из 21

Introduction

This article discusses one of the basic but greatly misunderstood objects in SQL server Views. Views could be looked as an additional layer on the table which
enables us to protect intricate or sensitive data based upon our needs. Its like a window exposed to the flocks and they can see very selective items inside the
room through the window. Since views are an additional layer sure they do add an overhead but there is a tradeoff for situations when they are of great help.

As we proceed, we will try to learn more about them not by theoretical explanation but by some practical examples.

Uses

We begin with creating 3 tables PRODUCTS, Customer & BOOKING. These are fictitious tables for our demo. The PRODUCTS stores data

for a retail shop with a flag column IsSalable based on whose value we treat the products as Salable.

Hide Copy Code

CREATE TABLE PRODUCTS


(ProductID INT PRIMARY KEY CLUSTERED,
ProductDesc VARCHAR(50) NOT NULL,
ManufacturingDate DATETIME,
ExpiryDate DATETIME,
IsSalable BIT,--1 Salable/Active FOR 0 For NonSalable/Passive Product
Price MONEY NOT NULL
)

Next, we have a Customer table which stores UserID and Password details for customers.

Hide Copy Code

CREATE TABLE Customer


(CustID INT IDENTITY(1002,2)PRIMARY KEY CLUSTERED,
FName VARCHAR(50) NOT NULL,
LNme VARCHAR(50) NOT NULL,
UserID VARCHAR(100) NOT NULL,
Pswd NVARCHAR(100) NOT NULL DEFAULT 'password123'
)

Lastly, I have created a BOOKING table which houses all the bookings from different customers.

Hide Copy Code

CREATE TABLE BOOKING


( BookingID INT IDENTITY(10,2) PRIMARY KEY CLUSTERED,
ProductID INT REFERENCES dbo.Products(ProductID),
CustID INT REFERENCES dbo.Customer(CustID),
DateOfBooking DATETIME NOT NULL,
QTY INT
)

Next, insert a few records into these tables:

Hide Copy Code

INSERT INTO PRODUCTS VALUES


(1,'Biscuits','2011-09-01 00:00:00.000','2012-09-01 00:00:00.000',1,20),
(2,'Butter','2010-09-01 00:00:00.000','2011-09-01 00:00:00.000',1,30),
(3,'Milk','2011-10-01 00:00:00.000','2011-11-01 00:00:00.000',1,46)
INSERT INTO Customer (FName,LNme,UserID,Pswd)
VALUES
('Sara','Verma','S.Verma@abc.com','S123'),
('Rick','Singh','G.Singh@xyz.com','G311'),
('Micky','Khera','M.Khera@mno.com','M222')

INSERT INTO BOOKING (ProductID,CustID,DateOfBooking,QTY)


VALUES
(1,1002,'2011-11-01 00:00:00.000',3),
(2,1004,GETDATE(),4),
(3,1006,'2011-10-01 00:00:00.000',2)

Our tables contents look like this. I know the tables are not completely normalized, for now please ignore them, these are simple demo tables.

Hide Copy Code

SELECT * FROM Customer


CustID FName LNme UserID Pswd
--------- -------- ---------- --------------- ---------
1002 Sara Verma S.Verma@abc.com S123
1004 Rick Singh G.Singh@xyz.com G311
1006 Micky Khera M.Khera@mno.com M222

(3 row(s) affected)

Select * from PRODUCTS

ProductID ProductDesc ManufacturingDate ExpiryDate IsSalable Price


---------- ------------ ----------------------- ----------------------- --------- -------
1 Biscuits 2011-09-01 00:00:00.000 2012-09-01 00:00:00.000 1 20.00
2 Butter 2010-09-01 00:00:00.000 2011-09-01 00:00:00.000 1 30.00
3 Milk 2011-10-01 00:00:00.000 2011-11-01 00:00:00.000 1 46.00

(3 row(s) affected)

Select * from BOOKING


BookingID ProductID CustID DateOfBooking QTY
----------- ----------- ----------- ----------------------- -----------
10 1 1002 2011-11-01 00:00:00.000 3
12 2 1004 2011-10-09 17:31:31.790 4
14 3 1006 2011-10-01 00:00:00.000 2

(3 row(s) affected)

A customer purchases/books a product and the same gets recorded into the BOOKING table now to generate the bill on his name we can
uses a VIEW which would help us do away with a physical table. Instead it would enable us to generate the bill based on the information from these 3 tables itself.
Lets see how its possible.

Hide Copy Code

CREATE VIEW Bill_V


AS
SELECT C.FName
,C.LNme
,P.ProductDesc
,B.DateOfBooking
,P.Price
,B.QTY
,(B.QTY*P.Price) AS TotalAmountPayable
FROM BOOKING B
INNER JOIN PRODUCTS P
ON B.ProductID=P.ProductID
INNER JOIN Customer C
ON B.CustID=C.CustID;
Next if I,
Select * from Bill_V
Hide Copy Code

FName LNme ProductDesc DateOfBooking Price QTY TotalAmountPayable


-------------------------------------------------- ------------------------------------
Sara Verma Biscuits 2011-11-01 00:00:00.000 20.00 3 60.00
Rick Singh Butter 2011-10-09 17:31:31.790 30.00 4 120.00
Micky Khera Milk 2011-10-01 00:00:00.000 46.00 2 92.00

(3 row(s) affected)

We have been able to generate the bill based on the 3 tables hence we have not only optimized the bill generation also we have saved ourselves from hosting a
physical table in the database with this information.

This is the most credible use of a VIEW; it can not only reduce apparent complexity but also prevent redundant hosting of data in the DB.

Next say there is some API which enables the Customer care executives to view the customerinformation details. Now exposing the Password
might be risky, its strictly confidential info.

We create a View which can be exposed to the API:

Hide Copy Code

CREATE VIEW dbo.CustomerInfo_V


AS
Select CustID
,FNAME AS [FIRST NAME]
,LNME AS [LAST NAME]
,UserID
FROM dbo.Customer

We have a created a View which can be used by the API to fetch customer details (Minus) the Password Column.

Views can be used to prevent sensitive information from being selected, while still allowing other important data.

Views do not have a physical existence, but still they do return a set of record set as a table does, the differences is it is simply an additional layer which calls the
underlying code which finally returns the record set.

When I execute the code...

Hide Copy Code

Select * from CustomerInfo_V

...I get recordsets as I would get in a table with the only difference that the data returned is as per the below query:

Hide Copy Code

Select CustID
,FNAME AS [FIRST NAME]
,LNME AS [LAST NAME]
,UserID
FROM dbo.Customer
But arguably, we still get a set of records, isnt it? So say if there are 1 million customers in my database, wouldnt it be cool if I have clustered/non clustered index
on my view for optimized queries. But is it possible as the view doesnt host data physically, the answer is yes. It is possible to have indexes on views. But before we
find ourselves capable of creating index on views, we have to SCHEMABIND our VIEWs.

What is SCHEMABINDING a VIEW

Schema binding binds your views to the dependent physical columns of the accessed tables specified in the contents of the view, i.e.
if CustomerInfo_V is schema bind no one will be able to alter the dbo.Customer table unless they drop the table.

Why would we need that?

The answer is, it prevents your views from being orphaned. Just think that someone drops/alters the table dbo.Customer without paying any heed to
our view. Now that would leave our view nowhere. Hence schema bind it, this will prevent any such accidents from happening.

Also to be able to create an index on the view you need it essentially schema bound.

Lets make the change:

Hide Copy Code

ALTER VIEW Bill_V


WITH SCHEMABINDING
AS
SELECT C.FName
,C.LNme
,P.ProductDesc
,B.DateOfBooking
,P.Price
,B.QTY
,(B.QTY*P.Price) AS TotalAmountPayable
FROM dbo.BOOKING B
INNER JOIN dbo.PRODUCTS P
ON B.ProductID=P.ProductID
INNER JOIN dbo.Customer C
ON B.CustID=C.CustID;
Now we are licensed to have an Index on this dbo.Bill_V view.
CREATE UNIQUE CLUSTERED INDEX Bill_View_Indx
ON dbo.Bill_V(Fname,LNme);

UNIQUE CLUSTERED INDEX to be able to create


Cool now we have an index on the view, remember you need to have a
a NONCLUSTERED INDEX. By which I mean I can create the below index mandatorily post the creation of the Bill_View_Indx .

Hide Copy Code

CREATE NONCLUSTERED INDEX Bill_View_Indx2


ON dbo.Bill_V(ProductDesc);
So next use of the View is to be able to create an additional index upon the db to speed up your query performance.

Features

Are views only meant for reading data in a customized mode? Not really views also facilitate DML ( Insert/Update/Delete). But there is a set of
rules which needs to be adhered to enable DMLs.

o If you are using a view to insert data, then your view should have a single select and also all the mandatory columns of the being edited table must be included in
NOT NULL columns of the table.
the view unless the table has a default values for all
o Secondly dont forget, for views with WITH CHECK options enabled, its important to keep in mind that the data begin inserted qualifies in
the WHERE clause of the view and is certain to be selected by the view. Simply put the data you insert is picked up while you select from your view.
o join
If the view is having s with more than one table, then most cases chances of modifying capabilities are negligible unless INSTEAD OF Triggers
are in place to handle the request.

Keeping these in mind, lets turn to an example and perform INSERTs/Updates/Deletes.

I am altering the below view as:

Hide Copy Code

ALTER VIEW dbo.CustomerInfo_V


WITH SCHEMABINDING
AS
Select CustID
,FNAME AS [FIRST NAME]
,LNME AS [LAST NAME]
,UserID
FROM dbo.Customer
WITH CHECK OPTION

4. Insert

Hide Copy Code

INSERT INTO CustomerInfo_V


([FIRST NAME],[LAST NAME],UserID)
VALUES ('Gurum','Ramaswamy','G.Ram@qrs.com')

The insert happened because though the columns CustID and Pswd are mandatory but CustID is IDENTITY and PSWD has
a DEFAULT. All the other mandatory data was supplied in the insert query.

Hide Copy Code

SELECT * FROM Customer

CustID FName LNme UserID Pswd


--------- -------- ---------- --------------- ---------
1002 Sara Verma S.Verma@abc.com S123
1004 Rick Singh G.Singh@xyz.com G311
1006 Micky Khera M.Khera@mno.com M222
1008 Gurum Ramaswamy G.Ram@qrs.com password123

(4 row(s) affected)

5. Update

Hide Copy Code

UPDATE CustomerInfo_V
SET [FIRST NAME]='Gurumoorthy'
WHERE [FIRST NAME]='Gurum'
SELECT * FROM Customer
Hide Copy Code

CustID FName LNme UserID Pswd


--------- -------- ---------- --------------- ---------
1002 Sara Verma S.Verma@abc.com S123
1004 Rick Singh G.Singh@xyz.com G311
1006 Micky Khera M.Khera@mno.com M222
1008 Gurumoorthy Ramaswamy G.Ram@qrs.com password123

(4 row(s) affected)
6. Delete

Hide Copy Code

DELETE FROM CustomerInfo_V


WHERE [FIRST NAME]='Gurumoorthy'
SELECT * FROM Customer
Hide Copy Code

CustID FName LNme UserID Pswd


--------- -------- ---------- --------------- ---------
1002 Sara Verma S.Verma@abc.com S123
1004 Rick Singh G.Singh@xyz.com G311
1006 Micky Khera M.Khera@mno.com M222
Displaying the View Contents
For retrieving what is under the hood of the view use,
EXECUTE SP_HELPTEXT 'dbo.CustomerInfo_V'

Alternatively what I do is in my SSMS.

Tools > Options > Environment >Keyboard > Ctrl-F1 == SP_HELPTEXT

From next time, to see the contents of a VIEW/StoreProcedure, simply select it and hit Ctrl+F1?

Refreshing Views
Just in case we are working with a non-schema bound view and there is some change in the underlying table, to prevent the view from producing unexpected
results, we have an option to refresh the view with:

Hide Copy Code

EXECUTE SP_REFRESHVIEW 'dbo.BILL_V'

This updates the metadata of a non-schema bound view.

Encrypting your Views

The WITH ENCRYPTION option encrypts the views by which I mean it will not be visible viaSP_HELPTEXT so in case of strict requirements
where the contents of the view dont need to be exposed this option freezes the view. Its important to save the contents script in some archive to be able to
retrieve the code for any change.

Hide Copy Code

ALTER VIEW Bill_V


WITH ENCRYPTION
AS
SELECT C.FName
,C.LNme
,P.ProductDesc
,B.DateOfBooking
,P.Price
,B.QTY
,(B.QTY*P.Price) AS TotalAmountPayable
FROM dbo.BOOKING B
INNER JOIN dbo.PRODUCTS P
ON B.ProductID=P.ProductID
INNER JOIN dbo.Customer C
ON B.CustID=C.CustID;
So we have encrypted the view, now if we try to EXECUTE SP_HELPTEXT 'dbo.Bill_V'.

The result would be:

Hide Copy Code

The text for object ' Bill_V ' is encrypted.

It is not advised to encrypt your views unless you have a very strong reason behind it. That was some key elements involved. I hope the next time you work with
views, this article will help you make better decisions.
SQL Server Indexed Views: The Basics
Views are a valuable tool for the SQL Server Developer, because they hide complexity and allow for a readable style of SQL expression. They aren't there
for reasons of performance, and so indexed views are designed to remedy this shortcoming. They're great in certain circumstances but they represent a
trade-off, and they come with considerable 'small print'. Jes Borland explains.

SQL Server views are helpful in many ways, for example in encapsulating complex multi-table query logic, allowing us to simplify client code.
Views make queries faster to write, but they dont improve the underlying query performance. However, we can add a unique, clustered
index to a view, creating an indexed view, and realize potential and sometimes significant performance benefits, especially when performing
complex aggregations and other calculations. In short, if an indexed view can satisfy a query, then under certain circumstances, this can
drastically reduce the amount of work that SQL Server needs to do to return the required data, and so improve query performance.

Indexed views can be a powerful tool, but they are not a free lunch and we need to use them with care. Once we create an indexed view,
every time we modify data in the underlying tables then not only must SQL Server maintain the index entries on those tables, but also the
index entries on the view. This can affect write performance. In addition, they also have the potential to cause other issues. For example, if
one or more of the base tables is subject to frequent updates, then, depending on the aggregations we perform in the indexed view, it is
possible that we will increase lock contention on the views index.

This article will start from the basics of creating indexed views, and the underlying requirements in order to do so, and then discuss their
advantages and the situations in which they can offer a significant boost to query performance. Well also consider the potential pitfalls of
which you need to be aware before deciding to implement an indexed view.

From Views to Indexed Views


Nobody sets out to write overly complex queries. Unfortunately, however, applications grow more complex as the users demand new
features, and so the accompanying queries grow more complex also. We dont always have time to rewrite a query completely, or may not
even know a better way to write it.

Standard SQL Server views can help. When we encapsulate complex multi-table query logic in a view, any application that needs that data is
then able to issue a much simpler query against the view, rather than a complex multi-JOIN query against the underlying tables. Views bring
other advantages too. We can grant users SELECT permissions on the view, rather than the underlying tables, and use the view to restrict
the columns and rows that are accessible to the user. We can use views to aggregate data in a meaningful way.

Lets say we need to run various queries against the AdventureWorks2012 database to return information regarding items that customers
have purchased. The query in Listing 1 joins five tables to get information such as the client name, the order number and date, the products
and quantities ordered.

SELECT CUST.CustomerID ,
PER.FirstName ,
PER.LastName ,
SOH.SalesOrderID ,
SOH.OrderDate ,
SOH.[Status] ,
SOD.ProductID ,
PROD.Name ,
SOD.OrderQty
FROM Sales.SalesOrderHeader SOH
INNER JOIN Sales.SalesOrderDetail SOD
ON SOH.SalesOrderID = SOD.SalesOrderID
INNER JOIN Production.Product PROD
ON PROD.ProductID = SOD.ProductID
INNER JOIN Sales.Customer CUST
ON SOH.CustomerID = CUST.CustomerID
INNER JOIN Person.Person PER
ON PER.BusinessEntityID = CUST.PersonID;

Listing 1

Notice that we use two-part naming for all tables. Not only is this a good practice, its also a requirement when creating an indexed view (well
discuss further requirements as we progress). Lets assume that many applications need to run queries like this, joining the same tables, and
referencing the same columns in various combinations. To make it easier for our application to consume this data, we can create a view

Create a View
Listing 2 creates a view based on our query definition, as shown in Listing 2.

CREATE VIEW Sales.vCustomerOrders


WITH SCHEMABINDING
AS
<Select Statmenet from Listing 1>

Listing 2

Note that the WITH SCHEMABINDING option is included here and is a requirement for creating an index on the view, which well want to do
shortly. This option stipulates that we cannot delete any of the base tables for the view, or ALTER any of the columns in those tables. In order
to make one of these changes, we would have to drop the view, change the table, and then recreate the view (and any indexes on the view).

Now, each application simply has to run a much simpler query referencing the view, as shown in Listing 3.

SELECT CustomerID ,
FirstName ,
LastName ,
SalesOrderID ,
OrderDate ,
Status ,
ProductID ,
Name ,
OrderQty
FROM Sales.vCustomerOrders CO;

Listing 3

However, when looking at the execution plan (Figure 1) we can see that SQL Server still performs index scans against each of the five
underlying tables.

Figure 1

Likewise, the STATISTICS IO output (Figure 2) shows that SQL Server performed 2,172 logical reads against the five base tables.

Figure 2
The execution plan reports the query cost as 6.01323, as shown in Figure 3.

Figure 3

We see the exact same execution plan, STATISTICS IO output, and query cost if we run the query in Listing 1 again.

Although the use of the view made writing the query easier, it had no impact on query performance. A simple view is just a virtual table,
generated from a saved query. It does not have its own physical page structure to use, so it reads the pages of its underlying tables. In other
words, when we query a simple view, the optimizer still has to access all of the underlying tables and perform the necessary JOINs and
aggregations. It derives cardinality estimations, and hence the query plan, from statistics associated with those tables.

Lets see what happens, however, if we turn our standard view into an indexed view.

Create a Unique, Clustered Index


Before we start, I should mention that there are a host of requirements attached to the creation of indexed views, in any SQL Server Edition.
Well discuss these in more detail in the Indexed View Requirements section, but if you have trouble creating an index on a view, its likely
youre breaking one of the rules.

In order to turn our normal Sales.vCustomerOrders view into an indexed view, we need to add a unique clustered index, as shown in
Listing 4.

CREATE UNIQUE CLUSTERED INDEX CIX_vCustomerOrders


ON Sales.vCustomerOrders(CustomerID, SalesOrderID, ProductID);

Listing 4

When we add a unique clustered index to a view, we materialize it. In other words, the virtual table persists to disk, with its own page
structure, and we can treat it just like a normal table. Any aggregations defined by the indexed view are now pre-computed, and any joins
pre-joined, so the engine no longer has to do this work at execution time. SQL Server creates statistics for the indexed view, different from
those of the underlying tables, to optimize cardinality estimations.

A well-crafted indexed view can write fewer pages to disk than the underlying tables, meaning fewer pages queries need to read fewer pages
to return results. This means faster, more efficient queries. Use the techniques and tips in this article to ensure your views are optimal!

Lets see the impact of our indexed view on query performance. These examples assume youre running SQL Server Enterprise Edition,
which will automatically consider indexes on a view when creating a query execution plan, whereas SQL Server Standard Edition wont;
youll need to use the WITH (NOEXPAND) table hint directly in the FROM clause of any query you wish to use the view (more on this shortly).
When we re-run the query from Listing 3, we get the same result set, but the execution plan, shown in Figure 4, looks very different. Rather
than several index scans with joins, the optimizer now determines that the optimal way to satisfy the query is to scan the clustered index of
our view.

Figure 4

The optimizer now reads all the pages required from one index, rather than five, and STATISTICS IOoutput reveals that this results in a
27% reduction in the number of logical reads the engine must perform in order to return the data, from 2,172 to 1,590.

Figure 5

The overall query cost falls to 1.30858, as seen in Figure 6.

Figure 6

Its not only queries that reference the view directly that will benefit in this way. Any query that the Optimizer determines the view could
satisfy can use the indexed view rather than underlying tables, a process termed view matching. Try re-running Listing 1, which references
the base tables rather than our indexed view. The Optimizer determines that the views index is the optimal way to retrieve the data and the
execution plan will be identical to that in Figure 4.

In Listing 5, we access the same base tables but also perform some aggregations.

SELECT CUST.CustomerID ,
SOH.SalesOrderID ,
SOH.OrderDate ,
SOD.ProductID ,
PROD.Name ,
SUM(SOD.OrderQty) AS TotalSpent
FROM Sales.SalesOrderHeader SOH
INNER JOIN Sales.SalesOrderDetail SOD
ON SOH.SalesOrderID = SOD.SalesOrderID
INNER JOIN Production.Product PROD
ON PROD.ProductID = SOD.ProductID
INNER JOIN Sales.Customer CUST
ON SOH.CustomerID = CUST.CustomerID
INNER JOIN Person.Person PER
ON PER.BusinessEntityID = CUST.PersonID
GROUP BY CUST.CustomerID ,
SOH.SalesOrderID ,
SOH.OrderDate ,
SOD.ProductID ,
PROD.Name;

Listing 5

Here, the execution plan shows that the Optimizer chose to use the clustered index on the view, rather than indexes on the base tables.

Figure 7

This execution plan shows a yellow exclamation point over the clustered index scan, which is warning us of Columns with no statistics.
Well discuss this in more detail shortly.
Figure 8

Aggregating Data with Indexed Views


Indexed views can really come into their own when we have many applications that need to perform complex aggregations, and other
calculations, on the same set of base tables. Rather than force SQL Server to perform these aggregations and calculations every time, upon
query execution, we can encapsulate them in an indexed view. This can significantly reduce the amount of IO SQL Server must perform to
retrieve the necessary data, and CPU time required to perform the calculations, and so can provide tremendous performance boosts.

Before we dive into another example, its worth mentioning again that, despite the potential performance benefits, caution is required when
implementing an indexed view unless the base tables are relatively static. Well discuss this in more detail shortly.

Consider the query in Listing 6.

SELECT CUST.CustomerID ,
SOH.SalesOrderID ,
SOD.ProductID ,
SUM(SOD.OrderQty) AS TotalOrderQty ,
SUM(LineTotal) AS TotalValue
FROM Sales.SalesOrderHeader SOH
INNER JOIN Sales.SalesOrderDetail SOD
ON SOH.SalesOrderID = SOD.SalesOrderID
INNER JOIN Production.Product PROD ON PROD.ProductID = SOD.ProductID
INNER JOIN Sales.Customer CUST ON SOH.CustomerID = CUST.CustomerID
INNER JOIN Person.Person PER ON PER.BusinessEntityID = CUST.PersonID
GROUP BY CUST.CustomerID ,
SOH.SalesOrderID ,
SOD.ProductID;

Listing 6

This query produces an execution plan with several index scans and joins, shown in Figure 9. It also requires aggregation. Its execution plan
is similar in nature to the one we saw in Figure 4, but with additional operations and a higher cost, of 7.62038.

Figure 9

The logical reads are high as well, spanning several tables, as seen in Figure 10.

(121317 row(s) affected)


Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads
0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 1246, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-
ahead reads 0.
Table 'SalesOrderHeader'. Scan count 1, logical reads 58, physical reads 1, read-ahead reads 63, lob logical reads 0, lob physical reads 0, lob read-
ahead reads 0.
Table 'Customer'. Scan count 1, logical reads 123, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead
reads 0.
Table 'Person'. Scan count 1, logical reads 67, physical reads 1, read-ahead reads 65, lob logical reads 0, lob physical reads 0, lob read-ahead reads
0.
Table 'Product'. Scan count 1, logical reads 4, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Figure 10

Listing 7 creates an indexed view, vSalesSummaryCustomerProduct, to help reduce the cost of this and similar queries.

CREATE VIEW Sales.vSalesSummaryCustomerProduct


WITH SCHEMABINDING
AS
SELECT CUST.CustomerID ,
SOH.SalesOrderID ,
SOD.ProductID ,
SUM(SOD.OrderQty) AS TotalOrderQty ,
SUM(LineTotal) AS TotalValue ,
COUNT_BIG(*) AS CountLines
FROM Sales.SalesOrderHeader SOH
INNER JOIN Sales.SalesOrderDetail SOD
ON SOH.SalesOrderID = SOD.SalesOrderID
INNER JOIN Production.Product PROD
ON PROD.ProductID = SOD.ProductID
INNER JOIN Sales.Customer CUST
ON SOH.CustomerID = CUST.CustomerID
INNER JOIN Person.Person PER
ON PER.BusinessEntityID = CUST.PersonID
GROUP BY CUST.CustomerID ,
SOH.SalesOrderID ,
SOD.ProductID;
GO
CREATE UNIQUE CLUSTERED INDEX CX_vSalesSummaryCustomerProduct
ON Sales.vSalesSummaryCustomerProduct(CustomerID, SalesOrderID, ProductID);
GO

Listing 7

Note the use of COUNT_ BIG( *) in this view, a requirement for indexed views that have a GROUP BY. It is there for the internal
maintenance of indexed views it maintains a count of the rows per group in the indexed view.

Now we can return the same result set by running the simple query in Listing 8.

SELECT CustomerID ,
SalesOrderID ,
TotalOrderQty ,
TotalValue
FROM Sales.vSalesSummaryCustomerProduct;

Listing 8

Figure 11 shows that weve reduced the query cost from 7.62038 to 0.694508. If we check the STATISTICS IO output, well also find a
substantial reduction in the number logical reads, from 1,498 across five indexes to 758.

Figure 11

Again, notice the yellow exclamation mark; hovering over the index scan icon reveals that it is a Columns with no statistics warning on
the SalesOrderID column, the second column in the clustered index key. We can see that SQL Server has created a statistics object for
this clustered index, as shown in Figure 12.
Figure 12

However, if we run the query using the WITH (NOEXPAND) hint, as shown in Figure 14, we will no longer see the warning.

SELECT CustomerID ,
SalesOrderID ,
TotalOrderQty ,
TotalValue
FROM Sales.vSalesSummaryCustomerProduct WITH ( NOEXPAND );

Listing 9

What is going on? The difference lies in when and how SQL Server creates automatic statistics, and when it uses them. Simply put, if we do
not use the WITH (NOEXPAND) hint when querying an indexed view, the query optimizer will not use statistics created on the indexed view
and neither will it create or update statistics automatically (i.e. those statistics objects that begin with _WA_SYS).

Without automatically created or updated statistics, there can be a slight or even drastic difference between the numbers of rows the
optimizer estimates a query will return, and the actual number of rows returned. Pay attention to statistics warnings if you see them!

What is the lesson to be learned here? Using the WITH (NOEXPAND) hint when writing queries that reference indexed views is the best way
to ensure optimal query plans.

Wait, SQL Server didnt use my index!


Unfortunately, there may still be occasions when the query optimizer decides not to use an indexed view, even though it seems that it could
satisfy a query. In fact, SQL Server may refuse to use the clustered index (or any non-clustered indexes) on a view, even if we reference the
view directly in the query.

Lets return to our vCustomerOrders example. Lets say we want to query the view for the total number of orders a customer has placed,
along with the total value of those orders, and we want to search by CustomerID.

SELECT CustomerID ,
COUNT(SalesOrderID) AS OrderCount ,
SUM(TotalValue) AS OrderValue
FROM Sales.vSalesSummaryCustomerProduct
WHERE CustomerID = 30103
GROUP BY CustomerID;

Listing 10

The execution plan, in Figure 13, shows that the plan references the underlying tables and ignores our view and its index. The query cost is
.072399.
Figure 13

To make the query optimizer use the unique clustered index I created on vSalesSummaryCustomerProduct, we can use
the NOEXPAND hint.

SELECT CustomerID ,
COUNT(SalesOrderID) AS OrderCount ,
SUM(TotalValue) AS OrderValue
FROM Sales.vSalesSummaryCustomerProduct WITH ( NOEXPAND )
WHERE CustomerID = 30103
GROUP BY CustomerID;

Listing 11

Now, the execution plan shows a clustered index seek, as shown in Figure 14, and the query cost is .003522.

Figure 14

Adding Non-clustered Indexes to an Indexed View


Once weve created an indexed view we can then treat it in much the same way as a normal table. We can add non-clustered indexes to
boost query performance. Once again, exercise care. SQL Server has to maintain every index we add to a view, every time someone
updates one of the contributing base tables. Indexed views work better for relatively static base tables.

Lets say we want to query our Sales.vCustomerOrders view by product name.

DECLARE @ProductName VARCHAR(50)


SET @ProductName = 'LL Mountain Frame - Black, 44'

SELECT CustomerID ,
SalesOrderID ,
OrderQty ,
Name
FROM Sales.vCustomerOrders
WHERE Name = @ProductName;

Listing 12

We get a clustered index scan on the view and the query cost is 1.30858.
Figure 15

Its great that SQL Server is using the clustered index on the view; but a scan isnt what we want; a seek would be better. However, once the
clustered index exists, we can easily add useful non-clustered indexes, just as we can for any normal table.

CREATE NONCLUSTERED INDEX IX_vCustomerOrders_Name


ON Sales.vCustomerOrders(Name);

Listing 13

When we run the query in Listing 12 again, the execution plan is as shown in Figure 16. The query cost has gone down, to 1.27252.

Figure 16

This time the optimizer chose a seek operation on the new non-clustered index, which is what we wanted. However, it also needed to
perform a key lookup to return the additional columns contained in the SELECT clause but not included in the non-clustered index.

To make this index more effective, we can make it a covering index for this query by including all of the columns the query references, as
shown in Listing 14.
DROP INDEX IX_vCustomerOrders_Name ON Sales.vCustomerOrders;
GO

CREATE NONCLUSTERED INDEX IX_vCustomerOrders_Name


ON Sales.vCustomerOrders(Name)
INCLUDE (SalesOrderID, CustomerID, OrderQty);
GO

Listing 14

When we run the query again, we see an optimized index seek on the non-clustered index. We also see a significantly reduced query cost,
down to .0059714.

Indexed View Requirements


An underlying assumption of all previous query examples was use of SQL Server Enterprise Edition. In this edition, SQL Servers query
optimizer will automatically consider indexes on a view when creating a query execution plan. In SQL Server Standard Edition, we can still
create indexed views, but the optimizer will not automatically consider its indexes when formulating an execution plan for a query; it will
simply access all of the underlying tables. We have to use the WITH (NOEXPAND) table hint, directly in the FROM clause of each query, to
force SQL Server to use the indexed view.

SELECT <column-list>
FROM Sales.vCustomerOrders CO WITH ( NOEXPAND );

Listing 15

We also noted occasions, even when using SQL Server Enterprise Edition, when we may need to use this hint to get the plan we expect.
However, it can be dangerous to boss the query optimizer around, telling it what it can or cant do. Bear in mind also that if you write queries
in stored procedures, your applications, or reports that use WITH (NOEXPAND) and then drop the index on the view at a later point in time,
the queries that reference that index will fail. In short, the same proviso applies here as applies to the use of any table (index), join, or query
hints: use them cautiously and sparingly, and document them.

As well as this requirement when using views on Standard Edition, there is a lengthy list of requirements attached to the creation of indexed
views, in any SQL Server Edition. Weve encountered several already, in the need to create them with the SCHEMABINDING option, use fully
qualified table references, and use COUNT_ BIG ( *) if the view definition contains a GROUP BY clause. Another, implied but not
discussed directly, is that the indexed view definition can only reference tables, not other views.

The Microsoft documentation (http://msdn.microsoft.com/en-us/library/ms191432.aspx) provides a full list of limitations and requirements, so
Ill just briefly summarize some of the more significant here:

Certain database SET options have required values if we wish to create any indexed views in that database for
example, ANSI_NULLS, ANSI_PADDING, ANSI_WARNINGS, ARITHABORT, CONCAT_NULL_YIELDS_NULL,
and QUOTED_IDENTIFIER must be ON; NUMERIC_ROUNDABORT must be OFF.
All columns referenced in the view must be deterministic that is, they must return the same value each time. As an
example, GETDATE( ) is non-deterministic. DATEADD and DATEDIFFare deterministic.
We cannot include certain common functions in an indexed view COUNT, DISTINCT, MIN, MAX, TOP, and more.
You cant have a self-join or an outer join, an OUTER APPLY or a CROSS APPLY.

If you are having difficulty creating an index on a view, reference the Microsoft documentation, as youve broken one of the rules.

Impact of Updating the Base Tables


A few times, Ive mentioned the impact of modifying data, i.e. inserting into, updating or deleting from, the base tables of an indexed view,
and its now time to discuss this issue in more detail.

SQL Server has to guarantee that it can return a consistent result set regardless of whether a query accesses a view or the underlying
tables, so it will automatically maintain indexes in response to data modifications on the base tables. We can see this in action if we update
one of the base tables that make up our vSalesSummaryCustomerProduct view.

UPDATE Sales.SalesOrderDetail
SET OrderQty = 5
WHERE SalesOrderID = 71803
AND ProductID = 917;

Listing 16

Table 'vCustomerOrders'. Scan count 0, logical reads 12, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-
ahead reads 0.
Table 'Product'. Scan count 0, logical reads 8, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Person'. Scan count 0, logical reads 12, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Customer'. Scan count 0, logical reads 8, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads
0.
Table 'SalesOrderHeader'. Scan count 0, logical reads 12, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-
ahead reads 0.
Table 'Worktable'. Scan count 3, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead
reads 0.
Table 'vSalesSummaryCustomerProduct'. Scan count 0, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads
0, lob read-ahead reads 0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 7, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead
reads 0.

Figure 17

The execution plan includes many operators, including an update of the vSalesSummaryCustomerProduct clustered index.

Figure 18

SQL Server must always ensure that the data in the index and base tables is synchronized, so we need to be careful when adding indexes to
views. Every time an underlying column has a new row added or deleted, or is updated, SQL Server must maintain every clustered and non-
clustered index, whether on the base table or the referenced indexed view. This will lead to additional writes, which can decrease
performance.

Another side effect of indexed views is increased potential for blocking on the base tables during inserts, updates, and deletes, due to
increased lock contention on the views index. Lets say we want to insert two records into the SalesOrderHeader table. With no indexed
view referenced by the table, both inserts will succeed. However, add an indexed view and the behavior will change. The first insert will have
to modify a row in the table, and it will have to modify the index. It will hold locks on both objects. Until that has completed, the second
operation will not be able to complete because it also needs locks on both objects.

A great demo of this is available from Alex Kuznetsov in his article, Be ready to drop your indexed view.

Directly Modifying a View


Can we, and should we, insert into, update, or delete from an indexed view, directly? Yes, we can, and no, we probably shouldnt.

The conditions for directly modifying data in an indexed view are the same as for a regular view, namely:

The columns can only be from one underlying table


The columns cant be derived so you cant have an aggregation, function, or computation
The columns cant be affected by GROUP BY, HAVING, or DISTINCT
The view cant use TOP with WITH CHECK OPTION
Those are the official rules, but there are a host of other things to think about, as well. For example, when inserting into a table via a view, if
there are NOT NULL columns defined in the table, but are not in the view, your insert will fail, as demonstrated with this example.

CREATE VIEW Production.vProductInfoCategory


WITH SCHEMABINDING
AS
SELECT PC.Name AS CategoryName ,
PSC.Name AS SubcategoryName ,
PROD.ProductID ,
PROD.ProductNumber ,
PROD.Name AS ProductName
FROM Production.Product PROD
INNER JOIN Production.ProductSubcategory PSC
ON PSC.ProductSubcategoryID = PROD.ProductSubcategoryID
INNER JOIN Production.ProductCategory PC
ON PC.ProductCategoryID = PSC.ProductCategoryID;
GO
CREATE UNIQUE CLUSTERED INDEX CIX_vProductInfoCategory
ON Production.vProductInfoCategory(ProductID);
GO
INSERT INTO Production.vProductInfoCategory
( ProductNumber ,
ProductName
)
VALUES ( 'VE-C304-XL' ,
'Classic Vest, XL'
);
GO

Listing 17

Running this query results in the error in Figure 19.

Msg 515, Level 16, State 2, Line 1


Cannot insert the value NULL into column 'SafetyStockLevel', table
'AdventureWorks2012.Production.Product'; column does not allow nulls. INSERT fails.

Figure 19

The column SafetyStockLevel was not part of the view, and not part of my insert, but it is one of the NOT NULL columns defined in the
table.

Generally, unless there are security reasons to use a view for inserts, or you are using an indexed view to enforce a uniqueness constraint, I
dont recommend inserting, updating, or deleting in views. It is much easier to deal directly with the base tables.

Summary
Views are a powerful tool in SQL Server to help you write queries more efficiently and provide a layer of additional security. With indexed
views, we can also significantly reduce the I/O, cost, and duration for a query. They can make complex aggregations more efficient and we
can even apply additional non-clustered indexes to help satisfy further queries.

Вам также может понравиться