Worked examples — Views — creating, updatable views
Before we start, three words we lean on constantly:
See SQL SELECT and JOINs for the SELECT mechanics, Materialized Views for the stored-result cousin, Triggers for the INSTEAD OF escape hatch, and the parent topic.
The scenario matrix
Every view you will ever meet falls into one of these case classes. The examples below hit every cell.
| Cell | Case class | Updatable? | Covered by |
|---|---|---|---|
| A | Simple projection + WHERE, plain columns |
✅ Yes | Ex 1 |
| B | Insert that lands outside the WHERE (degenerate visibility) |
⚠️ Succeeds but vanishes | Ex 2 |
| B′ | Same, guarded with WITH CHECK OPTION |
✅ Rejected safely | Ex 2 |
| C | Computed column (price*1.1) in SELECT list |
❌ That column read-only | Ex 3 |
| D | Aggregate + GROUP BY (many→one mapping) |
❌ No | Ex 4 |
| E | DISTINCT (collapses duplicates) |
❌ No | Ex 5 |
| F | Multi-table JOIN, key-preserved side |
⚠️ Engine-dependent yes | Ex 6 |
| F′ | Same JOIN, touching the non-key-preserved side |
❌ No | Ex 6 |
| G | UNION (set operation) |
❌ No | Ex 7 |
| H | Non-updatable view rescued by INSTEAD OF trigger |
✅ You supply the map | Ex 8 |
| I | Real-world word problem (row-level security) | ✅ Yes | Ex 9 |
| J | Exam twist: DELETE vs NULL on a WHERE boundary |
✅ Yes, subtle | Ex 10 |
The single picture behind all of this — when the arrow from view to base is one-to-one, you can write; when it fans out or funnels in, you cannot:

AVG view where three base rows fan up into one view row, so the reverse arrow has no single target.
Ex 1 — Cell A: the clean updatable view
Forecast: guess the new price and whether the row survives before reading on.
- Identify the row mapping. The view is a projection (
item_id, name, price) plus a filter (price < 100). No aggregation, no join, noDISTINCT. Why this step? These are exactly the ingredients that keep the map one-to-one — cell A of the matrix. - Rewrite the write onto the base table.
item_id = 7picks one view row, which is one base row. So the engine rewrites toUPDATE items SET price = 90 WHERE item_id = 7;. Why this step?priceis a plain base column, so "set price to 90" has an unambiguous meaning downstairs. - Check the new value against the view filter. New price is , and is true. Why this step? We must know whether the row stays in the view — an update can silently push a row out.
Ex 2 — Cells B & B′: the insert that disappears (and the guard)
Forecast: succeed-and-visible / succeed-and-invisible / rejected — pick one.
- Route the insert to the base table. An
INSERTthrough a view is a base-table insert. The engine rewrites it toINSERT INTO items (item_id, name, price) VALUES (50, 'Mug', 150);. Why this step? The view holds no data; every write lands directly initems, and showing the rewritten SQL makes the target explicit. - Check validity against the base table only.
itemshas columnsitem_id, name, price— the row is a legalitemsrow, so it is stored. Why this step? TheWHERE price < 100filters reads, not writes — inserts don't consult it (cell B). - Now re-read the view. The view recomputes
WHERE price < 100. Row 50 has price , and is false, so the row is absent from the view. Why this step? This is the "vanishing row" trap: your insert worked yet you cannot see it. - Add the guard (cell B′). Re-create with
... WHERE price < 100 WITH CHECK OPTION;. Now the engine tests the incoming row against the filter before writing. is false → the insert is rejected. Why this step?WITH CHECK OPTIONmakes the write-filter equal the read-filter, closing the trap.
Ex 3 — Cell C: the computed column is read-only
Forecast: allowed / rejected — and what would price even become?
- Classify each SELECT column.
item_id,nameare plain base columns → updatable.list_priceisprice * 1.1, an expression → not a plain base column. Why this step? Only plain-column outputs have an obvious inverse write. - Try to invert the expression. To satisfy
list_price = 110you would needprice * 1.1 = 110, i.e. . The engine refuses to guess this inverse. Why this step? In general the expression may not be invertible (e.g.price*0), so SQL declares any expression column non-updatable rather than special-casing multiplication. - Consequence.
UPDATE ... SET list_price = ...is rejected. You can still updatenamethrough this view, because that column is plain.
Ex 4 — Cell D: aggregate + GROUP BY, the many-to-one killer
Forecast: compute the current average first, then decide if the update has a unique meaning.
- Compute the view row. . Why this step? To see what one view row summarises — three base rows funnel into one.
- Look at the mapping. One view row (dept 3) is built from three base rows. This is many→one, not one↔one (cell D). Why this step? Writability needs the reverse arrow to point at a single base row.
- Ask the reverse question. "Set avg to 60000" — but which of the three salaries do we change, and to what? Change one by any amount and offset another, and the average can still be anything. Infinitely many solutions. Why this step? No unique inverse ⇒ SQL rejects the update.
Ex 5 — Cell E: DISTINCT collapses rows
Forecast: how many base rows sit behind the single dept_id = 3 view row?
- Count the collapse.
DISTINCTfolds three base rows (emps 11, 12, 13, all dept 3) into one view row. Why this step?DISTINCTis a many→one collapse, just likeGROUP BY(cell E). - Try the reverse write. Changing the single view row's
dept_idfrom 3 to 9 — should it change all three employees, or just one? The mapping is ambiguous. Why this step? No unique base row to touch ⇒ non-updatable. - Reject. The engine refuses the update. Why this step? Because
DISTINCTmerged three distinct base rows into one view row, the reverse arrow fans out to three targets — there is no single base row (nor a single unambiguous set of them) that "setdept_id = 9" should hit, which is exactly the non-updatability condition. (AnINSERTof(9)would fail for a second reason too: it must invent an employee row with unknownname,salary.)
Ex 6 — Cells F & F′: the join view, key-preserved vs not
Forecast: guess which side of the join each column belongs to.
The geometry of a key-preserved join:

employees rows each pair one-to-one with a view row (cyan arrows), so salary is writable. But a single departments row (amber) fans out to all three view rows, so dept_name has no unique reverse target and is not writable.
- Find the key-preserved table. Each
emp_deptrow keepsemp_idunique, so every view row maps to exactly oneemployeesrow.employeesis key-preserved. Why this step? Key-preservation is precisely the "one view row ↔ one base row" condition for the join case (cell F). - Case F — update a column of the key-preserved side.
salarylives inemployees. Since the map to that table is unique, the engine rewrites toUPDATE employees SET salary = 55000 WHERE emp_id = 7;. Why this step? Unique target row ⇒ well-defined write. Many engines (Oracle, SQL Server) accept this. - Case F′ — update a column of the other side.
dept_namelives indepartments. But many employees share one department, so onedepartmentsrow backs many view rows — not key-preserved (cell F′). Changingdept_namehere would silently rename the department for everyone. The engine rejects it. Why this step? Thedepartmentsside is one→many; no unique reverse arrow. - Portability caveat. Standard SQL permits engines to reject even case F. So treat join-view writes as engine-dependent, or use an
INSTEAD OFtrigger.
Ex 7 — Cell G: UNION is a set operation
Forecast: current_staff, alumni, both, or neither?
- Look at where a row could come from. A
UNIONrow could originate incurrent_staff, oralumni, or both (duplicates removed). Why this step? The view row does not remember its source table — the mapping is not to a single table (cell G). - Try to route the insert. Should
(200, 'Zoe')go intocurrent_staff,alumni, or both? Undefined. Why this step? No unique base table ⇒ no rewrite target. - Reject. The engine refuses the insert. Why this step?
UNIONis a set operation: it merges two result sets and, crucially, removes duplicate rows, so a single view row may correspond to a row in the first branch, the second branch, or both at once. That destroys the one-view-row ↔ one-base-row pairing — the mapping is now to an unknown table, and there is no unambiguous place to send the write. (Note:UNION ALLkeeps duplicates and some engines allow writes through it when the branches are provably disjoint, so each row's source table is recoverable — but plainUNIONde-duplicates and is never updatable.)
Ex 8 — Cell H: rescuing a join view with INSTEAD OF
Forecast: which of the five view columns become a real employees row?
- Understand what the trigger replaces.
INSTEAD OFmeans "don't try your automatic rewrite — run my body instead". Why this step? You are supplying the missing map the engine could not infer (see Triggers). - Follow the body. It inserts only into
employees, usingNEW.emp_id=300, NEW.name='Ken', NEW.salary=48000, NEW.dept_id=3. Thedept_name='Ops'value is ignored —departmentsis left untouched. Why this step? You chose to treatdepartmentsas reference data, not a write target. - Mind the foreign key.
employees.dept_idalmost always referencesdepartments.dept_id. Since we are not creating a department, dept 3 must already exist indepartments, or the insert fails on the foreign-key constraint. Why this step? The trigger writes only one table; it does not conjure the referenced parent row, so referential integrity is your responsibility (see Normalization). - Result. With dept 3 present, exactly one row is added to
employees; zero rows added todepartments. If dept 3 did not exist, the whole insert is rejected by the constraint.
Ex 9 — Cell I: real-world row-level security
Forecast: both succeed / first only / second only.
- First update — inside the filter. Setting
salary = 62000for emp 12 keepsdept_id = 3true, soWITH CHECK OPTIONis satisfied. Rewrites toUPDATE employees SET salary = 62000 WHERE emp_id = 12;. Why this step? Plain base column, one↔one map (cell A/I), row stays in view — accepted. - Second update — pushes the row out. Setting
dept_id = 4would makedept_id = 3false. That row would leave the view.WITH CHECK OPTIONforbids a write that ejects the row. Why this step? This is the security point: a manager cannot "reassign an employee out of their own view" through it. - Result. Update 1 accepted (salary → 62000); update 2 rejected. Combined with
GRANT SELECT, UPDATE ON my_team(see Database Security and GRANT), the manager is boxed into dept 3.
Ex 10 — Cell J: exam twist — DELETE vs boundary NULL
Forecast: does deleting through a view drop the real customer? Does NULL leave it visible?
- Case (a) — DELETE propagates to base. The view has no data;
DELETE FROM activerewrites toDELETE FROM customers WHERE cust_id = 5 AND status = 'A';. The real customer row is gone, socustomersdrops from 10 to 9 rows. Why this step? Writes always hit the base table — deleting through a view is not "hiding", it is destroying. This is cell J's first half:DELETEthrough a simple projection+WHEREview is fully updatable and cascades to the base row. - Case (b) — boundary NULL. Set
status = NULL. In SQL,NULL = 'A'evaluates to UNKNOWN, not TRUE, so the view'sWHERE status = 'A'excludes the row. The base row survives (still 10 rows) but vanishes from the view. Why this step? Three-valued logic:NULLnever equals anything, so the row silently leaves the filter (like cell B, but via an update). WithoutWITH CHECK OPTION, this is allowed. - Contrast. (a) removes the base row entirely (10 → 9); (b) keeps it (still 10) but makes it invisible here. Same "disappears from view" outcome, completely different base-table reality — the exam trap.
Recall Self-test
Why is UPDATE dept_stats SET avg_pay = 60000 rejected? ::: AVG maps many base rows to one view row; no unique salary assignment yields a given average.
An INSERT succeeds through cheap_items but the row isn't visible — why? ::: The WHERE filters reads only; the base insert landed outside it. Fix with WITH CHECK OPTION.
On a join view, which column can you update? ::: One in the key-preserved table (unique reverse map), engine permitting.
Does DELETE FROM active WHERE cust_id = 5 delete the real customer? ::: Yes — writes propagate to the base table; the row is destroyed, not hidden.
Why does SET status = NULL hide a row from a status='A' view? ::: NULL = 'A' is UNKNOWN (not TRUE), so the row fails the filter.
See also Query Optimization and View Merging for how these rewrites are executed, and Normalization for why base tables are split (making join-views common in the first place).