DSA, Database System & Operating System β Data Models, Normalization, and SQL, NEC licence examination syllabus (Nepal Engineering Council).
Virtual tables, declarative invariants, and code that runs automatically on data change.
patient table; the billing view exposes the account number and not the diagnosis, the clinical view exposes the diagnosis and not the account, and the research view exposes neither β only anonymised age brackets. Grant access to the view and revoke it on the base table, and the restriction is enforced by the engine rather than by everyone remembering not to select the wrong column. Triggers are the other half: they are how an audit log gets written even when someone updates a row from a SQL console. Search "row level security vs views" for the modern refinement of this idea.-- verified against PostgreSQL 18
CREATE TABLE student (roll INT PRIMARY KEY, name TEXT NOT NULL,
dept CHAR(6), marks NUMERIC(5,2));
INSERT INTO student VALUES
(101,'Ram','ACtE07',87.5), (102,'Sita','ACtE07',91.0),
(103,'Hari','AExE01',55.0), (104,'Gita','AExE01',68.0);
-- ===== 1. a simple, updatable view =====
CREATE VIEW v_cs AS
SELECT roll, name, marks FROM student WHERE dept = 'ACtE07';
SELECT * FROM v_cs ORDER BY roll;
-- roll | name | marks
-- -----+------+-------
-- 101 | Ram | 87.50
-- 102 | Sita | 91.00
-- UPDATE through the view reaches the base table
UPDATE v_cs SET marks = 88.0 WHERE roll = 101;
SELECT roll, marks FROM student WHERE roll = 101;
-- roll | marks
-- -----+-------
-- 101 | 88.00 <- base table changed β
-- ===== 2. an AGGREGATE view is read-only =====
CREATE VIEW v_summary AS
SELECT dept, COUNT(*) AS n, AVG(marks) AS avg_m
FROM student GROUP BY dept;
SELECT * FROM v_summary ORDER BY dept;
-- dept | n | avg_m
-- -------+---+---------------------
-- ACtE07 | 2 | 89.5000000000000000
-- AExE01 | 2 | 61.5000000000000000
UPDATE v_summary SET n = 99 WHERE dept = 'ACtE07';
-- ERROR: cannot update view "v_summary"
-- DETAIL: Views containing GROUP BY are not automatically
-- updatable.
-- HINT: To enable updating the view, provide an INSTEAD OF
-- UPDATE trigger ...
-- ===== 3. a view for SECURITY =====
CREATE VIEW v_public AS
SELECT roll, name, dept FROM student; -- no marks
GRANT SELECT ON v_public TO readonly_role;
REVOKE ALL ON student FROM readonly_role;
-- readonly_role CANNOT reach marks at all, even with a
-- hand-written query β it has no privilege on the base
-- table. That is enforcement, not convention.
-- ===== 4. a MATERIALIZED view for an expensive query =====
CREATE MATERIALIZED VIEW mv_dept_stats AS
SELECT dept, COUNT(*) AS n, AVG(marks) AS avg_m,
MAX(marks) AS top
FROM student GROUP BY dept;
CREATE UNIQUE INDEX ON mv_dept_stats (dept);
-- ^ possible ONLY because it is materialised;
-- a plain view cannot be indexed
-- at this point ACtE07 has 2 students, so the materialised
-- view holds n = 2. Now add a third:
INSERT INTO student VALUES (107,'New','ACtE07',99.0);
SELECT n FROM mv_dept_stats WHERE dept = 'ACtE07';
-- n
-- ---
-- 2 <- STALE. The base table has 3 rows now, but the
-- stored snapshot still says 2.
REFRESH MATERIALIZED VIEW mv_dept_stats;
SELECT n FROM mv_dept_stats WHERE dept = 'ACtE07';
-- n
-- ---
-- 3 <- recomputed from the base table β
-- The gap between those two SELECTs is the staleness window.
-- Nothing errors, nothing warns β a query against a
-- materialised view is always a query against a snapshot,
-- and it is your job to know how old that snapshot is.
The staleness of a materialized view is not a bug to work around β it is the whole trade. You are choosing to read a cheap snapshot instead of computing an expensive truth. The design question is only "how stale is acceptable", and the answer belongs to the business, not the DBA. Refresh on a schedule for a dashboard; refresh in a trigger if it must be near-real-time; use a plain view if it must be exact.
-- ===== 1. BEFORE trigger: validate and normalise =====
CREATE OR REPLACE FUNCTION normalise_student()
RETURNS TRIGGER AS $$
BEGIN
-- clean the data before it is stored
NEW.name := INITCAP(TRIM(NEW.name));
IF NEW.marks IS NULL THEN NEW.marks := 0; END IF;
-- reject what a CHECK cannot express
IF NEW.marks > 100 THEN
RAISE EXCEPTION 'marks % exceeds 100', NEW.marks;
END IF;
RETURN NEW; -- must return NEW, or the row is dropped
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_normalise
BEFORE INSERT OR UPDATE ON student
FOR EACH ROW EXECUTE FUNCTION normalise_student();
INSERT INTO student (roll,name,dept,marks)
VALUES (108,' raM baHAdur ','ACtE07',NULL);
SELECT roll, name, marks FROM student WHERE roll = 108;
-- roll | name | marks
-- -----+-------------+-------
-- 108 | Ram Bahadur | 0.00
-- trimmed, title-cased, and NULL defaulted β
-- ===== 2. AFTER trigger: an audit log =====
CREATE TABLE student_audit (
audit_id SERIAL PRIMARY KEY,
roll INTEGER,
action CHAR(6),
old_marks NUMERIC(5,2),
new_marks NUMERIC(5,2),
changed_by TEXT DEFAULT CURRENT_USER,
changed_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE OR REPLACE FUNCTION audit_student()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO student_audit (roll,action,new_marks)
VALUES (NEW.roll,'INSERT',NEW.marks);
ELSIF TG_OP = 'UPDATE' THEN
-- only log an ACTUAL change
IF OLD.marks IS DISTINCT FROM NEW.marks THEN
INSERT INTO student_audit
(roll,action,old_marks,new_marks)
VALUES (NEW.roll,'UPDATE',OLD.marks,NEW.marks);
END IF;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO student_audit (roll,action,old_marks)
VALUES (OLD.roll,'DELETE',OLD.marks);
RETURN OLD;
END IF;
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_audit
AFTER INSERT OR UPDATE OR DELETE ON student
FOR EACH ROW EXECUTE FUNCTION audit_student();
UPDATE student SET marks = 95 WHERE roll = 101;
UPDATE student SET marks = 95 WHERE roll = 101; -- no change
DELETE FROM student WHERE roll = 104;
SELECT roll, action, old_marks, new_marks FROM student_audit
ORDER BY audit_id;
-- roll | action | old_marks | new_marks
-- -----+--------+-----------+-----------
-- 101 | UPDATE | 88.00 | 95.00
-- 104 | DELETE | 68.00 |
-- Only ONE update logged β the second changed nothing, and
-- IS DISTINCT FROM correctly treated it as a no-op.
-- ===== 3. INSTEAD OF trigger: make an aggregate view
-- writable is impossible, but a JOIN view can be =====
CREATE VIEW v_student_dept AS
SELECT s.roll, s.name, s.dept, d.dname
FROM student s LEFT JOIN department d ON d.code = s.dept;
CREATE OR REPLACE FUNCTION ins_student_dept()
RETURNS TRIGGER AS $$
BEGIN
-- create the department if it does not exist
INSERT INTO department (code, dname)
VALUES (NEW.dept, COALESCE(NEW.dname, NEW.dept))
ON CONFLICT (code) DO NOTHING;
INSERT INTO student (roll, name, dept)
VALUES (NEW.roll, NEW.name, NEW.dept);
RETURN NEW;
END; $$ LANGUAGE plpgsql;
CREATE TRIGGER trg_ins_v
INSTEAD OF INSERT ON v_student_dept
FOR EACH ROW EXECUTE FUNCTION ins_student_dept();
-- now INSERT INTO v_student_dept works, writing to BOTH
-- base tables β something no automatically-updatable view
-- can do.
CREATE POLICY own_rows ON student USING (roll = current_user_roll()) β and every query, from any client, is silently filtered. Multi-tenant SaaS applications are built on this, because it makes "tenant A cannot see tenant B's data" an engine guarantee rather than a WHERE clause everyone must remember. Search "PostgreSQL row level security multi-tenant".WITH CHECK OPTION using the disappearing-row example. For triggers, know BEFORE vs AFTER vs INSTEAD OF, ROW vs STATEMENT granularity, and the NEW/OLD availability table. State that ASSERTION is in the standard but unimplemented, so multi-table rules use triggers.Create a free account to tick topics off, take notes as you read, watch the video lessons and get a day-by-day study plan built around your exam date.
Loadingβ¦