MS SQL Server
Views
A view is a saved SELECT statement with a name. From the outside it looks and behaves exactly like a table — you can SELECT from it, filter it with WHERE, join it to other tables, and grant or deny access to it independently from the underlying tables. On the inside it is a query definition stored in the database catalog that runs every time the view is referenced. Views serve three distinct purposes: they simplify complex queries by hiding JOIN and aggregation logic behind a clean name, they enforce security by exposing only the columns and rows a user is permitted to see, and they provide a stable interface that insulates applications from underlying schema changes. This lesson covers creating, modifying, and managing views in DataplexaStore — including the restrictions that apply and the important distinction between regular and indexed views.
Creating a View
The CREATE VIEW statement names the view and defines the SELECT query it wraps. The SELECT can be as simple or complex as needed — joins, aggregations, expressions, subqueries, and window functions are all valid. Once created the view appears in the database alongside tables and can be queried exactly like one. The convention for naming views is to prefix them with vw_ so they are immediately distinguishable from base tables in any query or schema browser. Views do not store data — every query against a view executes the underlying SELECT fresh, against the current state of the base tables.
-- Create a view that joins Users and Orders for a clean customer order summary
USE DataplexaStore;
CREATE VIEW vw_CustomerOrders AS
SELECT
u.UserID,
u.FullName,
u.Email,
u.Country,
u.MembershipTier,
o.OrderID,
o.OrderDate,
o.Status,
o.TotalAmount
FROM Users u
JOIN Orders o ON u.UserID = o.UserID;
-- Query the view exactly like a table
SELECT * FROM vw_CustomerOrders ORDER BY TotalAmount DESC;
-- Filter the view — WHERE applies on top of the view's own query
SELECT FullName, OrderID, TotalAmount, Status
FROM vw_CustomerOrders
WHERE Status = 'delivered'
ORDER BY TotalAmount DESC;UserID FullName Email Country MembershipTier OrderID OrderDate Status TotalAmount
────── ──────────── ───────────────────────── ─────────── ────────────── ─────── ─────────────────────────── ───────── ───────────
2 Bob Williams bob.williams@example.com UK Premium 2 2024-03-18 14:45:00.0000000 delivered 512.92
3 Clara Davis clara.davis@example.com Canada VIP 3 2024-03-20 09:11:00.0000000 shipped 159.91
4 David Kim david.kim@example.com South Korea Standard 5 2024-03-27 11:05:00.0000000 shipped 42.98
2 Bob Williams bob.williams@example.com UK Premium 6 2024-03-28 14:22:31.0000000 delivered 80.99
-- Delivered orders only:
FullName OrderID TotalAmount Status
──────────── ─────── ─────────── ─────────
Bob Williams 2 512.92 delivered
Bob Williams 6 80.99 delivered
- The view hides the JOIN — any query against vw_CustomerOrders reads clean flat output without needing to know that Users and Orders are separate tables with a UserID relationship
- WHERE Status = 'delivered' is applied after the view's query runs — SQL Server's optimiser often pushes predicates down into the view's query for efficiency, but the logical behaviour is always filter-after-view
- Bob Williams appears twice because he has two orders — the view returns one row per order, not one row per user; this is the correct behaviour for an order-centric view
Aggregated Views
Views can contain GROUP BY and aggregate functions, turning complex summarisation logic into a reusable named object. An aggregated view pre-defines the grouping and the metrics — any query against it can then filter, sort, and join on those pre-computed labels. This is particularly useful for reporting layers where multiple dashboards and reports need the same summary figures but should not each contain a copy of the same GROUP BY logic.
-- Aggregated view — product sales summary
CREATE VIEW vw_ProductSales AS
SELECT
p.ProductID,
p.ProductName,
p.Category,
p.Price,
p.StockQty,
COUNT(oi.OrderItemID) AS times_ordered,
ISNULL(SUM(oi.Quantity), 0) AS units_sold,
ISNULL(SUM(oi.Quantity * oi.UnitPrice), 0) AS revenue_generated,
AVG(r.Rating) AS avg_rating,
COUNT(r.ReviewID) AS review_count
FROM Products p
LEFT JOIN OrderItems oi ON p.ProductID = oi.ProductID
LEFT JOIN Reviews r ON p.ProductID = r.ProductID
GROUP BY p.ProductID, p.ProductName, p.Category, p.Price, p.StockQty;
-- Query the aggregated view
SELECT
ProductName,
Category,
units_sold,
revenue_generated,
avg_rating,
review_count
FROM vw_ProductSales
ORDER BY revenue_generated DESC;──────────────────── ─────────── ────────── ───────────────── ────────── ────────────
Monitor 27 inch Electronics 2 719.98 5 1
Notebook A5 Stationery 8 103.92 5 1
Mechanical Keyboard Electronics 2 80.99 4 1
Desk Lamp Home Office 2 69.98 NULL 0
USB-C Hub Electronics 1 44.99 NULL 0
Wireless Mouse Electronics 1 29.69 5 1
Webcam HD Electronics 0 0.00 NULL 0
Standing Desk Home Office 0 0.00 NULL 0
Ergonomic Chair Home Office 0 0.00 NULL 0
Ballpoint Pen Set Stationery 0 0.00 NULL 0
- LEFT JOIN on both OrderItems and Reviews means products with no sales or no reviews still appear — ISNULL converts the NULL sums to 0 for clean numeric output, while avg_rating correctly stays NULL (not 0) for unreviewed products since a 0 rating is a real rating and NULL means "no data"
- Monitor 27 inch generates the highest revenue (719.98) despite being sold in fewer units (2) than Notebook A5 (8 units, 103.92 revenue) — a view that shows both metrics makes this trade-off immediately visible without any ad-hoc calculation
- Once this view exists every report, dashboard query, and ad-hoc analysis draws from the same definition — if the calculation changes (for example adding a discount column) it is fixed once in the view and all consumers see the corrected figure automatically
Modifying and Dropping Views
An existing view is modified with ALTER VIEW, which replaces the entire SELECT definition. There is no way to alter just one column or one join in a view — the full SELECT must be rewritten. The safer pattern in deployment scripts is CREATE OR ALTER VIEW, available since SQL Server 2016, which creates the view if it does not exist or replaces it if it does — making the script safely re-runnable without needing to check for prior existence. DROP VIEW removes the view definition from the catalog but leaves the underlying base tables completely untouched.
-- ALTER VIEW — add the JoinDate column to vw_CustomerOrders
ALTER VIEW vw_CustomerOrders AS
SELECT
u.UserID,
u.FullName,
u.Email,
u.Country,
u.MembershipTier,
u.JoinDate, -- newly added column
o.OrderID,
o.OrderDate,
o.Status,
o.TotalAmount
FROM Users u
JOIN Orders o ON u.UserID = o.UserID;
-- CREATE OR ALTER — idempotent, safe to run multiple times
CREATE OR ALTER VIEW vw_UnorderedProducts AS
SELECT
ProductID,
ProductName,
Category,
Price,
StockQty
FROM Products
WHERE ProductID NOT IN (SELECT DISTINCT ProductID FROM OrderItems);
-- Inspect view definitions stored in the catalog
SELECT
v.name AS view_name,
v.create_date,
v.modify_date,
m.definition AS view_sql
FROM sys.views v
JOIN sys.sql_modules m ON v.object_id = m.object_id
WHERE v.name LIKE 'vw_%'
ORDER BY v.name;
-- Query the new view
SELECT * FROM vw_UnorderedProducts ORDER BY Category, Price;view_name create_date modify_date view_sql
────────────────────── ─────────────────────── ─────────────────────── ────────────────────
vw_CustomerOrders 2024-03-28 15:00:00.000 2024-03-28 15:10:00.000 CREATE VIEW vw_Cu...
vw_ProductSales 2024-03-28 15:05:00.000 2024-03-28 15:05:00.000 CREATE VIEW vw_Pr...
vw_UnorderedProducts 2024-03-28 15:10:00.000 2024-03-28 15:10:00.000 CREATE OR ALTER V...
-- Unordered products:
ProductID ProductName Category Price StockQty
───────── ───────────────── ─────────── ────── ────────
4 Webcam HD Electronics 71.99 18
6 Standing Desk Home Office 367.49 8
7 Ergonomic Chair Home Office 262.49 12
9 Ballpoint Pen Set Stationery 8.99 0
- ALTER VIEW rewrites the entire view definition — the old definition is discarded completely; the modify_date in sys.views reflects when it was last altered
- CREATE OR ALTER is the deployment-safe form — running it twice is harmless, unlike CREATE VIEW which fails on the second run with "object already exists"
- sys.sql_modules stores the full text of every view, stored procedure, and function definition — querying it is how you retrieve a view's SQL without using SSMS's GUI
View Restrictions and WITH SCHEMABINDING
Standard views have several restrictions that catch developers by surprise. A view cannot use ORDER BY unless it also uses TOP, OFFSET/FETCH, or FOR XML — ORDER BY in a view without one of these is a syntax error because the relational model defines a table as an unordered set, and a view is a virtual table. The order of rows returned from a view is controlled by the ORDER BY in the query that selects from the view, not inside the view itself. WITH SCHEMABINDING is an option that binds the view to the schema of the underlying tables — it prevents any ALTER TABLE or DROP TABLE on the base tables that would break the view, protecting the view definition from silent breakage. Schemabinding is also required before a view can have an index built on it.
-- WITH SCHEMABINDING — protect the view from base table changes
CREATE OR ALTER VIEW vw_ActiveInventory
WITH SCHEMABINDING -- binds view to base table schema
AS
SELECT
p.ProductID,
p.ProductName,
p.Category,
p.Price,
p.StockQty
FROM dbo.Products p -- schema prefix required with SCHEMABINDING
WHERE p.StockQty > 0;
-- Verify schemabinding is set
SELECT
name,
is_schema_bound
FROM sys.views
WHERE name = 'vw_ActiveInventory';
-- Attempt to drop a column used by a schemabinding view — must fail
ALTER TABLE Products DROP COLUMN StockQty;─────────────────── ───────────────
vw_ActiveInventory 1
-- Attempt to drop StockQty:
Msg 5074, Level 16, State 1
The object 'vw_ActiveInventory' is dependent on column 'StockQty'.
Msg 4922, Level 16, State 9
ALTER TABLE DROP COLUMN StockQty failed because one or more objects access this column.
- is_schema_bound = 1 confirms the binding is active — SQL Server now tracks the dependency between the view and every column and table it references
- The DROP COLUMN attempt was blocked — without schemabinding the column would have been dropped silently, leaving the view broken and returning an error only when someone next queried it; schemabinding catches this at ALTER time instead
- The
dbo.schema prefix on table names is mandatory with SCHEMABINDING — SQL Server requires two-part names (schema.table) to resolve dependencies unambiguously
Using Views for Security — Column and Row Filtering
One of the most practical uses of views is access control. By granting a user SELECT permission on a view rather than the base table you can precisely control which columns they see (column-level security) and which rows they see (row-level security). A view that excludes an Email column, for example, can be granted to a reporting role without exposing personal contact data. A view that filters with WHERE Country = 'UK' can be granted to a regional team who should only ever see their own region's data. The underlying table is never directly accessible to those users — they only see what the view exposes.
-- Security view — expose order data without revealing personal user details
CREATE OR ALTER VIEW vw_OrderReport AS
SELECT
o.OrderID,
o.OrderDate,
o.Status,
o.TotalAmount,
u.Country,
u.MembershipTier -- tier visible for segmentation
-- u.Email, u.FullName intentionally excluded for privacy
FROM Orders o
JOIN Users u ON o.UserID = u.UserID;
-- Row-filtering view — only show UK orders (regional access control)
CREATE OR ALTER VIEW vw_UKOrders AS
SELECT
o.OrderID,
o.OrderDate,
o.Status,
o.TotalAmount,
u.FullName,
u.MembershipTier
FROM Orders o
JOIN Users u ON o.UserID = u.UserID
WHERE u.Country = 'UK'; -- row filter baked into the view definition
SELECT * FROM vw_OrderReport ORDER BY TotalAmount DESC;
SELECT * FROM vw_UKOrders ORDER BY OrderDate;OrderID OrderDate Status TotalAmount Country MembershipTier
─────── ─────────────────────────── ───────── ─────────── ─────── ──────────────
2 2024-03-18 14:45:00.0000000 delivered 512.92 UK Premium
3 2024-03-20 09:11:00.0000000 shipped 159.91 Canada VIP
5 2024-03-27 11:05:00.0000000 shipped 42.98 South Korea Standard
6 2024-03-28 14:22:31.0000000 delivered 80.99 UK Premium
-- UK orders only:
OrderID OrderDate Status TotalAmount FullName MembershipTier
─────── ─────────────────────────── ───────── ─────────── ──────────── ──────────────
2 2024-03-18 14:45:00.0000000 delivered 512.92 Bob Williams Premium
6 2024-03-28 14:22:31.0000000 delivered 80.99 Bob Williams Premium
- vw_OrderReport exposes no Email or FullName — a reporting user granted access to this view cannot retrieve personal contact data even indirectly, because the underlying Users table is not directly accessible to them
- vw_UKOrders enforces the row filter at the view level — a UK regional analyst granted access to this view literally cannot see orders from Canada or South Korea, even by adding their own WHERE clause, because those rows are excluded before the view result is returned
- Granting
GRANT SELECT ON vw_UKOrders TO [regional_analyst_role]without granting any access to the base tables is the correct security pattern — the view is the only access point
Summary Table
| Feature | Behaviour | Key Point |
|---|---|---|
| CREATE VIEW | Saves a SELECT as a named object | No data stored — query runs fresh every time |
| ALTER VIEW | Replaces the entire SELECT definition | Use CREATE OR ALTER for idempotent scripts |
| ORDER BY in a view | Not allowed without TOP or OFFSET | Sort in the query that selects from the view |
| WITH SCHEMABINDING | Locks view to base table schema | Required for indexed views — prevents silent breakage |
| Column filtering | Exclude sensitive columns from SELECT | Grant view access — deny base table access |
| Row filtering | WHERE in view definition limits visible rows | Users cannot bypass the view's own WHERE |
Practice Questions
Practice 1. Does a view store data? Explain what happens when a view is queried.
Practice 2. Why is ORDER BY not allowed inside a view definition without TOP or OFFSET?
Practice 3. What does WITH SCHEMABINDING protect against?
Practice 4. How can a view enforce row-level security so that a user cannot see data outside their permitted scope?
Practice 5. What is the advantage of CREATE OR ALTER VIEW over DROP VIEW followed by CREATE VIEW?
Quiz
Quiz 1. A developer adds a new column to a base table. A view that uses SELECT * from that table — what happens to the view's output?
Quiz 2. Which system view stores the SQL definition of a view?
Quiz 3. What happens to the base tables when DROP VIEW is executed?
Quiz 4. WITH SCHEMABINDING requires table names in the view to be written in which format?
Quiz 5. A user is granted SELECT on vw_UKOrders but not on the underlying Users or Orders tables. Can they retrieve non-UK order data?
Next up - Indexes - Understand how SQL Server uses indexes to find rows instantly and learn which columns to index, when indexes hurt as much as they help, and how to keep them healthy.