Expert: design, speed and engines · Indexes, query plans and views · topic 51 of 62 · 30 min
Views and materialized views
Explanation
A view is a saved query
Some queries are written again and again: "delivered orders with their totals" appears in the sales report, the finance export and the dashboard. A view gives that query a name. After CREATE VIEW, you can SELECT from it, join it and filter it as if it were a table.
CREATE VIEW delivered_order_totals AS
SELECT o.id AS order_id, c.name AS customer, o.order_date,
SUM(oi.qty * oi.price) AS total
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'Delivered'
GROUP BY o.id, c.name, o.order_date;
SELECT customer, COUNT(*) AS orders, SUM(total) AS spent
FROM delivered_order_totals
GROUP BY customer
ORDER BY spent DESC;
-- Ravi Kumar | 2 | 6497
-- Asha Verma | 2 | 4028Because a view stores only the query, its results are always current, and it uses no extra space. It is also no faster than the query inside it: the database runs that query (usually merged into yours) every time. To speed up a view, index its underlying tables.
Views for security and stable interfaces
A view can expose some columns and hide others. A support team may see customers' names and cities but not emails or phone numbers. In PostgreSQL, SQL Server and MySQL you GRANT SELECT on the view and give no access to the table itself. Views also act as a stable interface: if you later split a table, a view with the old name and columns keeps old reports working.
CREATE VIEW customers_public AS
SELECT id, name, city FROM customers;
SELECT email FROM customers_public;
-- Error: no such column: emailCan you change data through a view?
Some engines let you INSERT, UPDATE or DELETE through simple views. In PostgreSQL a view is automatically updatable when it reads from one table with no GROUP BY, DISTINCT, aggregates, UNION or LIMIT; MySQL has similar rules. WITH CHECK OPTION stops changes that would make a row vanish from the view. Views with joins and totals, like delivered_order_totals, are read-only.
In SQLite every view is read-only. UPDATE customers_public SET city = 'Nagpur' fails with "cannot modify customers_public because it is a view". An INSTEAD OF trigger can redirect such changes to the real table; module 11 covers triggers.
Materialized views: storing the result
Sometimes the query behind a view is heavy, such as a year of sales grouped by city and category, and nobody needs it to the second. A materialized view runs the query once and stores the rows. Reading it is as fast as reading a small table, and you can index it. The price: it goes stale until refreshed.
-- PostgreSQL
CREATE MATERIALIZED VIEW category_sales AS
SELECT p.category, SUM(oi.qty * oi.price) AS revenue
FROM order_items oi JOIN products p ON p.id = oi.product_id
GROUP BY p.category;
REFRESH MATERIALIZED VIEW category_sales; -- rebuilds; readers wait
REFRESH MATERIALIZED VIEW CONCURRENTLY category_sales; -- readers keep reading; needs a UNIQUE index on the view- SQL Server: an indexed view. Create the view WITH SCHEMABINDING, then a UNIQUE CLUSTERED INDEX on it; SQL Server then keeps the stored result up to date on every change.
- Oracle: materialized views with several refresh options.
- MySQL and SQLite: no materialized views. Use a summary table.
Emulating one in SQLite: a summary table
CREATE TABLE category_sales (category TEXT PRIMARY KEY, revenue INTEGER NOT NULL);
-- Refresh (run on a schedule, inside one transaction):
BEGIN;
DELETE FROM category_sales;
INSERT INTO category_sales (category, revenue)
SELECT p.category, SUM(oi.qty * oi.price)
FROM order_items oi
JOIN orders o ON o.id = oi.order_id
JOIN products p ON p.id = oi.product_id
WHERE o.status <> 'Cancelled'
GROUP BY p.category;
COMMIT;Common mistakes
- Expecting a plain view to be faster than its query. Fix: index the base tables, or use a materialized view or summary table.
- Stacking views on views on views until nobody can read the plan. Fix: keep views one or two levels deep.
- Forgetting to refresh a materialized view or summary table. Fix: schedule the refresh and show "updated at" on the report.
- Using SELECT * inside a view. Fix: list columns, so the view does not change shape when the table does.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Views and materialized views · 1/7
A view is a saved query with a name. You select from it like a table.
Prefer a video by a teacher? Find free videos on “sql views vs materialized views explained postgresql refresh materialized view” on YouTube ↗
In real life: A hospital reporting view
A hospital's billing tables hold patient names, phone numbers and diagnoses. The finance team needs daily revenue per department, but has no reason to see medical details. The database team creates a view that joins bills to departments and exposes only the date, department and amount, and grants the finance role SELECT on that view alone. A heavy monthly summary is kept in a materialized view refreshed every night.
CREATE VIEW dept_revenue AS
SELECT b.bill_date, d.name AS department, SUM(b.amount_paise) AS revenue_paise
FROM bills b
JOIN departments d ON d.id = b.department_id
GROUP BY b.bill_date, d.name;
-- PostgreSQL: GRANT SELECT ON dept_revenue TO finance;Practice (0/4 done)
Create a view named delivered_order_totals with the columns order_id, customer (the customer's name), order_date and total (the sum of qty * price), for orders with status 'Delivered'. Then query the view: return order_id, customer and total, from the largest total to the smallest.
The support team must not see emails. Create a view customers_public with only id, name and city from customers. Then, using the view, return id, name and city of customers in 'Pune'.
Emulate a materialized view. 1) Create table category_sales (category TEXT PRIMARY KEY, revenue INTEGER NOT NULL) and fill it with INSERT ... SELECT: revenue (sum of qty * price) per product category, excluding Cancelled orders. 2) A new order arrives: INSERT INTO orders VALUES (109, 6, '2025-04-05', 'Placed'); and INSERT INTO order_items VALUES (109, 8, 2, 1299);. 3) Refresh: DELETE everything from category_sales and run the same INSERT ... SELECT again. 4) Return category and revenue, sorted by category.
Which PostgreSQL view can you UPDATE directly without triggers?
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Reading query plansIndexes, query plans and views · same module
- Composite, covering, unique and partial indexesIndexes, query plans and views · same module
- Writing reports with aggregationAggregation: GROUP BY and HAVING
- What is a database?Backend Development · Databases and SQL basics
- TriggersProcedures, triggers, SQL engines and NoSQL
- Indexes: making queries fastBackend Development · SQL in depth
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does a plain view store?
Answer: Only the query; it runs each time you use it
A view is a saved query, so it is always current and uses no space for rows.
Why is a view useful for privacy?
Answer: You can grant access to a view that leaves out private columns
Users see only the columns the view exposes.
What is the main trade-off of a materialized view?
Answer: It is fast to read but stale until refreshed
The stored result does not change until REFRESH runs.
SQLite has no materialized views. What is the usual substitute?
Answer: A summary table rebuilt with DELETE and INSERT ... SELECT on a schedule
A summary table holds the stored result; the refresh is up to you.
In PostgreSQL, what does REFRESH MATERIALIZED VIEW CONCURRENTLY need?
Answer: A UNIQUE index on the materialized view
CONCURRENTLY compares old and new rows, which needs a unique index; readers are not blocked.
In SQLite, you write INSERT INTO tickets (customer_id, subject, status) VALUES (3, 'Hi', DEFAULT). What happens?Related
Answer: A syntax error: SQLite does not accept DEFAULT in VALUES
Leave the column out instead. PostgreSQL, MySQL and SQL Server do accept DEFAULT there.
From INSERT
You want a named query that many reports and users can reuse across sessions. What fits best?Related
Answer: A view
A view is saved in the database (module 10); a CTE lives for one statement.
The sales view has one row per order line: 12 rows for 8 orders. How do you count orders?Related
Answer: COUNT(DISTINCT order_id)
Several lines share one order_id; DISTINCT counts each order once.
Which column gets an index automatically in SQLite and PostgreSQL?Related
Answer: A column with a UNIQUE constraint
UNIQUE and PRIMARY KEY are enforced with an index. Foreign keys are not indexed automatically.
From How indexes work
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- This is the last topic of “Indexes, query plans and views”. The module exam comes next.
- Module 10 exam: Indexes, query plans and views · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Procedures, triggers, SQL engines and NoSQL. 62 topics left in the course before the final exam.