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.
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;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.
Every column needs a name
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;Msg 4511, Level 16, State 1, Procedure OrderTotals, Line 3 Create View or Function failed because no column name was specified for column 5.
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;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
CREATE OR ALTER VIEW dbo.DearProductsASSELECT Name, PriceFROM dbo.ProductsWHERE Price > 20ORDER BY Price DESC;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.
SELECT * FROM dbo.DearProducts ORDER BY Price DESC.
The SELECT * trap
SELECT * is expanded once, at that moment. Columns added to the table later do not appear in the view:
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 oneSupplierId Name
---------- ---------------
1 Northern Timber
2 Fixings Direct
SupplierId Name Phone
---------- --------------- -----
1 Northern Timber NULL
2 Fixings Direct NULLsp_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
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);ProductId Name Price Stock
--------- ---------------- ----- -----
2 Cordless drill 89.00 12
9 Wall plugs (100) 4.50 100UPDATE dbo.CheapProducts SET Price = 10.50 WHERE ProductId = 3; -- Tape measure, was 9.75 SELECT ProductId, Name, PriceFROM dbo.CheapProductsORDER BY Price DESC;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.10WITH CHECK OPTION: SQL Server then rejects any change through the view that would move a row out of it.
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;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.50UPDATE 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;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 ToolsSCHEMABINDING: protect what the view depends on
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:
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 againMsg 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.
Views for security
CREATE OR ALTER VIEW dbo.CustomerDirectoryASSELECT CustomerId, FirstName, LastName, City -- no Email or PhoneFROM dbo.Customers;GOSELECT * FROM dbo.CustomerDirectory WHERE City = N'York';CustomerId FirstName LastName City
---------- --------- -------- ----
2 Ben Carter York
6 Farid Khan YorkGRANT 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.
Show a hint
Show the solution
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;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.