<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. SQL
  3. Programmability
All lessons in this course

Lesson 36 of 54 16 minAdvanced

Views

Save a query under a name and use it like a table: creating views, naming their columns, why ORDER BY is not allowed, the SELECT * trap, updating through a view, WITH CHECK OPTION, SCHEMABINDING and views for security.

You have written the same join between Products and Categories many times. A view saves a SELECT under a name. After that you can query the view as if it were a table, and SQL Server runs the saved SELECT for you.

Query.sql
CREATE OR ALTER VIEW dbo.ProductListASSELECT p.ProductId, p.Name, c.Name AS Category, p.Price, p.StockFROM dbo.Products AS pLEFT JOIN dbo.Categories AS c ON c.CategoryId = p.CategoryId;GOSELECT Name, Category, PriceFROM dbo.ProductListWHERE Category = N'Tools'ORDER BY Price DESC;
Output
Name              Category  Price
----------------  --------  -----
Cordless drill    Tools     89.00
Claw hammer       Tools     18.50
Tape measure 8 m  Tools      9.75

CREATE OR ALTER creates the view, or replaces it if it already exists, so you can run the script again. The GO line ends the batch: a CREATE VIEW must be in a batch of its own, which the next lesson explains.

A view stores no data, only its SELECT. Every query against it runs that SELECT again, so a view always shows the current rows. SQL Server merges the view’s SELECT into your query before it plans it, so a view is usually no slower than writing the join yourself.

Every column needs a name

A view is used like a table, so each of its columns needs a name. An expression without an alias stops the view being created:

Query.sql
CREATE OR ALTER VIEW dbo.OrderTotalsASSELECT o.OrderId, o.CustomerId, o.OrderDate, o.Status,       SUM(i.Quantity * i.UnitPrice)FROM dbo.Orders AS oJOIN dbo.OrderItems AS i ON i.OrderId = o.OrderIdGROUP BY o.OrderId, o.CustomerId, o.OrderDate, o.Status;
Output
Msg 4511, Level 16, State 1, Procedure OrderTotals, Line 3
Create View or Function failed because no column name was specified for column 5.

Name the expressions and the view works. Views can then be joined and grouped like any table. Here one view answers a question that would otherwise need a join inside a join:

Query.sql
CREATE OR ALTER VIEW dbo.OrderTotalsASSELECT o.OrderId, o.CustomerId, o.OrderDate, o.Status,       COUNT(*) AS Lines,       SUM(i.Quantity * i.UnitPrice) AS TotalFROM dbo.Orders AS oJOIN dbo.OrderItems AS i ON i.OrderId = o.OrderIdGROUP BY o.OrderId, o.CustomerId, o.OrderDate, o.Status;GOSELECT c.FirstName, COUNT(*) AS Orders, SUM(t.Total) AS SpentFROM dbo.OrderTotals AS tJOIN dbo.Customers AS c ON c.CustomerId = t.CustomerIdWHERE t.Status <> 'Cancelled'GROUP BY c.CustomerId, c.FirstNameORDER BY Spent DESC;
Output
FirstName  Orders   Spent
---------  ------  ------
Dev             2  723.50
Farid           2  211.85
Asha            2  150.80
Ben             2  131.00
Zoë             1  102.50

No ORDER BY inside a view

A view is a set of rows, like a table, and a set has no order. SQL Server refuses an ORDER BY in a view:

Query.sql
CREATE OR ALTER VIEW dbo.DearProductsASSELECT Name, PriceFROM dbo.ProductsWHERE Price > 20ORDER BY Price DESC;
Output
Msg 1033, Level 15, State 1, Procedure DearProducts, Line 6
The ORDER BY clause is invalid in views, inline functions, derived tables, subqueries, and common table expressions, unless TOP, OFFSET or FOR XML is also specified.

Sort in the query that reads the view: SELECT * FROM dbo.DearProducts ORDER BY Price DESC.

The SELECT * trap

When a view is created, SQL Server records its column list. SELECT * is expanded once, at that moment. Columns added to the table later do not appear in the view:

Query.sql
DROP TABLE IF EXISTS dbo.Suppliers;CREATE TABLE dbo.Suppliers (    SupplierId int          NOT NULL PRIMARY KEY,    Name       nvarchar(60) NOT NULL);INSERT INTO dbo.Suppliers VALUES (1, N'Northern Timber'), (2, N'Fixings Direct');GOCREATE OR ALTER VIEW dbo.SupplierList AS SELECT * FROM dbo.Suppliers;GOALTER TABLE dbo.Suppliers ADD Phone varchar(20) NULL;GOSELECT * FROM dbo.SupplierList;        -- no Phone column EXEC sp_refreshview N'dbo.SupplierList';SELECT * FROM dbo.SupplierList;        -- now it has one
Output
SupplierId  Name
----------  ---------------
         1  Northern Timber
         2  Fixings Direct

SupplierId  Name             Phone
----------  ---------------  -----
         1  Northern Timber  NULL
         2  Fixings Direct   NULL

sp_refreshview makes SQL Server read the table’s columns again. It is better not to need it: list the columns in a view, so that the view only changes when you change it.

Changing data through a view

You can INSERT, UPDATE and DELETE through a view when SQL Server can trace every changed column back to one base table. The view’s WHERE still applies, so an UPDATE through a view only reaches the rows the view shows:

Query.sql
CREATE OR ALTER VIEW dbo.CheapProductsASSELECT ProductId, Name, Price, StockFROM dbo.ProductsWHERE Price < 10.00;GOUPDATE dbo.CheapProducts SET Stock = Stock + 100 WHERE ProductId = 9;  -- Wall plugs: in the viewUPDATE dbo.CheapProducts SET Stock = Stock + 100 WHERE ProductId = 2;  -- Cordless drill: not in it SELECT ProductId, Name, Price, StockFROM dbo.ProductsWHERE ProductId IN (2, 9);
Output
ProductId  Name              Price  Stock
---------  ----------------  -----  -----
        2  Cordless drill    89.00     12
        9  Wall plugs (100)   4.50    100

The second UPDATE changed nothing: the drill costs 89.00, so it is not in the view. Now raise the price of something that is in the view, past the view’s own limit:

Query.sql
UPDATE dbo.CheapProducts SET Price = 10.50 WHERE ProductId = 3;  -- Tape measure, was 9.75 SELECT ProductId, Name, PriceFROM dbo.CheapProductsORDER BY Price DESC;
Output
ProductId  Name              Price
---------  ----------------  -----
        4  Cement 25 kg       7.20
        6  Timber 2x4 2.4 m   6.80
       12  Safety goggles     6.80
        9  Wall plugs (100)   4.50
        5  Sand 25 kg         4.10

The update worked, and the tape measure disappeared from the view that changed it. Sometimes that is what you want. When it is not, add WITH CHECK OPTION: SQL Server then rejects any change through the view that would move a row out of it.

Query.sql
CREATE OR ALTER VIEW dbo.CheapProductsASSELECT ProductId, Name, Price, StockFROM dbo.ProductsWHERE Price < 10.00WITH CHECK OPTION;GOUPDATE dbo.CheapProducts SET Price = 10.50 WHERE ProductId = 4;  -- Cement: would leave the viewUPDATE dbo.CheapProducts SET Price = 7.50  WHERE ProductId = 4;  -- still under 10.00: allowed SELECT ProductId, Name, Price FROM dbo.CheapProducts WHERE ProductId = 4;
Output
Msg 550, Level 16, State 1, Line 1
The attempted insert or update failed because the target view either specifies WITH CHECK OPTION or spans a view that specifies WITH CHECK OPTION and one or more rows resulting from the operation did not qualify under the CHECK OPTION constraint.
The statement has been terminated.

ProductId  Name          Price
---------  ------------  -----
        4  Cement 25 kg   7.50
Interactive

Updating through a view

New price
CREATE OR ALTER VIEW dbo.CheapProducts AS
SELECT ProductId, Name, Price, Stock
FROM dbo.Products
WHERE Price < 10.00;
GO
UPDATE dbo.CheapProducts
SET Price = 10.50
WHERE ProductId = 4;
(1 row affected)
dbo.CheapProducts after
ProductIdNamePrice
14Cement 25 kg10.50
23Tape measure 8 m9.75
36Timber 2x4 2.4 m6.80
412Safety goggles6.80
59Wall plugs (100)4.50
65Sand 25 kg4.10
The row in dbo.Products
ProductIdNamePrice
14Cement 25 kg10.50
The update worked, and Cement 25 kg has now left the view that changed it (struck through). Tick WITH CHECK OPTION to stop changes like this.

A change that touches columns from two tables cannot go through a view, and neither can a change to a column that is calculated or comes from GROUP BY. SQL Server finds these problems when it compiles the batch, so the whole batch is rejected. That is why each statement below has its own batch:

Query.sql
UPDATE dbo.ProductListSET Name = N'Claw hammer 16 oz', Category = N'Hand tools'WHERE ProductId = 1;GOUPDATE dbo.OrderTotals SET Total = 0 WHERE OrderId = 1;GOUPDATE dbo.ProductList                 -- one table only: allowedSET Name = N'Claw hammer 16 oz'WHERE ProductId = 1; SELECT ProductId, Name, Category FROM dbo.ProductList WHERE ProductId = 1;
Output
Msg 4405, Level 16, State 1, Line 1
View or function 'dbo.ProductList' is not updatable because the modification affects multiple base tables.
Msg 4406, Level 16, State 1, Line 1
Update or insert of view or function 'dbo.OrderTotals' failed because it contains a derived or constant field.

ProductId  Name               Category
---------  -----------------  --------
        1  Claw hammer 16 oz  Tools

SCHEMABINDING: protect what the view depends on

Normally nothing stops someone changing or dropping a table that a view reads, and the view then fails when it is next used. WITH SCHEMABINDING ties the view to the columns it uses, so they cannot be changed while the view exists. It needs two-part names (dbo.Products) and an explicit column list:

Query.sql
CREATE OR ALTER VIEW dbo.PriceListWITH SCHEMABINDINGASSELECT ProductId, Name, PriceFROM dbo.Products;GOALTER TABLE dbo.Products ALTER COLUMN Name nvarchar(80) NOT NULL;GODROP VIEW dbo.PriceList;   -- so the BuildMart reset script can drop Products again
Output
Msg 5074, Level 16, State 1, Line 1
The object 'PriceList' is dependent on column 'Name'.
Msg 4922, Level 16, State 9, Line 1
ALTER TABLE ALTER COLUMN Name failed because one or more objects access this column.

Schema binding is also required before a view can be indexed: a unique clustered index on a view stores its result, like a table, and SQL Server keeps it up to date on every change. That is a performance tool for the Indexes module.

Views for security

A view can show some columns and rows of a table and hide the rest. Give a team permission on the view, not the table, and they can only ever see what the view shows:

Query.sql
CREATE OR ALTER VIEW dbo.CustomerDirectoryASSELECT CustomerId, FirstName, LastName, City   -- no Email or PhoneFROM dbo.Customers;GOSELECT * FROM dbo.CustomerDirectory WHERE City = N'York';
Output
CustomerId  FirstName  LastName  City
----------  ---------  --------  ----
         2  Ben        Carter    York
         6  Farid      Khan      York

With GRANT SELECT ON dbo.CustomerDirectory TO SalesTeam; the SalesTeam role can read the directory without any permission on dbo.Customers. Logins, roles and permissions are covered in the Master level module.

Quick check

What does a view store?

Quick check

A view shows products with Price < 10, without CHECK OPTION. You set a product’s price to 15 through the view. What happens?

Practice

Create a view dbo.CategoryStock with one row per category that has products: the category Name as Category, the number of products as Products, and the value of the stock (Price × Stock, added up) as StockValue. Then use it to list the categories whose stock is worth more than 1,000, the most valuable first.

Show a hint

GROUP BY inside the view, with an alias for each expression. The ORDER BY and the 1,000 filter go in the query that reads the view.

Show the solution
Solution.sql
CREATE OR ALTER VIEW dbo.CategoryStockASSELECT c.Name AS Category,       COUNT(*) AS Products,       SUM(p.Price * p.Stock) AS StockValueFROM dbo.Products AS pJOIN dbo.Categories AS c ON c.CategoryId = p.CategoryIdGROUP BY c.Name;GOSELECT Category, Products, StockValueFROM dbo.CategoryStockWHERE StockValue > 1000ORDER BY StockValue DESC;
Output
Category  Products  StockValue
--------  --------  ----------
Timber           2     6760.00
Building         2     3152.00
Tools            3     2490.50
Fixings          2     2235.00
Paint            2     1725.00

Remember

  • A view is a saved SELECT that you query like a table; it stores no rows and always shows current data.
  • Use CREATE OR ALTER VIEW, in a batch of its own, and give every column a name.
  • ORDER BY is not allowed in a view: sort in the outer query. TOP 100 PERCENT … ORDER BY does not order anything.
  • SELECT * in a view is expanded once, at creation; list the columns instead.
  • You can change data through a view when the change touches one base table; the view’s WHERE limits which rows are reached.
  • WITH CHECK OPTION rejects changes that would move a row out of the view (Msg 550).
  • WITH SCHEMABINDING stops the base columns being changed while the view exists, and is required for indexed views.
  • Views can hide columns and rows: grant permission on the view instead of the table.
0 of 54 lessons completed