A view is a named, saved SELECT query — it stores the query definition, not the data itself
Views re-execute the underlying query every time they are accessed — they are not a cache
Materialized views store the query results physically and can be indexed — they are a cache
Updatable views allow INSERT/UPDATE/DELETE on the underlying table through the view — subject to restrictions
Views enforce security: GRANT SELECT ON view while hiding sensitive columns from the underlying table
Biggest mistake: treating a view as a performance optimization — a slow query behind a view is still a slow query
Plain-English First
Imagine your company has a massive filing cabinet with thousands of folders. Every morning your manager needs the same five folders from the same three drawers. Instead of digging through the cabinet each time, you create a shortcut folder on the desk that automatically shows exactly what she needs. A SQL View is that shortcut folder — it's a saved query that looks and feels like a table, but the data always comes fresh from the real tables underneath.
Every production database grows complicated fast. A single order in an e-commerce system might touch five or six tables — customers, orders, order_items, products, addresses, and discounts. If every developer on your team writes their own join query to pull a customer order summary, you get six slightly different versions of the truth, and the day a column gets renamed you're hunting down broken queries across a dozen files. SQL Views were built precisely to kill that problem before it kills your sanity.
A view wraps a complex query into a single named object that lives inside the database. Anyone who needs that data just selects from the view — they don't need to know or care about the joins, filters, or subqueries underneath. When the underlying structure changes, you update the view in one place and every consumer is instantly fixed. Beyond simplicity, views also act as a security layer: you can grant a user access to a view that shows only certain columns or rows, without ever giving them access to the raw tables.
By the end of this article you'll know exactly when to reach for a view instead of repeating a query, how to build both read-only and updatable views, how to use views as a security boundary, and — critically — the limitations that trip up even experienced developers. You'll walk away with patterns you can drop into a real project today.
Creating Your First View — and Understanding What Actually Happens
When you run CREATE VIEW, the database doesn't execute the query and store a snapshot of the results. It stores the query definition itself. Every time someone SELECTs from the view, the database runs that stored query fresh against the live tables. This is the single most important thing to understand about views: they are not cached copies of data. They are reusable, named queries.
This design has a beautiful consequence. If a new order is inserted into the orders table at 2pm, anyone querying the view at 2:01pm sees it immediately — no refresh, no sync, no ETL job needed.
Start with a realistic scenario. You have an e-commerce database. Business stakeholders constantly ask: 'Show me each customer's name, their total number of orders, and their lifetime spend.' Writing that join every time is tedious and error-prone. A view makes it a one-liner for every future query.
The syntax is simple, but the thinking behind it matters more than the keywords. You're essentially giving a SELECT statement a permanent name and home inside your database schema.
create_customer_order_summary_view.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
-- ─────────────────────────────────────────────────────────────-- SETUP: Create the base tables we'll build our view on top of-- ─────────────────────────────────────────────────────────────CREATETABLEcustomers (
customer_id INTPRIMARYKEY,
full_name VARCHAR(100) NOTNULL,
email VARCHAR(150) NOTNULL,
country VARCHAR(50) NOTNULL
);
CREATETABLEorders (
order_id INTPRIMARYKEY,
customer_id INTNOTNULLREFERENCEScustomers(customer_id),
order_date DATENOTNULL,
total_amount NUMERIC(10,2) NOTNULL
);
-- ─────────────────────────────────────────────────────────────-- SEED DATA: Realistic rows so we can see real output-- ─────────────────────────────────────────────────────────────INSERTINTO customers VALUES
(1, 'Amara Osei', 'amara@example.com', 'Ghana'),
(2, 'Lena Fischer', 'lena@example.com', 'Germany'),
(3, 'Carlos Ruiz', 'carlos@example.com', 'Mexico');
INSERTINTO orders VALUES
(101, 1, '2024-01-15', 89.99),
(102, 1, '2024-03-22', 214.50),
(103, 2, '2024-02-10', 49.00),
(104, 3, '2024-04-01', 320.00),
(105, 3, '2024-04-18', 75.25);
-- ─────────────────────────────────────────────────────────────-- THE VIEW: Stores the query definition, NOT the result data.-- Every SELECT against this view re-runs the JOIN live.-- ─────────────────────────────────────────────────────────────CREATEVIEW customer_order_summary ASSELECT
c.customer_id,
c.full_name,
c.country,
COUNT(o.order_id) AS total_orders, -- aggregates across all rows for this customerSUM(o.total_amount) AS lifetime_spend, -- total money spent, everMAX(o.order_date) AS last_order_date -- most recent purchase dateFROM customers c
LEFTJOIN orders o ON c.customer_id = o.customer_id -- LEFT JOIN keeps customers with zero ordersGROUPBY
c.customer_id,
c.full_name,
c.country;
-- ─────────────────────────────────────────────────────────────-- USING THE VIEW: Looks exactly like querying a table-- ─────────────────────────────────────────────────────────────SELECT
full_name,
total_orders,
lifetime_spend
FROM customer_order_summary
WHERE lifetime_spend > 100.00ORDERBY lifetime_spend DESC;
Output
full_name | total_orders | lifetime_spend
---------------+--------------+---------------
Carlos Ruiz | 2 | 395.25
Amara Osei | 2 | 304.49
Key Mental Model:
Think of a view as a 'query alias', not a 'data copy'. The database runs the underlying SELECT fresh on every access. This means your data is always current, but it also means a poorly-written view query will be slow every time it's called — there's nowhere to hide a bad JOIN inside a view.
Production Insight
Views are query aliases, not data caches — a view on a slow query is a slow view.
EXPLAIN on a view-based query shows the full expanded plan — the view is transparent to the optimizer.
Materialized views (CREATE MATERIALIZED VIEW) store data physically and can be indexed — use them for expensive aggregations accessed frequently.
Key Takeaway
A regular view re-executes its query every time — no caching, no performance benefit from the view itself.
Materialized views store results and can be indexed — the right tool for expensive report queries.
Push selective WHERE conditions inside view definitions when the view is always queried with a specific filter.
Views as a Security Layer — Hide Columns, Filter Rows, Control Access
This is the use case that makes views indispensable in production systems — and the one most tutorials skip entirely. Views let you expose only the data a given role should see, without duplicating tables or writing application-level filters that can be bypassed.
Consider a HR system. The employees table holds salary, bank account numbers, and performance review scores alongside public info like name and department. Your operations team needs to look up who's in which department. Your payroll team needs salary data. Under no circumstances should either team see the other's sensitive columns.
Instead of granting direct table access, you create purpose-built views for each audience, grant SELECT on the view, and explicitly deny SELECT on the raw table. The database enforces this at the engine level — no application bug can accidentally leak a salary figure through a view that was never designed to show one.
Row-level filtering works the same way. A regional manager should only see employees in their region. You bake that WHERE clause into the view definition. They literally cannot query rows outside their region, because the view definition never fetches them.
security_views_hr_example.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
-- ─────────────────────────────────────────────────────────────-- BASE TABLE: Contains sensitive columns alongside public ones-- ─────────────────────────────────────────────────────────────CREATETABLEemployees (
employee_id INTPRIMARYKEY,
full_name VARCHAR(100) NOTNULL,
department VARCHAR(80) NOTNULL,
region VARCHAR(50) NOTNULL,
annual_salary NUMERIC(12,2) NOTNULL, -- SENSITIVE
bank_account VARCHAR(30) NOTNULL, -- SENSITIVE
hire_date DATENOTNULL
);
INSERTINTO employees VALUES
(1, 'Yuki Tanaka', 'Engineering', 'APAC', 95000.00, 'GB29NWBK60161331926819', '2021-06-01'),
(2, 'Sofia Delgado', 'HR', 'EMEA', 72000.00, 'DE89370400440532013000', '2020-03-15'),
(3, 'Marcus Webb', 'Engineering', 'EMEA', 88000.00, 'FR7614508059405402982935', '2019-11-22'),
(4, 'Priya Sharma', 'Sales', 'APAC', 67000.00, 'IN30267931234567890123', '2022-09-10');
-- ─────────────────────────────────────────────────────────────-- VIEW 1: Operations team — sees name, department, region only.-- Salary and bank account columns are simply not included.-- ─────────────────────────────────────────────────────────────CREATEVIEW employee_directory ASSELECT
employee_id,
full_name,
department,
region,
hire_date
FROM employees; -- no salary, no bank_account — they don't exist in this view-- ─────────────────────────────────────────────────────────────-- VIEW 2: Payroll team — salary visible, but still no bank-- account number (that's only for the finance system to access)-- ─────────────────────────────────────────────────────────────CREATEVIEW employee_payroll_summary ASSELECT
employee_id,
full_name,
department,
annual_salary
FROM employees; -- bank_account intentionally excluded-- ─────────────────────────────────────────────────────────────-- VIEW 3: EMEA regional manager — row-level restriction.-- This view physically cannot return rows from other regions.-- ─────────────────────────────────────────────────────────────CREATEVIEW emea_employee_directory ASSELECT
employee_id,
full_name,
department,
hire_date
FROM employees
WHERE region = 'EMEA'; -- the filter lives in the database, not the application-- ─────────────────────────────────────────────────────────────-- GRANT access to the VIEW only. The ops_user role never gets-- SELECT on the raw employees table.-- ─────────────────────────────────────────────────────────────-- GRANT SELECT ON employee_directory TO ops_user;-- REVOKE SELECT ON employees FROM ops_user;-- Test the EMEA view — only EMEA rows should appearSELECT * FROM emea_employee_directory ORDERBY full_name;
If you allow INSERTs or UPDATEs through a view, add WITH CHECK OPTION to the view definition. This forces the database to reject any write that would create a row the view itself couldn't see — for example, inserting an APAC employee through the emea_employee_directory view would be blocked. Without it, someone can silently insert a row that then vanishes from the view, which is a data integrity nightmare.
Production Insight
Views are the standard database security layer — grant SELECT on a view, revoke SELECT on the underlying table.
CREATEOR REPLACE VIEW is safe — it redefines the view without dropping dependent objects.
For row-level security, add WHERE clauses inside the view: CREATE VIEW my_orders AS SELECT * FROM orders WHERE user_id = current_user_id().
Key Takeaway
Views are the correct way to expose a subset of columns or rows without granting access to the underlying table.
GRANT SELECT ON view_name TO role_name — the role never sees the underlying table's sensitive columns.
Row-level security inside views (WHERE user_id = current_user()) is a proven pattern for multi-tenant data isolation.
Updatable Views vs. Read-Only Views — Knowing Which is Which
Not all views are equal when it comes to writing data back through them. Some views allow INSERT, UPDATE, and DELETE — these are called updatable views. Others are permanently read-only. Knowing the rules prevents runtime errors and design mistakes.
A view is updatable when it maps cleanly to a single base table with no transformation: no GROUP BY, no aggregate functions like SUM or COUNT, no DISTINCT, no subqueries in the SELECT list, and no JOINs that would make row identity ambiguous. If the database can figure out exactly which row in exactly which table to touch, it'll allow the write.
The moment you add an aggregate, a GROUP BY, or a JOIN across multiple tables, the view becomes read-only. This makes sense when you think about it: if a view row represents the SUM of five order rows, which of those five rows should an UPDATE actually modify?
Materialized views are a separate concept (available in PostgreSQL, Oracle, and others) where the query result IS physically stored. They're fast to read but require explicit refreshing. The trade-off is freshness vs. performance — crucial for dashboards and reporting.
updatable_vs_readonly_views.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
-- ─────────────────────────────────────────────────────────────-- SETUP-- ─────────────────────────────────────────────────────────────CREATETABLEproducts (
product_id INTPRIMARYKEY,
product_name VARCHAR(100) NOTNULL,
category VARCHAR(50) NOTNULL,
unit_price NUMERIC(8,2) NOTNULL,
is_active BOOLEANNOTNULLDEFAULTTRUE
);
INSERTINTO products VALUES
(1, 'Wireless Keyboard', 'Electronics', 49.99, TRUE),
(2, 'Desk Lamp', 'Office', 29.99, TRUE),
(3, 'Ergonomic Chair', 'Furniture', 349.00, FALSE),
(4, 'USB-C Hub', 'Electronics', 34.99, TRUE);
-- ─────────────────────────────────────────────────────────────-- UPDATABLE VIEW: Single table, no aggregates, no DISTINCT.-- The database can map each view row to exactly one table row.-- ─────────────────────────────────────────────────────────────CREATEVIEW active_products ASSELECT
product_id,
product_name,
category,
unit_price
FROM products
WHERE is_active = TRUE; -- simple filter, view is still updatable-- This UPDATE works — the database knows exactly which row to changeUPDATE active_products
SET unit_price = 54.99WHERE product_id = 1; -- maps cleanly to products row with product_id = 1-- Confirm the change made it through to the base tableSELECT product_id, product_name, unit_price FROM products WHERE product_id = 1;
-- ─────────────────────────────────────────────────────────────-- READ-ONLY VIEW: Uses GROUP BY + COUNT — not updatable.-- The database can't reverse-engineer which base rows to touch.-- ─────────────────────────────────────────────────────────────CREATEVIEW product_count_by_category ASSELECT
category,
COUNT(*) AS product_count,
AVG(unit_price) AS avg_price
FROM products
GROUPBY category;
-- This would FAIL with: ERROR: cannot update a non-updatable view-- UPDATE product_count_by_category SET avg_price = 40.00 WHERE category = 'Electronics';-- (Commented out so the script runs cleanly — uncomment to see the error)-- Safe to SELECT from it thoughSELECT * FROM product_count_by_category ORDERBY category;
Output
-- After the UPDATE through active_products:
product_id | product_name | unit_price
-----------+--------------------+-----------
1 | Wireless Keyboard | 54.99
-- product_count_by_category SELECT result:
category | product_count | avg_price
------------+---------------+-----------
Electronics | 2 | 44.99
Furniture | 1 | 349.00
Office | 1 | 29.99
Watch Out — The Silent DELETE Problem:
With an updatable view that has a WHERE filter, you can UPDATE a row so it no longer satisfies the view's WHERE clause. The row doesn't get deleted — it just disappears from the view while still living in the base table. This looks exactly like a delete to anyone querying the view. Always add WITH CHECK OPTION to prevent writes that would make rows 'vanish' silently.
Production Insight
Updatable views require a simple SELECT on a single table without DISTINCT, GROUP BY, aggregate functions, or subqueries.
Most production views are read-only — treat them as such and update the underlying tables directly.
INSTEAD OF triggers on views (PostgreSQL) allow DML on complex non-updatable views when the update semantics are well-defined.
Key Takeaway
Updatable views are the exception, not the rule — most views with JOINs or aggregates are read-only.
For complex view DML, INSTEAD OF triggers handle the routing to underlying tables.
Always test INSERT/UPDATE through a view with a single row before bulk operations.
Replacing and Dropping Views — Managing Views in a Real Schema
Views aren't fire-and-forget. Business requirements change, columns get renamed, and performance optimizations force you to refactor the underlying query. Knowing how to safely update and remove views without taking down dependent code is a practical skill that separates juniors from seniors.
CREATE OR REPLACE VIEW lets you redefine a view's query in place, without dropping dependent objects or revoking existing permissions. There's one catch: you can't remove columns from the SELECT list using REPLACE — you can only add new ones or change existing expressions. Removing columns requires a DROP followed by a CREATE.
Before dropping a view, check whether other views, stored procedures, or application queries depend on it. Most databases give you a system catalog to query for this. Dropping a view with CASCADE will also drop anything that depends on it — a powerful option that can silently destroy more than you intended.
Replacing the query inside a view is also how you fix a performance problem. If you realize the underlying JOIN was written inefficiently, you REPLACE the view with an optimized query and every caller instantly benefits, with no code changes outside the database.
managing_views_lifecycle.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
-- ─────────────────────────────────────────────────────────────-- ORIGINAL VIEW (from our first section)-- ─────────────────────────────────────────────────────────────-- CREATE VIEW customer_order_summary AS ... (already created above)-- ─────────────────────────────────────────────────────────────-- SCENARIO: Business now wants average order value added.-- Use CREATE OR REPLACE — keeps existing GRANTs and dependencies.-- ─────────────────────────────────────────────────────────────CREATEORREPLACEVIEW customer_order_summary ASSELECT
c.customer_id,
c.full_name,
c.country,
COUNT(o.order_id) AS total_orders,
SUM(o.total_amount) AS lifetime_spend,
ROUND(AVG(o.total_amount), 2) AS avg_order_value, -- NEW column added safelyMAX(o.order_date) AS last_order_date
FROM customers c
LEFTJOIN orders o ON c.customer_id = o.customer_id
GROUPBY
c.customer_id,
c.full_name,
c.country;
-- ─────────────────────────────────────────────────────────────-- CHECK DEPENDENCIES before dropping (PostgreSQL syntax).-- Run this BEFORE any DROP VIEW to see what you'd break.-- ─────────────────────────────────────────────────────────────SELECT
dependent_view.relname AS view_that_depends,
source_view.relname AS depends_on_this_view
FROM pg_depend dep
JOIN pg_rewrite rw ON dep.objid = rw.oid
JOIN pg_class dependent_view ON rw.ev_class = dependent_view.oid
JOIN pg_class source_view ON dep.refobjid = source_view.oid
WHERE source_view.relname = 'customer_order_summary' -- replace with your view nameAND dependent_view.relname != source_view.relname; -- exclude self-reference-- ─────────────────────────────────────────────────────────────-- SAFE DROP: No cascade — fails loudly if dependencies exist.-- That loud failure is a feature, not a bug.-- ─────────────────────────────────────────────────────────────-- DROP VIEW customer_order_summary; -- safe: errors if depended on-- DROP VIEW customer_order_summary CASCADE; -- dangerous: silently drops dependents-- Verify the updated view includes the new columnSELECT
full_name,
total_orders,
lifetime_spend,
avg_order_value
FROM customer_order_summary
ORDERBY lifetime_spend DESC;
DROP VIEW my_view CASCADE will silently remove every view, function, or trigger that depends on my_view — including views that depend on those views. In production, always use DROP VIEW without CASCADE first. If it errors, read the dependency list carefully. Only use CASCADE when you genuinely intend to destroy the entire dependency chain and have confirmed it's safe to do so.
Production Insight
CREATE OR REPLACE VIEW preserves view permissions — prefer it over DROP + CREATE which loses all grants.
Orphan views (referencing renamed or dropped tables) produce errors at query time, not at view creation time.
View dependencies can be checked: SELECT * FROM information_schema.view_table_usage WHERE view_name = 'your_view'.
Key Takeaway
CREATE OR REPLACE VIEW is idempotent and preserves permissions — use it in migration scripts.
Views don't validate their underlying tables at creation time — only at query time.
Document view dependencies explicitly — renaming a table breaks views silently until they're queried.
● Production incidentPOST-MORTEMseverity: high
Dashboard Timed Out After Adding a View for 'Convenience'
Symptom
The customer dashboard timed out for all users after a 'non-breaking' change that added a view. The dashboard team reported 504 errors within an hour of deployment.
Assumption
The developer believed the view would cache the expensive JOIN, making the dashboard query cheaper. Views are 'precomputed', right?
Root cause
Standard views are not precomputed. The dashboard query SELECT * FROM customer_dashboard_view was equivalent to running the full 6-table JOIN inline. Without the WHERE clause on the view's source tables (the dashboard query applied WHERE after the view), the view joined all rows before filtering. EXPLAIN showed a 12M row intermediate result before the final WHERE narrowed it to 50 rows.
Fix
Added the critical WHERE predicate inside the view definition itself. Converted to a materialized view with REFRESH MATERIALIZED VIEW CONCURRENTLY on a 5-minute cron. Dashboard query went from 45s to 80ms.
Key lesson
Views are not caches — every SELECT from a view re-runs the underlying query
Push WHERE predicates inside the view definition when the view is always queried with a specific filter
For expensive aggregate views, use materialized views with scheduled refresh
Production debug guideDiagnosing view performance and updatability issues3 entries
Symptom · 01
Query on a view is much slower than expected
→
Fix
Run EXPLAIN ANALYZE on the view query. The view is expanded inline — you'll see the full underlying query in the plan. Look for missing WHERE predicates being applied outside the view. Add the most selective filter conditions inside the view definition itself.
Symptom · 02
Error: cannot update a non-updatable view
→
Fix
Views with JOINs, GROUP BY, DISTINCT, aggregate functions, or subqueries are not updatable. Simplify the view to a single-table SELECT without these features, or use INSTEAD OF triggers (PostgreSQL) to intercept DML on complex views.
Symptom · 03
View returns stale data after underlying table changes
→
Fix
If this is a regular view, it should return current data — investigate whether you're querying a materialized view instead (\dv in PostgreSQL shows view type). For materialized views, run REFRESH MATERIALIZED VIEW view_name to update the data.
Aspect
Regular View
Materialized View
Data storage
No data stored — query runs on every access
Result set physically stored on disk
Data freshness
Always current — reflects live table changes instantly
Stale until manually or scheduled REFRESH is run
Read performance
As slow as the underlying query every time
Very fast — reads pre-computed results like a table
Heavy aggregation, dashboards, slow-changing analytical queries
REFRESH needed
Never — no concept of refresh
Yes — REFRESH MATERIALIZED VIEW must be triggered
Key takeaways
1
A view stores a query definition, not data
every SELECT re-runs the underlying query against the live tables, so your results are always fresh but a bad query is always slow.
2
Views are one of the most underused security tools in SQL
by granting access to a view instead of a base table, you enforce column-level and row-level access control at the database engine, not the application layer.
3
A view is updatable only when the database can unambiguously map one view row to exactly one base table row
aggregates, GROUP BY, DISTINCT, and multi-table JOINs break updatability.
4
Always use CREATE OR REPLACE VIEW to update a view in place (preserving permissions), and always run a dependency check before DROP VIEW
DROP CASCADE can silently destroy dependent objects across your entire schema.
Common mistakes to avoid
3 patterns
×
Treating a view as a performance optimization or cache
Symptom
Dashboard or report query is slow despite being 'simplified' by a view — EXPLAIN shows the full expensive underlying join is still executing
Fix
Views are transparent to the query optimizer — a slow query behind a view is still slow. For caching, use a materialized view: CREATE MATERIALIZED VIEW ... AS SELECT ... with REFRESH MATERIALIZED VIEW on a schedule. Index the materialized view for fast access.
×
Selecting from a view without the WHERE clause that belongs inside the view
Symptom
The view generates a massive intermediate result set before the WHERE is applied, causing full table scans on large tables
Fix
Move the most selective WHERE conditions inside the view definition when the view is always accessed with those conditions. A view that always filters by active status should have WHERE status = 'active' in its definition, not in every query that uses it.
×
Dropping and recreating a view instead of using CREATE OR REPLACE VIEW
Symptom
After a view is recreated, all previously granted permissions are lost — users or roles that had SELECT on the view can no longer access it
Fix
Use CREATE OR REPLACE VIEW view_name AS SELECT ... — it redefines the view while preserving all existing permissions. DROP VIEW is necessary only when changing the number of columns or column types.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is a view in SQL and what are its main use cases?
Q02SENIOR
What is the difference between a view and a materialized view?
Q03SENIOR
Can you UPDATE rows through a view? What are the restrictions?
Q01 of 03JUNIOR
What is a view in SQL and what are its main use cases?
ANSWER
A view is a named saved SELECT query stored in the database. When queried, the view's underlying SELECT is executed as if it were written inline. The three main use cases are: (1) Simplification — wrapping a complex multi-table JOIN into a readable name that application code can SELECT from without duplicating the JOIN logic. (2) Security — granting SELECT on a view that exposes only non-sensitive columns while the application role has no access to the underlying table. (3) Logical data model — presenting data in a shape the application expects without denormalizing the physical schema.
Q02 of 03SENIOR
What is the difference between a view and a materialized view?
ANSWER
A regular view stores only the query definition — every SELECT from the view re-executes the underlying query against current data. It's always up-to-date but has no performance benefit over writing the query inline. A materialized view stores the query results physically on disk — it behaves like a table. It can be indexed, making reads very fast. The trade-off is staleness: materialized views must be explicitly refreshed (REFRESH MATERIALIZED VIEW) to pick up changes from the underlying tables. Use regular views for simplicity and security; use materialized views for expensive aggregation queries that are read frequently and can tolerate periodic staleness.
Q03 of 03SENIOR
Can you UPDATE rows through a view? What are the restrictions?
ANSWER
Yes, but only for simple views meeting specific requirements: (1) The view selects from exactly one base table. (2) The SELECT does not contain DISTINCT, GROUP BY, HAVING, aggregate functions, or set operators (UNION, INTERSECT). (3) The view doesn't include subqueries in the SELECT list. (4) The view must include the base table's primary key. If any of these conditions are violated, the view is non-updatable and DML returns an error. For complex views that need to support DML, INSTEAD OF triggers (PostgreSQL) can be defined to intercept INSERT/UPDATE/DELETE and translate them into the appropriate base table operations.
01
What is a view in SQL and what are its main use cases?
JUNIOR
02
What is the difference between a view and a materialized view?
SENIOR
03
Can you UPDATE rows through a view? What are the restrictions?
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
Does a SQL view store data or just the query?
A regular SQL view stores only the query definition — no data is saved. Every time you SELECT from the view, the database executes the underlying query fresh against the live base tables. If you need the results physically stored for performance, you want a Materialized View instead.
Was this helpful?
02
Can you INSERT or UPDATE data through a SQL view?
Yes, but only if the view meets specific conditions: it must reference a single base table, include no aggregate functions, no GROUP BY, no DISTINCT, and no subqueries in the SELECT list. When these conditions are met, writes go directly to the underlying base table. Complex views involving joins or aggregates are permanently read-only.
Was this helpful?
03
Why would you use a view instead of just writing the query every time?
Three main reasons. First, consistency — one view definition means every team uses identical logic rather than subtly different versions of a query. Second, security — you can grant users access to a view without exposing sensitive columns or rows from the base tables. Third, maintainability — if your schema changes, you update the view in one place and all consumers are fixed immediately with no application code changes.