Hook: Interviewers love this topic because it checks whether you know the difference between saving a query and saving its result.
Question: What are materialized views in SQL?
Answer: A materialized view is a stored copy of a query result. A normal view is just a saved SQL definition that runs every time you query it, but a materialized view stores rows on disk, so reads are much faster. The trade-off is freshness: the data can become stale until you refresh it.
Interview-Ready Answer: I think of a materialized view as a physically stored snapshot of a query. I use it when the same expensive join or aggregate is read many times, like in dashboards or reports. The big benefit is speed on reads, and the big cost is that it does not update automatically, so I need to refresh it. In PostgreSQL, I can also refresh concurrently in some cases, but that requires a unique index and is mainly for reducing blocking, not for making the data live.
A materialized view is like a table that the database fills from a query. The key idea is simple: the database runs the query once, stores the output, and later queries read that stored output directly. That is why it helps with expensive work such as big joins, grouped totals, or window-heavy reports.
SELECT statement, often one that is slow when run repeatedly.CONCURRENTLY is designed to let readers keep going, but it needs a unique index and can take longer.| Object | Stores data? | Freshness | Best for |
|---|---|---|---|
| View | No | Always current | Abstraction |
| Materialized view | Yes | Stale until refresh | Fast reports |
| Table | Yes | Current on writes | Operational data |
The read path is usually much faster because the database avoids redoing joins and aggregates. If the original report takes 8 seconds over 50 million rows, the materialized view might answer in 20 to 300 milliseconds, depending on its size and indexes. Refresh cost is roughly the cost of running the underlying query again, plus writing the output. Storage cost is the size of the stored result, plus any indexes you add.
Think in simple complexity terms: reading a materialized view is closer to scanning the result set, while refreshing is closer to recomputing the original query. So the work moves from every read to every refresh.
REFRESH ... CONCURRENTLY arrived in 9.4.Real-World Story: Imagine an e-commerce checkout service that powers a finance dashboard showing revenue by day, by region, and by product category. The raw report joins orders, payments, refunds, and product tables, then groups millions of rows. Without a materialized view, every dashboard load takes several seconds and piles work onto the primary database.
The team adds a materialized view that stores the daily summary and refreshes it every 5 minutes. Now the dashboard opens quickly, and the database stops doing the same expensive aggregation over and over.
What goes wrong when someone misunderstands it: a developer assumes the view is live and uses it for a near-real-time alert on failed payments. The source tables update immediately, but the materialized view is still waiting for its next refresh, so the alert is late. Users see old numbers in the dashboard, logs show the refresh job missed its schedule, and support gets reports like “the site says yesterday’s revenue is still today’s total.” The symptom is not bad SQL syntax; it is stale data being mistaken for fresh data.
-- PostgreSQL example: a materialized view stores a snapshot of an aggregate report.
-- The comments explain WHY each step matters.
DROP MATERIALIZED VIEW IF EXISTS daily_sales_summary;
DROP TABLE IF EXISTS sales;
CREATE TABLE sales (
sale_id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sale_date DATE NOT NULL,
amount NUMERIC(10,2) NOT NULL CHECK (amount >= 0)
);
INSERT INTO sales (sale_date, amount) VALUES
('2026-07-01', 100.00),
('2026-07-01', 50.00),
('2026-07-02', 25.00),
('2026-07-02', 75.00),
('2026-07-03', 10.00);
-- The result of this SELECT is physically stored, so reads are cheap.
CREATE MATERIALIZED VIEW daily_sales_summary AS
SELECT
sale_date,
COUNT(*) AS order_count,
SUM(amount) AS total_amount
FROM sales
GROUP BY sale_date;
-- A materialized view can be indexed like a table.
-- This is helpful when the report is filtered by date very often.
CREATE UNIQUE INDEX daily_sales_summary_sale_date_uq
ON daily_sales_summary (sale_date);
-- Initial snapshot.
SELECT *
FROM daily_sales_summary
ORDER BY sale_date;
-- New transactions arrive after the snapshot was built.
-- This is the key trade-off: the materialized view does NOT change automatically.
INSERT INTO sales (sale_date, amount) VALUES
('2026-07-02', 200.00),
('2026-07-04', 99.99);
-- Still stale here: the summary has not been refreshed yet.
SELECT *
FROM daily_sales_summary
ORDER BY sale_date;
-- Refresh recomputes the stored result from the base table.
REFRESH MATERIALIZED VIEW daily_sales_summary;
-- Now the snapshot reflects the new rows.
SELECT *
FROM daily_sales_summary
ORDER BY sale_date;
-- Edge case to remember in real systems:
-- if a report needs second-by-second freshness, the refresh gap is a correctness problem.
Follow-up & Tricky Questions:
REFRESH MATERIALIZED VIEW CONCURRENTLY do? It lets reads continue while the refresh runs, but it needs a unique index and usually uses more time and space. It is a good fit when uptime matters more than refresh speed.Common Mistakes:
Memory Hook: Photo, not video. A materialized view is a snapshot photo of query results: fast to look at, but it can be out of date until someone takes a new picture.
Cheat Sheet:
CONCURRENTLY can reduce blocking, but needs a unique index.Practice Tasks:
orders table.