A view is a named query you can read like a table. A materialized view is a stored copy of that query's result. Choose the live interface when truth must stay current; choose the stored copy when a heavy summary can be a little stale.
A view is a saved SELECT
Once a filter or join is reviewed, you should not copy it into every report. CREATE VIEW stores the query in the schema. Consumers SELECT from the view name. They do not have to remember is_active = 1, and they cannot accidentally forget it.
The view below is a catalog interface: active products only, with the columns a storefront needs. The unpublished poster is present in products and absent from active_products.
SQL BROWSER RUNNER
Create a view of active products
Save a filter as a named interface, then read it like a table.
Edit the query, predict the rows it will return, then run it.
Ordinary views follow live tables
A view does not keep its own rows. When the poster is published, the next SELECT from active_products includes it. There is no refresh step. That is the point: the interface always means “active products now.”
SQL BROWSER RUNNER
Change a base table and reread the view
Publish the poster, then confirm the view picked up the change.
CREATETABLE products (
product_id INTEGERPRIMARYKEY,
product_name TEXTNOTNULL,
is_active INTEGERNOTNULL);INSERTINTO products VALUES(1,'SQL notebook',1),(2,'Schema poster',0);CREATEVIEW active_products ASSELECT product_id, product_name
FROM products
WHERE is_active =1;UPDATE products SET is_active =1WHERE product_id =2;SELECT product_id, product_name
FROM active_products
ORDERBY product_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Views can hide a join
A reporting view often joins several normalized tables and returns one card-shaped row. The consumer asks for paid order cards; the view owns the join and the paid filter. That is how a normalized model stays clean while screens stay simple.
SQL BROWSER RUNNER
Publish a paid-order card view
Join customers to orders once, then query the named result.
CREATETABLE customers (
customer_id INTEGERPRIMARYKEY,
customer_name TEXTNOTNULL);CREATETABLE orders (
order_id INTEGERPRIMARYKEY,
customer_id INTEGERNOTNULL,statusTEXTNOTNULL,
amount_cents INTEGERNOTNULL);INSERTINTO customers VALUES(101,'Amina Idrissi'),(102,'Bilal Karim');INSERTINTO orders VALUES(5001,101,'paid',1499),(5002,101,'pending',899),(5003,102,'paid',299);CREATEVIEW paid_order_cards ASSELECT
customer.customer_name,
sale.order_id,
sale.amount_cents
FROM customers AS customer
JOIN orders AS sale ON sale.customer_id = customer.customer_id
WHERE sale.status='paid';SELECT customer_name, order_id, amount_cents
FROM paid_order_cards
ORDERBY order_id;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
A view can be an aggregate interface
Grouped results are awkward to copy. A view that returns one row per customer with paid-order counts becomes a stable source for dashboards. The grouping still happens on every read. If that becomes expensive, you are looking at a snapshot problem, not a naming problem.
SQL BROWSER RUNNER
Name paid totals per customer
Store an aggregate query as a view with explicit result columns.
Edit the query, predict the rows it will return, then run it.
Inspect what the schema stored
In this SQLite runner, sqlite_master lists tables and views. A view is a schema object, not an application-only nickname. Other databases expose views through information schema or catalog views. The important check is the same: is this a stored query, or a stored result?
SQL BROWSER RUNNER
List tables and views in the catalog
Create one table and one view, then read SQLite's schema list.
Edit the query, predict the rows it will return, then run it.
A materialized view stores a result
PostgreSQL's CREATE MATERIALIZED VIEW keeps the query output on disk. Reads can skip a heavy join. The cost is freshness: after base tables change, REFRESH MATERIALIZED VIEW (or an equivalent job) must run. Until then, the snapshot can lie.
SQLite has no native MATERIALIZED VIEW syntax. The next example uses an ordinary table filled with INSERT ... SELECT. The maintenance question is identical: who rebuilds it, and how stale may it be?
SQL BROWSER RUNNER
Watch a snapshot fall behind live data
Store paid totals, add another paid order, then compare snapshot and live sums.
Edit the query, predict the rows it will return, then run it.
Review a view before you ship it
Write the one-row meaning of the view in a sentence.
List explicit column names; do not use SELECT *.
Decide whether the result must be live or may be stale.
If it is live, CREATE VIEW and query that name from applications.
If it is stored, name the refresh job, the allowed lag, and the source query.
Prove it: change a base row, then check whether the interface should follow or stay still.
Independent lab: a live catalog and a snapshot
Build a published-workshop card interface. The view joins workshops to registrations, keeps unpublished workshops out, and returns registration and seat counts. The snapshot table copies that result so a dashboard could read it without repeating the join.
After you run the starter, insert another registration for workshop 10 and compare published_workshop_cards with workshop_card_snapshot. The view should move; the snapshot should not until you rebuild it.
SQL BROWSER RUNNER
Ship a workshop card view and a copy
Create a live grouped view, copy it into a snapshot table, then reason about freshness.
CREATETABLE workshops (
workshop_id INTEGERPRIMARYKEY,
title TEXTNOTNULL,
city TEXTNOTNULL,
is_published INTEGERNOTNULL);CREATETABLE registrations (
registration_id INTEGERPRIMARYKEY,
workshop_id INTEGERNOTNULL,
attendee_email TEXTNOTNULL,
seats INTEGERNOTNULL);INSERTINTO workshops VALUES(10,'SQL foundations','Casablanca',1),(20,'Schema design','Rabat',1),(30,'Draft internals','Tangier',0);INSERTINTO registrations VALUES(1,10,'amina@example.com',1),(2,10,'bilal@example.com',2),(3,20,'amina@example.com',1);CREATEVIEW published_workshop_cards ASSELECT
workshop.workshop_id,
workshop.title,
workshop.city,COUNT(registration.registration_id)AS registration_count,COALESCE(SUM(registration.seats),0)AS seat_count
FROM workshops AS workshop
LEFTJOIN registrations AS registration
ON registration.workshop_id = workshop.workshop_id
WHERE workshop.is_published =1GROUPBY workshop.workshop_id, workshop.title, workshop.city;CREATETABLE workshop_card_snapshot (
workshop_id INTEGERPRIMARYKEY,
title TEXTNOTNULL,
city TEXTNOTNULL,
registration_count INTEGERNOTNULL,
seat_count INTEGERNOTNULL);INSERTINTO workshop_card_snapshot
SELECT workshop_id, title, city, registration_count, seat_count
FROM published_workshop_cards;SELECT title, city, registration_count, seat_count
FROM published_workshop_cards
ORDERBY title;
QUERY OUTPUT
Edit the query, predict the rows it will return, then run it.
Common mistakes to avoid
Copying the same join into five reports instead of naming one view.
Using SELECT * so a base-table change silently reshapes the interface.
Assuming a view stores rows, then wondering why it “refreshed itself.”
Writing into a join view without an explicit, documented write rule.
Creating a snapshot with no owner, schedule, or comparison against live totals.
Using a stale dashboard number as the only copy of a financial fact.
Lesson review
I can create a view as a named, reviewed SELECT.
I can explain why an ordinary view follows base-table changes.
I can hide a join or aggregate behind stable column names.
I can treat most views as read interfaces.
I can tell a live view from a stored snapshot that needs a refresh.
I can simulate a materialized view in SQLite with a snapshot table.
KNOWLEDGE CHECK
Check your view reasoning
Answer all ten questions, then revisit the example whose freshness or interface still feels unclear.