{"id":1760,"date":"2014-02-12T00:00:00","date_gmt":"2014-02-12T00:00:00","guid":{"rendered":"https:\/\/test.simple-talk.com\/uncategorized\/sql-server-indexed-views-the-basics\/"},"modified":"2026-04-16T09:53:15","modified_gmt":"2026-04-16T09:53:15","slug":"sql-server-indexed-views-the-basics","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/sql-server-indexed-views-the-basics\/","title":{"rendered":"SQL Server Indexed Views: What They Are and How to Create Them"},"content":{"rendered":"<div class=\"article-content\">\n<p><strong>An indexed view in SQL Server is a view with a unique clustered index physically built on it &#8211; persisting the view&#8217;s result set on disk rather than recomputing it on every query. This makes complex aggregations and multi-table joins significantly faster when the same view is queried frequently, at the cost of additional storage and write overhead. SQL Server&#8217;s indexed views are equivalent to what other databases call materialised views. <\/strong><\/p>\n<p><strong>Creating an indexed view requires two conditions: the view must be created WITH SCHEMABINDING (locking the underlying table schema), and a unique clustered index must be created on the view. In Enterprise Edition, the query optimiser automatically considers indexed views to satisfy queries; in Standard Edition, you must use the NOEXPAND hint explicitly.<\/strong><\/p>\n<p class=\"start\">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 don&#8217;t 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.<\/p>\n<p>Indexed views can be a powerful tool, but they are not a &#8216;free lunch&#8217; 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 view&#8217;s index.<\/p>\n<p>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. We&#8217;ll also consider the potential pitfalls of which you need to be aware before deciding to implement an indexed view.<\/p>\n<h1>From Views to Indexed Views<\/h1>\n<p>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 don&#8217;t always have time to rewrite a query completely, or may not even know a better way to write it.<\/p>\n<p>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-<code>JOIN<\/code> query against the underlying tables. Views bring other advantages too. We can grant users <code>SELECT<\/code> 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.<\/p>\n<p>Let&#8217;s say we need to run various queries against the <code>AdventureWorks2012<\/code> 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.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT  CUST.CustomerID ,\n        PER.FirstName ,\n        PER.LastName ,\n        SOH.SalesOrderID ,\n        SOH.OrderDate ,\n        SOH.[Status] ,\n        SOD.ProductID ,\n        PROD.Name ,\n        SOD.OrderQty\nFROM    Sales.SalesOrderHeader SOH\n        INNER JOIN Sales.SalesOrderDetail SOD \n               ON SOH.SalesOrderID = SOD.SalesOrderID\n        INNER JOIN Production.Product PROD\n               ON PROD.ProductID = SOD.ProductID\n        INNER JOIN Sales.Customer CUST\n               ON SOH.CustomerID = CUST.CustomerID\n        INNER JOIN Person.Person PER\n               ON PER.BusinessEntityID = CUST.PersonID;\n\n<\/pre>\n<p class=\"caption\">Listing 1<\/p>\n<p>Notice that we use two-part naming for all tables. Not only is this a good practice, it&#8217;s also a requirement when creating an indexed view (we&#8217;ll discuss further requirements as we progress). Let&#8217;s 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<\/p>\n<h3>Create a View<\/h3>\n<p>Listing 2 creates a view based on our query definition, as shown in Listing 2.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE VIEW Sales.vCustomerOrders\nWITH SCHEMABINDING\nAS\n  &lt;Select Statmenet from Listing 1&gt;<\/pre>\n<p class=\"caption\">Listing 2<\/p>\n<p>Note that the <code>WITH SCHEMABINDING<\/code> option is included here and is a requirement for creating an index on the view, which we&#8217;ll want to do shortly. This option stipulates that we cannot delete any of the base tables for the view, or <code>ALTER<\/code> 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).<\/p>\n<p>Now, each application simply has to run a much simpler query referencing the view, as shown in Listing 3.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT  CustomerID ,\n        FirstName ,\n        LastName ,\n        SalesOrderID ,\n        OrderDate ,\n        Status ,\n        ProductID ,\n        Name ,\n        OrderQty\nFROM    Sales.vCustomerOrders CO; \n<\/pre>\n<p class=\"caption\">Listing 3<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-f5ac13b3-4c98-42d7-a553-c8524405e34a.png\" alt=\"1938-1-f5ac13b3-4c98-42d7-a553-c8524405e\" \/><\/p>\n<p class=\"caption\">Figure 1<\/p>\n<p>Likewise, the <code>STATISTICS IO<\/code> output (Figure 2) shows that SQL Server performed 2,172 logical reads against the five base tables.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-4%20-%20query%20view%20statsio-7dcc181a-5e72-441f-bae8-c57c625c8eb7.png\" alt=\"1938-4%20-%20query%20view%20statsio-7dcc\" \/><\/p>\n<p class=\"caption\">Figure 2<\/p>\n<p>The execution plan reports the query cost as 6.01323, as shown in Figure 3.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-3b%20-%20query%20view%20cost-d74f3bcc-7503-414e-8152-9ffa11523273.png\" alt=\"1938-3b%20-%20query%20view%20cost-d74f3b\" \/><\/p>\n<p class=\"caption\">Figure 3<\/p>\n<p>We see the exact same execution plan, <code>STATISTICS IO<\/code> output, and query cost if we run the query in Listing 1 again.<\/p>\n<p>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 <code>JOIN<\/code>s and aggregations. It derives cardinality estimations, and hence the query plan, from statistics associated with those tables.<\/p>\n<p>Let&#8217;s see what happens, however, if we turn our standard view into an indexed view.<\/p>\n<h3>Create a Unique, Clustered Index<\/h3>\n<p>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. We&#8217;ll discuss these in more detail in the <em>Index<\/em><em>ed<\/em><em> View Requirements<\/em> section, but if you have trouble creating an index on a view, it&#8217;s likely you&#8217;re breaking one of the rules.<\/p>\n<p>In order to turn our normal <code>Sales.vCustomerOrders<\/code> view into an indexed view, we need to add a unique clustered index, as shown in Listing 4.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE UNIQUE CLUSTERED INDEX CIX_vCustomerOrders \n\tON Sales.vCustomerOrders(CustomerID, SalesOrderID, ProductID);\n<\/pre>\n<p class=\"caption\">Listing 4<\/p>\n<p>When we add a unique clustered index to a view, we &#8216;materialize&#8217; it. In other words, the &#8216;virtual table&#8217; 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.<\/p>\n<p>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!<\/p>\n<p>Let&#8217;s see the impact of our indexed view on query performance. These examples assume you&#8217;re running <strong>SQL Server Enterprise Edition<\/strong>, which will automatically consider indexes on a view when creating a query execution plan, whereas SQL Server Standard Edition won&#8217;t; you&#8217;ll need to use the <code>WITH (NOEXPAND)<\/code> table hint directly in the <code>FROM<\/code> clause of any query you wish to use the view (more on this shortly).<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-5%20-%20query%20view%20exec%20plan-76e98933-026f-4e8c-8b1d-4fccee5c2ab0.png\" alt=\"1938-5%20-%20query%20view%20exec%20plan-\" \/><\/p>\n<p class=\"caption\">Figure 4<\/p>\n<p>The optimizer now reads all the pages required from one index, rather than five, and <code>STATISTICS IO<\/code> output 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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-6%20-%20query%20view%20statsio-4b9bc835-9575-4f12-95c2-fe0e2c958a32.png\" alt=\"1938-6%20-%20query%20view%20statsio-4b9b\" \/><\/p>\n<p class=\"caption\">Figure 5<\/p>\n<p>The overall query cost falls to 1.30858, as seen in Figure 6.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-7%20-%20query%20view%20cost-60fb50eb-52ce-414c-8715-167c13efc39b.png\" alt=\"1938-7%20-%20query%20view%20cost-60fb50e\" \/><\/p>\n<p class=\"caption\">Figure 6<\/p>\n<p>It&#8217;s 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 <em>view matching<\/em>. Try re-running Listing 1, which references the base tables rather than our indexed view. The Optimizer determines that the view&#8217;s index is the optimal way to retrieve the data and the execution plan will be identical to that in Figure 4.<\/p>\n<p>In Listing 5, we access the same base tables but also perform some aggregations.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT  CUST.CustomerID ,\n        SOH.SalesOrderID ,\n        SOH.OrderDate ,\n        SOD.ProductID ,\n        PROD.Name ,\n        SUM(SOD.OrderQty) AS TotalSpent\nFROM    Sales.SalesOrderHeader SOH\n        INNER JOIN Sales.SalesOrderDetail SOD\n               ON SOH.SalesOrderID = SOD.SalesOrderID\n        INNER JOIN Production.Product PROD\n               ON PROD.ProductID = SOD.ProductID\n        INNER JOIN Sales.Customer CUST\n               ON SOH.CustomerID = CUST.CustomerID\n        INNER JOIN Person.Person PER\n               ON PER.BusinessEntityID = CUST.PersonID\nGROUP BY CUST.CustomerID ,\n        SOH.SalesOrderID ,\n        SOH.OrderDate ,\n        SOD.ProductID ,\n        PROD.Name; \n<\/pre>\n<p class=\"caption\">Listing 5<\/p>\n<p>Here, the execution plan shows that the Optimizer chose to use the clustered index on the view, rather than indexes on the base tables.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-8%20-%20query%20matching%20view-5f5befe5-0a58-40f7-b3b1-80f7f393f8a0.png\" alt=\"1938-8%20-%20query%20matching%20view-5f5\" \/><\/p>\n<p class=\"caption\">Figure 7<\/p>\n<p>This execution plan shows a yellow exclamation point over the clustered index scan, which is warning us of &#8220;Columns with no statistics&#8221;. We&#8217;ll discuss this in more detail shortly.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-98ce757e-71ba-42ab-b4f3-f91812469fad.png\" alt=\"1938-1-98ce757e-71ba-42ab-b4f3-f91812469\" \/><\/p>\n<p class=\"caption\">Figure 8<\/p>\n<h3>Aggregating Data with Indexed Views<\/h3>\n<p>Indexed views can really come into their own when we have many applications that need to perform <em>complex aggregations<\/em>, 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.<\/p>\n<p>Before we dive into another example, it&#8217;s worth mentioning again that, despite the potential performance benefits, caution is required when implementing an indexed view unless the base tables are relatively static. We&#8217;ll discuss this in more detail shortly.<\/p>\n<p>Consider the query in Listing 6.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT  CUST.CustomerID ,\n        SOH.SalesOrderID ,\n        SOD.ProductID ,\n        SUM(SOD.OrderQty) AS TotalOrderQty ,\n        SUM(LineTotal) AS TotalValue\nFROM    Sales.SalesOrderHeader SOH\n        INNER JOIN Sales.SalesOrderDetail SOD\n               ON SOH.SalesOrderID = SOD.SalesOrderID\n        INNER JOIN Production.Product PROD ON PROD.ProductID = SOD.ProductID\n        INNER JOIN Sales.Customer CUST ON SOH.CustomerID = CUST.CustomerID\n        INNER JOIN Person.Person PER ON PER.BusinessEntityID = CUST.PersonID\nGROUP BY CUST.CustomerID ,\n        SOH.SalesOrderID ,\n        SOD.ProductID; \n<\/pre>\n<p class=\"caption\">Listing 6<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-9%20-%20query%20with%20aggregation-afe8dd40-2360-4ced-af2e-b0e01ec2c4eb.png\" alt=\"1938-9%20-%20query%20with%20aggregation-\" \/><\/p>\n<p class=\"caption\">Figure 9<\/p>\n<p>The logical reads are high as well, spanning several tables, as seen in Figure 10.<\/p>\n<pre class=\"listing\">(121317 row(s) affected)\nTable '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.\nTable '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.\nTable '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.\nTable '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.\nTable '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.\nTable '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.\n<\/pre>\n<p class=\"caption\">Figure 10<\/p>\n<p>Listing 7 creates an indexed view, <code>vSalesSummaryCustomerProduct<\/code>, to help reduce the cost of this and similar queries.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE VIEW Sales.vSalesSummaryCustomerProduct\nWITH SCHEMABINDING\nAS\n    SELECT  CUST.CustomerID ,\n            SOH.SalesOrderID ,\n            SOD.ProductID ,\n            SUM(SOD.OrderQty) AS TotalOrderQty ,\n            SUM(LineTotal) AS TotalValue ,\n            COUNT_BIG(*) AS CountLines\n    FROM    Sales.SalesOrderHeader SOH\n            INNER JOIN Sales.SalesOrderDetail SOD\n                   ON SOH.SalesOrderID = SOD.SalesOrderID\n            INNER JOIN Production.Product PROD\n                   ON PROD.ProductID = SOD.ProductID\n            INNER JOIN Sales.Customer CUST\n                   ON SOH.CustomerID = CUST.CustomerID\n            INNER JOIN Person.Person PER\n                   ON PER.BusinessEntityID = CUST.PersonID\n    GROUP BY CUST.CustomerID ,\n            SOH.SalesOrderID ,\n            SOD.ProductID; \nGO\nCREATE UNIQUE CLUSTERED INDEX CX_vSalesSummaryCustomerProduct\n  ON Sales.vSalesSummaryCustomerProduct(CustomerID, SalesOrderID, ProductID); \nGO\n<\/pre>\n<p class=\"caption\">Listing 7<\/p>\n<p>Note the use of <code>COUNT_ BIG( *)<\/code> in this view, a requirement for indexed views that have a <code>GROUP BY<\/code>. It is there for the internal maintenance of indexed views &#8211; it maintains a count of the rows per group in the indexed view.<\/p>\n<p>Now we can return the same result set by running the simple query in Listing 8.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT  CustomerID ,\n        SalesOrderID ,\n        TotalOrderQty ,\n        TotalValue\nFROM    Sales.vSalesSummaryCustomerProduct; \n<\/pre>\n<p class=\"caption\">Listing 8<\/p>\n<p>Figure 11 shows that we&#8217;ve reduced the query cost from 7.62038 to 0.694508. If we check the <code>STATISTICS IO<\/code> output, we&#8217;ll also find a substantial reduction in the number logical reads, from 1,498 across five indexes to 758.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-10%20-%20view%20aggregated-1400d952-bd98-4cbc-a5dc-66fc72e92724.png\" alt=\"1938-10%20-%20view%20aggregated-1400d952\" \/><\/p>\n<p class=\"caption\">Figure 11<\/p>\n<p>Again, notice the yellow exclamation mark; hovering over the index scan icon reveals that it is a &#8220;Columns with no statistics&#8221; warning on the <code>SalesOrderID<\/code> 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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-744e48f0-8481-4e81-87b5-94ac603144eb.png\" alt=\"1938-1-744e48f0-8481-4e81-87b5-94ac60314\" \/><\/p>\n<p class=\"caption\">Figure 12<\/p>\n<p>However, if we run the query using the <code>WITH (NOEXPAND) <\/code>hint, as shown in Figure 14, we will no longer see the warning.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT    CustomerID ,\n          SalesOrderID ,\n          TotalOrderQty ,\n          TotalValue\nFROM      Sales.vSalesSummaryCustomerProduct WITH ( NOEXPAND );\n\n<\/pre>\n<p class=\"caption\">Listing 9<\/p>\n<p>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 <code>WITH (NOEXPAND)<\/code> 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 <code>_WA_SYS<\/code>).<\/p>\n<p>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!<\/p>\n<p>What is the lesson to be learned here? Using the <code>WITH (NOEXPAND)<\/code> hint when writing queries that reference indexed views is the best way to ensure optimal query plans.<\/p>\n<p>For a more in-depth review of statistics, try <a href=\"https:\/\/www.simple-talk.com\/sql\/performance\/managing-sql-server-statistics\/\">Managing SQL Server Statistics<\/a>, by Erin Stellato. For a thorough review of indexed views and statistics, reference Paul White&#8217;s article <a href=\"http:\/\/www.sqlperformance.com\/2014\/01\/sql-plan\/indexed-views-and-statistics\">Indexed Views and Statistics<\/a>.<\/p>\n<h3>Wait, SQL Server didn&#8217;t use my index!<\/h3>\n<p>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.<\/p>\n<p>Let&#8217;s return to our <code>vCustomerOrders<\/code> example. Let&#8217;s 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 <code>CustomerID<\/code>.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT  CustomerID ,\n        COUNT(SalesOrderID) AS OrderCount ,\n        SUM(TotalValue) AS OrderValue\nFROM    Sales.vSalesSummaryCustomerProduct\nWHERE   CustomerID = 30103\nGROUP BY CustomerID; \n<\/pre>\n<p class=\"caption\">Listing 10<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-729fe531-3aab-4982-b7e6-78bdcafecb08.png\" alt=\"1938-1-729fe531-3aab-4982-b7e6-78bdcafec\" \/><\/p>\n<p class=\"caption\">Figure 13<\/p>\n<p>To make the query optimizer use the unique clustered index I created on <code>vSalesSummaryCustomerProduct<\/code>, we can use the <code>NOEXPAND<\/code> hint.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT    CustomerID ,\n          COUNT(SalesOrderID) AS OrderCount ,\n          SUM(TotalValue) AS OrderValue\nFROM      Sales.vSalesSummaryCustomerProduct WITH ( NOEXPAND )\nWHERE     CustomerID = 30103\nGROUP BY  CustomerID; \n<\/pre>\n<p class=\"caption\">Listing 11<\/p>\n<p>Now, the execution plan shows a clustered index seek, as shown in Figure 14, and the query cost is .003522.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-92d77272-36b0-4557-9868-249a0489289e.png\" alt=\"1938-1-92d77272-36b0-4557-9868-249a04892\" \/><\/p>\n<p class=\"caption\">Figure 14<\/p>\n<h3>Adding Non-clustered Indexes to an Indexed View<\/h3>\n<p>Once we&#8217;ve 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.<\/p>\n<p>Let&#8217;s say we want to query our <code>Sales.vCustomerOrders<\/code> view by product name.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">DECLARE @ProductName VARCHAR(50) \nSET @ProductName = 'LL Mountain Frame - Black, 44'\n\nSELECT  CustomerID ,\n        SalesOrderID ,\n        OrderQty ,\n        Name\nFROM    Sales.vCustomerOrders\nWHERE   Name = @ProductName;\n<\/pre>\n<p class=\"caption\">Listing 12<\/p>\n<p>We get a clustered index scan on the view and the query cost is 1.30858.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-09a7841b-c248-44b1-a1c8-b026b1c3c0f7.png\" alt=\"1938-1-09a7841b-c248-44b1-a1c8-b026b1c3c\" \/><\/p>\n<p class=\"caption\">Figure 15<\/p>\n<p>It&#8217;s great that SQL Server is using the clustered index on the view; but a scan isn&#8217;t 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.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE NONCLUSTERED INDEX IX_vCustomerOrders_Name\n  ON Sales.vCustomerOrders(Name);\n<\/pre>\n<p class=\"caption\">Listing 13<\/p>\n<p>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.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-24305b30-d1e7-4e3e-91bb-7b0e63d8c5ec.png\" alt=\"1938-1-24305b30-d1e7-4e3e-91bb-7b0e63d8c\" \/><\/p>\n<p class=\"caption\">Figure 16<\/p>\n<p>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 <code>SELECT<\/code> clause but not included in the non-clustered index.<\/p>\n<p>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.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">DROP INDEX IX_vCustomerOrders_Name ON Sales.vCustomerOrders;\nGO\n\nCREATE NONCLUSTERED INDEX IX_vCustomerOrders_Name\n                          ON Sales.vCustomerOrders(Name)\nINCLUDE (SalesOrderID, CustomerID, OrderQty);\nGO\n<\/pre>\n<p class=\"caption\">Listing 14<\/p>\n<p>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.<\/p>\n<h2>Indexed View Requirements<\/h2>\n<p>An underlying assumption of all previous query examples was use of SQL Server Enterprise Edition. In this edition, SQL Server&#8217;s 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 <code>WITH (NOEXPAND)<\/code> table hint, directly in the <code>FROM<\/code> clause of each query, to force SQL Server to use the indexed view.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">SELECT &lt;column-list&gt; \nFROM Sales.vCustomerOrders CO WITH ( NOEXPAND );\n<\/pre>\n<p class=\"caption\">Listing 15<\/p>\n<p>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 can&#8217;t do. Bear in mind also that if you write queries in stored procedures, your applications, or reports that use <code>WITH (NOEXPAND)<\/code> 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.<\/p>\n<p>As well as this requirement when using views on Standard Edition, there is a lengthy list of &#8216;requirements&#8217; attached to the creation of indexed views, in any SQL Server Edition. We&#8217;ve encountered several already, in the need to create them with the <code>SCHEMABINDING<\/code> option, use fully qualified table references, and use <code>COUNT_ BIG ( *)<\/code> if the view definition contains a <code>GROUP BY<\/code> clause. Another, implied but not discussed directly, is that the indexed view definition can only reference tables, not other views.<\/p>\n<p>The Microsoft documentation (http:\/\/msdn.microsoft.com\/en-us\/library\/ms191432.aspx) provides a full list of limitations and requirements, so I&#8217;ll just briefly summarize some of the more significant here:<\/p>\n<ul>\n<li>Certain database <code>SET<\/code> options have required values if we wish to create any indexed views in that database &#8211; for example, <code>ANSI_NULLS<\/code>, <code>ANSI_PADDING<\/code>, <code>ANSI_WARNINGS<\/code>, <code>ARITHABORT<\/code>, <code>CONCAT_NULL_YIELDS_NULL<\/code>, and<code> QUOTED_IDENTIFIER<\/code> must be <code>ON<\/code>; <code>NUMERIC_ROUNDABORT<\/code> must be <code>OFF<\/code>.<\/li>\n<li>All columns referenced in the view must be deterministic &#8211; that is, they must return the same value each time. As an example, <code>GETDATE( )<\/code> is non-deterministic. <code>DATEADD<\/code> and <code>DATEDIFF<\/code> are deterministic.<\/li>\n<li>We cannot include certain common functions in an indexed view &#8211; <code>COUNT<\/code>, <code>DISTINCT<\/code>, <code>MIN<\/code>, <code>MAX<\/code>, <code>TOP<\/code>, and more.<\/li>\n<li>You can&#8217;t have a self-join or an outer join, an <code>OUTER APPLY<\/code> or a <code>CROSS APPLY<\/code>.<\/li>\n<\/ul>\n<p>If you are having difficulty creating an index on a view, reference the Microsoft documentation, as you&#8217;ve broken one of the &#8216;rules&#8217;.<\/p>\n<h2>Impact of Updating the Base Tables<\/h2>\n<p>A few times, I&#8217;ve mentioned the impact of modifying data, <em>i.e.<\/em> inserting into, updating or deleting from, the base tables of an indexed view, and it&#8217;s now time to discuss this issue in more detail.<\/p>\n<p>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 <code>vSalesSummaryCustomerProduct<\/code> view.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">UPDATE  Sales.SalesOrderDetail\nSET     OrderQty = 5\nWHERE   SalesOrderID = 71803\n        AND ProductID = 917;\n<\/pre>\n<p class=\"caption\">Listing 16<\/p>\n<pre class=\"listing\">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.\nTable '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.\nTable '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.\nTable '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.\nTable '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.\nTable '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.\nTable '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.\nTable '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.\n<\/pre>\n<p class=\"caption\">Figure 17<\/p>\n<p>The execution plan includes many operators, including an update of the <code>vSalesSummaryCustomerProduct<\/code> clustered index.<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/imported\/1938-1-a1690b4a-7e1d-4f2d-af5e-d300352a4f4c.png\" alt=\"1938-1-a1690b4a-7e1d-4f2d-af5e-d300352a4\" \/><\/p>\n<p class=\"caption\">Figure 18<\/p>\n<p>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.<\/p>\n<p>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 view&#8217;s index. Let&#8217;s say we want to insert two records into the <code>SalesOrderHeader<\/code> 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.<\/p>\n<p>A great demo of this is available from Alex Kuznetsov in his article, <a href=\"https:\/\/web.archive.org\/web\/20180409185333\/http:\/\/sqlblog.com\/blogs\/alexander_kuznetsov\/archive\/2009\/06\/02\/be-ready-to-drop-your-indexed-view.aspx\">Be ready to drop your indexed view<\/a>.<\/p>\n<h2>Directly Modifying a View<\/h2>\n<p>Can we, and should we, insert into, update, or delete from an indexed view, directly? Yes, we can, and no, we probably shouldn&#8217;t.<\/p>\n<p>The conditions for directly modifying data in an indexed view are the same as for a regular view, namely:<\/p>\n<ul>\n<li>The columns can only be from one underlying table<\/li>\n<li>The columns can&#8217;t be derived &#8211; so you can&#8217;t have an aggregation, function, or computation<\/li>\n<li>The columns can&#8217;t be affected by <code>GROUP BY<\/code>, <code>HAVING<\/code>, or <code>DISTINCT<\/code><\/li>\n<li>The view can&#8217;t use <code>TOP<\/code> with <code>WITH CHECK OPTION<\/code><\/li>\n<\/ul>\n<p>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 <code>NOT NULL<\/code> columns defined in the table, but are not in the view, your insert will fail, as demonstrated with this example.<\/p>\n<pre class=\"lang:tsql theme:ssms2012\">CREATE VIEW Production.vProductInfoCategory\nWITH SCHEMABINDING\nAS\n    SELECT  PC.Name AS CategoryName ,\n            PSC.Name AS SubcategoryName ,\n            PROD.ProductID ,\n            PROD.ProductNumber ,\n            PROD.Name AS ProductName\n    FROM    Production.Product PROD\n            INNER JOIN Production.ProductSubcategory PSC\n                   ON PSC.ProductSubcategoryID = PROD.ProductSubcategoryID\n            INNER JOIN Production.ProductCategory PC\n                   ON PC.ProductCategoryID = PSC.ProductCategoryID;\nGO\nCREATE UNIQUE CLUSTERED INDEX CIX_vProductInfoCategory\n    ON Production.vProductInfoCategory(ProductID);\nGO\nINSERT  INTO Production.vProductInfoCategory\n        ( ProductNumber ,\n          ProductName\n        )\nVALUES  ( 'VE-C304-XL' ,\n          'Classic Vest, XL'\n        );\nGO\n\n<\/pre>\n<p class=\"caption\">Listing 17<\/p>\n<p>Running this query results in the error in Figure 19.<\/p>\n<pre class=\"listing\">Msg 515, Level 16, State 2, Line 1\nCannot insert the value NULL into column 'SafetyStockLevel', table \n'AdventureWorks2012.Production.Product'; column does not allow nulls. INSERT fails.\n<\/pre>\n<p class=\"caption\">Figure 19<\/p>\n<p>The column <code>SafetyStockLevel<\/code> was not part of the view, and not part of my insert, but it is one of the <code>NOT NULL<\/code> columns defined in the table.<\/p>\n<p>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 don&#8217;t recommend inserting, updating, or deleting in views. It is much easier to deal directly with the base tables.<\/p>\n<h2>Summary<\/h2>\n<p>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.<\/p>\n<p>As with any tool, we must use it with care. In the case of indexed views, it&#8217;s best to assess the volatility of the data in the underlying tables, since every time someone modifies the base table, SQL Server must maintain all indexes, including those on the view. This can affect write performance and it can sometimes lead to increased lock contention and blocking during data modifications.<\/p>\n<p>Learn the benefits and drawbacks of indexed views and test them in your environment. They are another tool to have in your T-SQL toolbox.<\/p>\n<h2>Further Reading<\/h2>\n<ul class=\"reference-list\">\n<li>SQL View Basics <a href=\"https:\/\/www.simple-talk.com\/sql\/learn-sql-server\/sql-view-basics\/\">https:\/\/www.simple-talk.com\/sql\/learn-sql-server\/sql-view-basics\/<\/a><\/li>\n<li>Create Index Views <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ms191432.aspx\">http:\/\/msdn.microsoft.com\/en-us\/library\/ms191432.aspx<\/a><\/li>\n<li>Designing and Implementing Views <a href=\"http:\/\/technet.microsoft.com\/en-us\/library\/ms189918(v=sql.105).aspx\">http:\/\/technet.microsoft.com\/en-us\/library\/ms189918(v=sql.105).aspx <\/a><\/li>\n<li>Improving Performance with SQL Server 2008 Indexed Views <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/dd171921.aspx\">http:\/\/msdn.microsoft.com\/en-us\/library\/dd171921.aspx<\/a><\/li>\n<li>Indexed Views and Statistics <a href=\"http:\/\/www.sqlperformance.com\/2014\/01\/sql-plan\/indexed-views-and-statistics\">http:\/\/www.sqlperformance.com\/2014\/01\/sql-plan\/indexed-views-and-statistics<\/a><\/li>\n<li>Incorrect results with indexed views <a href=\"http:\/\/sqlblog.com\/blogs\/paul_white\/archive\/2013\/02\/06\/incorrect-results-with-indexed-views.aspx\">http:\/\/sqlblog.com\/blogs\/paul_white\/archive\/2013\/02\/06\/incorrect-results-with-indexed-views.aspx<\/a><\/li>\n<li>Caution with Indexed Views <a href=\"http:\/\/blogs.msdn.com\/b\/microsoftbob\/archive\/2009\/08\/15\/cautions-with-indexed-views.aspx\">http:\/\/blogs.msdn.com\/b\/microsoftbob\/archive\/2009\/08\/15\/cautions-with-indexed-views.aspx<\/a><\/li>\n<li>Be ready to drop your indexed view <a href=\"https:\/\/web.archive.org\/web\/20180409185333\/http:\/\/sqlblog.com\/blogs\/alexander_kuznetsov\/archive\/2009\/06\/02\/be-ready-to-drop-your-indexed-view.aspx\">http:\/\/sqlblog.com\/blogs\/alexander_kuznetsov\/archive\/2009\/06\/02\/be-ready-to-drop-your-indexed-view.aspx<\/a><\/li>\n<li>Optimizing Queries Using Materialized Views: A Practical, Scalable Solution <a href=\"http:\/\/db.uwaterloo.ca\/db_seminars\/notes\/larson-paper.pdf\">http:\/\/db.uwaterloo.ca\/db_seminars\/notes\/larson-paper.pdf<\/a><\/li>\n<\/ul>\n<\/div>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: Indexed views in SQL Server<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is an indexed view in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>An indexed view is a view with a unique clustered index physically materialised on it. Unlike a standard view (which executes its underlying query on every access), an indexed view stores the query results on disk &#8211; similar to a table. This makes it fast for read-heavy workloads with complex aggregations, but adds write overhead because INSERT, UPDATE, and DELETE operations on the base tables must also update the indexed view&#8217;s stored data.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What is the difference between a SQL Server indexed view and a materialized view?<\/h3>\n            <div class=\"faq-answer\">\n                <p>They are the same concept under different names. SQL Server calls them indexed views; Oracle, PostgreSQL, and other databases call the same concept materialized views. Both persist view results to disk for fast repeated query access. The implementation details differ: SQL Server requires WITH SCHEMABINDING and a unique clustered index; PostgreSQL uses CREATE MATERIALIZED VIEW directly without requiring a separate index creation step.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. What are the requirements for creating an indexed view in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>(1) The view must be created WITH SCHEMABINDING &#8211; this locks the underlying table schema so columns referenced by the view cannot be dropped or altered without dropping the view first. (2) The view cannot use *, subqueries, derived tables, row-set functions, UNION, OUTER JOIN, or non-deterministic functions. (3) The first index on the view must be a unique clustered index. (4) For the optimiser to automatically use the indexed view in Enterprise Edition, the view definition and the query must reference the same base tables. In Standard Edition, use NOEXPAND hint to force usage.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. When should I use an indexed view in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Indexed views are most valuable when: (1) the view is queried frequently and the underlying query is expensive (multi-table join with aggregations); (2) the base tables are updated infrequently relative to the read frequency (otherwise write overhead outweighs read gains); and (3) you are using Enterprise Edition and want the optimiser to consider the indexed view automatically. Avoid indexed views for views on OLTP tables with high write rates &#8211; the write overhead on the indexed view can become a bottleneck.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>SQL Server indexed views persist query results as a physical clustered index, making complex aggregations and joins fast for repeated access. Learn the WITH SCHEMABINDING requirement, how to create a clustered index on a view, and edition requirements.&hellip;<\/p>\n","protected":false},"author":97688,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":true,"footnotes":""},"categories":[143525],"tags":[5924,4242,5923,5922,4149,4150,5134,4151,4183,5921],"coauthors":[11322],"class_list":["post-1760","post","type-post","status-publish","format-standard","hentry","category-learn","tag-base-tables","tag-basics","tag-clustered-index","tag-indexed-views","tag-learn-sql-server","tag-sql","tag-sql-prompt","tag-sql-server","tag-t-sql","tag-views"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1760","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/97688"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=1760"}],"version-history":[{"count":9,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1760\/revisions"}],"predecessor-version":[{"id":109953,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/1760\/revisions\/109953"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=1760"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=1760"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=1760"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=1760"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}