skip to content

Views, Procedures & Server-Side Logic

I learn the database objects that live beyond plain tables — views, materialized views, stored procedures, triggers, generated columns, sequences, and temp tables — and when pushing logic into the engine helps or hurts. Interviewers use this area to test whether I understand what the engine can do server-side and can argue the business-logic-in-the-database trade-off.

part ofRelational database conceptsoverview, primer and where to startread it →
on this pageshow

questions

page 1 of 2

What is a generated (computed) column in a relational table, and what is the practical difference between declaring it STORED and declaring it VIRTUAL?

level: juniorimportance: must knowfreq 42%

answer

  1. GENERATED ALWAYS AS (expr) - engine owns the value
  2. STORED = on disk, free reads, wider rows
  3. VIRTUAL = computed on read, no storage
  4. same row only, deterministic only
  5. index needs STORED in most engines

basics

~20 s

A column the engine derives from other columns of the same row via a fixed expression; you never write to it. STORED materializes the value on disk at write time (uses space, free to read, indexable). VIRTUAL stores nothing and recomputes it on every read.

solid answer

~50 s

A generated column is declared `GENERATED ALWAYS AS (<expression>)`. The engine owns the value: you cannot INSERT or UPDATE it, and the expression may reference only other columns of the same row. - **STORED** - computed once at insert/update time and physically persisted. Reads are free, it can be indexed and referenced like an ordinary column, but it widens every row, adds write CPU and WAL/redo volume, and changing the expression means rewriting the table. - **VIRTUAL** - nothing is persisted; the value is computed each time it is selected. Writes stay cheap and adding the column is usually a metadata-only change, but every read pays the expression cost, and whether it can be indexed depends on the engine. Rule of thumb: STORED when the value is read or filtered far more often than written, or when you need an index on it; VIRTUAL when the table is write-heavy, the expression is trivial, and you mainly want a convenient named projection.

code

sql · 12 lines
sql
CREATE TABLE orders (
  id            bigint PRIMARY KEY,
  unit_price_c  integer NOT NULL,
  qty           integer NOT NULL,
  email         text    NOT NULL,
  total_c       integer GENERATED ALWAYS AS (unit_price_c * qty) STORED,
  email_lower   text    GENERATED ALWAYS AS (lower(email))      VIRTUAL
);

-- rejected: the engine owns generated values
INSERT INTO orders (id, unit_price_c, qty, email, total_c)
VALUES (1, 500, 2, '[email protected]', 999);

go deeper

for a junior

Know the definition, that the database computes and owns the value, and the one-line STORED-versus-VIRTUAL difference (on disk versus on read).

for a middle

Add the cost model - row width, write amplification, and read CPU - plus the same-row and determinism restrictions, and which choice lets you build an index.

for a senior

Pitch it as a materialization decision driven by read/write ratio and access paths, and mention the migration cost of adding or changing a STORED expression on a large hot table.

for a principal

Frame it as where a derived value should live at all - column, view, materialized view, or application - and what each choice implies for schema evolution, storage growth, and cross-engine portability.

## The idea A generated column (SQL Server calls it a computed column, Oracle a virtual column) is a table column whose value is not supplied by the client but derived by the database from an expression over other columns of the same row. It is declared with the `GENERATED ALWAYS AS (...)` clause. `GENERATED ALWAYS` is the point: the value is always the engine's to compute, so any attempt to insert or update it is an error. That is what makes it different from a plain column that the application happens to fill in with the same formula - here the invariant cannot be violated by a buggy code path, a bulk load, or a manual fix-up in a database console. ## STORED A STORED (SQL Server: PERSISTED) generated column is evaluated at write time and the result is written into the row on disk like any other value. Consequences: - Reads cost nothing extra - the value is already there, so scans, sorts, and comparisons see a plain column. - It can be indexed everywhere that supports generated columns at all, and in most engines it can carry constraints such as UNIQUE or CHECK. - Every row gets wider. That means more pages to scan, more buffer cache consumed, more WAL/redo written per change, and possibly off-row storage for large text values. - Every INSERT and every UPDATE that touches an input column pays the expression's CPU cost, and re-evaluates it even if the result is unchanged. - Because the bytes are on disk, adding a STORED column to an existing large table generally requires rewriting the whole table, and changing the expression later requires the same rewrite. ## VIRTUAL A VIRTUAL generated column stores nothing. The expression is evaluated whenever the column is read, so the column is essentially a per-table named projection that the engine substitutes at query time. Consequences: - Zero storage, zero extra write cost, and adding one is usually an instant catalog-only change. - Every read that projects or filters on the column pays the CPU cost, once per row touched. For an expensive expression over a wide scan, that can dominate. - Indexability varies. MySQL can build a secondary index on a VIRTUAL column (the index materializes the computed value even though the row does not). PostgreSQL gained virtual generated columns in version 18 and does not allow indexing them; before 18 only STORED existed there. SQL Server can index a computed column only if you mark it PERSISTED or the expression is deterministic and precise. ## Choosing between them Ask three questions. First, read/write ratio: a column read on every query but written rarely argues for STORED; a column on a high-ingest table read only by the occasional report argues for VIRTUAL. Second, do you need an index, a unique constraint, or a foreign key on it? Then STORED is the portable answer. Third, how expensive is the expression? Concatenating two short strings is nothing; parsing JSON, normalizing Unicode, or running a heavy formula over millions of rows per scan is not. A useful mental model: STORED trades write cost and disk for read cost; VIRTUAL trades read cost for write cost and disk. It is the same materialization tradeoff you make between a view and a materialized view, but at column granularity. ## What both share Both kinds are read-only to clients, both may reference only the current row (no subqueries, no other tables, no aggregates), and both require a deterministic expression. Neither is a place to put audit timestamps or anything drawn from the environment - `now()` and `random()` are rejected. Both are visible to the query planner as ordinary column references, which is exactly why they are attractive: application code, ORMs, and ad-hoc SQL can select and filter on them without knowing the formula. ## Defaults and syntax notes When the keyword is omitted, engines differ: MySQL defaults to VIRTUAL, PostgreSQL 18 defaults to VIRTUAL, and older PostgreSQL requires STORED explicitly. Always write the keyword out so the intent survives a port and a code review.

  • Can a generated column reference a column from another table, or an aggregate over the same table?
    No. The expression is evaluated per row with only that row's values in scope, so no subqueries, no joins, and no aggregates are allowed. Anything cross-row would have to be recomputed when some other row changed, which the per-row evaluation model cannot detect. Cross-row derivation belongs in a view, a materialized view, or explicit application/trigger logic.
  • If a VIRTUAL column costs nothing to store, why not make everything virtual?
    Because the cost moves to read time and is paid per row per query. A virtual column over a 50-million-row table scanned by a nightly report is fine; the same expression evaluated in a filter on a hot OLTP path multiplies CPU on your least scalable tier. You also lose portable indexability, so predicates on the column may force full scans.

STORED is meal-prepping on Sunday: freezer space consumed, dinner instant. VIRTUAL is cooking each night: nothing in the freezer, but you pay every time you eat.

saying these in an interview costs you the question

  • Saying you can INSERT or UPDATE a generated column with an explicit value (GENERATED ALWAYS forbids it)
  • Claiming VIRTUAL columns save resources with no downside - they move cost to every read
  • Believing a generated column can aggregate or look at other rows or tables
  • Assuming any generated column can be indexed in any engine
  • Using a generated column for now() or a random value - the expression must be deterministic

context

open as a page

What is a materialized view, how does it differ from an ordinary non-materialized view, and when would you introduce one?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A materialized view stores the actual result rows of its query on disk, like a table. An ordinary view stores only the query text and recomputes it on every read. You trade freshness and storage for cheap reads of expensive queries.

open as a page

What is a database sequence object, and how do IDENTITY columns and auto-increment/serial columns relate to it?

level: juniorimportance: must knowfreq 66%

basics

~20 s

A sequence is a standalone database object that hands out unique increasing numbers on request, independent of any table. An IDENTITY or serial column is a column wired to such a generator so inserts get a value automatically. Same machinery, different packaging.

open as a page

In a relational database, what is the difference between a stored procedure and a stored function, and when would you reach for each?

level: juniorimportance: must knowfreq 68%

basics

~20 s

A function returns a value and is called inside an expression or query. A procedure is invoked by a CALL statement, returns data through output parameters or result sets, and in most engines may commit or roll back. Functions compute values; procedures perform units of work.

open as a page

What is a temporary table in a relational database, and how do its visibility and lifetime differ from an ordinary table?

level: juniorimportance: must knowfreq 52%

basics

~20 s

A temporary table holds intermediate rows for one session (or one transaction). Its data is private to the creating session, it lives in a special temporary namespace, it is dropped automatically at session or transaction end, and it is typically unlogged and never crash-recovered.

open as a page

In a relational database, what is the difference between a BEFORE trigger and an AFTER trigger on an INSERT or UPDATE, and what can each one do that the other cannot?

level: juniorimportance: must knowfreq 55%

basics

~20 s

A BEFORE trigger runs before the row change is applied and can still modify the incoming row or reject the statement. An AFTER trigger runs once the change is applied, so it sees final values including generated keys, but changes it makes to the row image are ignored.

open as a page

Can an application run INSERT, UPDATE or DELETE statements against a database view, and if so what data actually changes?

level: juniorimportance: must knowfreq 55%

basics

~20 s

Yes, if the view is updatable. A view stores no rows of its own, so the engine rewrites the write into a write on the underlying base table. Simple views that only project columns and filter rows are usually updatable; views with aggregation or DISTINCT are not.

open as a page

What is a database view, and what problems do teams typically create one to solve?

level: juniorimportance: must knowfreq 60%

basics

~20 s

A view is a named query stored in the database. Selecting from it runs the underlying query against the live tables — it holds no data of its own. Teams use views to name a complex query once, hide columns or rows for access control, and give applications a stable shape while the tables underneath change.

open as a page

What are the main arguments for and against implementing business logic inside the database - in stored procedures and functions - rather than in the application tier?

level: middleimportance: must knowfreq 55%

basics

~20 s

For: fewer network round trips, set-based processing next to the data, one enforcement point for every client, naturally atomic multi-statement work. Against: harder to test and version, deployment is replace-in-place, it burns your least scalable CPU, it fights ORMs, and it ties you to one vendor's language.

open as a page

Explain the difference between a complete refresh and an incremental (fast) refresh of a materialized view, and what a database needs in place for the incremental form to be possible.

level: middleimportance: must knowfreq 58%

basics

~20 s

Complete refresh re-runs the whole defining query and replaces all stored rows. Incremental (fast) refresh applies only the changes since the last refresh, using a change log of the base tables. It needs recorded deltas and a query shape the engine can maintain incrementally.

open as a page

Your table's auto-generated primary keys have gaps — values 41, 42, then 57. Explain how a rollback, a crash, or ordinary concurrency can produce that, and whether it indicates a bug.

level: middleimportance: must knowfreq 62%

basics

~20 s

It is normal, not a bug. Sequence allocation is non-transactional: a value handed out is consumed even if the transaction rolls back, a crash discards cached values, and concurrent sessions interleave allocations. Sequences guarantee uniqueness, never contiguity.

open as a page

Many engines let you declare a stored function as deterministic, or classify it as IMMUTABLE, STABLE, or VOLATILE. What does that declaration actually mean, and what does the database do with it?

level: middleimportance: must knowfreq 52%

basics

~20 s

It is a promise about how stable the result is: deterministic/IMMUTABLE means same inputs always give the same output, STABLE means constant within one statement, VOLATILE means anything. The optimizer uses the promise to cache results, fold constants, push predicates, and allow use in indexes. A false promise silently corrupts results.

open as a page

When would you stage an intermediate result in a temporary table instead of leaving it as a common table expression, a derived table, or a table variable?

level: middleimportance: must knowfreq 46%

basics

~20 s

Stage in a temporary table when the intermediate is read more than once, when the optimizer needs real statistics on it, or when it should be indexed. Otherwise keep it inline so the optimizer can see and reshape the whole query. Table variables sit in between: real storage, but historically poor cardinality estimates.

open as a page

Explain the difference between a row-level trigger (FOR EACH ROW) and a statement-level trigger (FOR EACH STATEMENT), including exactly how many times each fires when one UPDATE matches zero rows and when it matches a thousand rows.

level: middleimportance: must knowfreq 50%

basics

~20 s

A row-level trigger fires once per affected row: zero times for a zero-row UPDATE, a thousand times for a thousand rows. A statement-level trigger fires once per statement regardless, including when zero rows match. Row-level sees individual old and new values; statement-level sees the change as a set, if at all.

open as a page

Triggers execute inside the transaction of the statement that fired them. What practical consequences does that have for error handling, locking, latency, and for calling an external system such as an HTTP API from trigger code?

level: middleimportance: must knowfreq 46%

basics

~20 s

Everything a trigger writes commits or rolls back with the caller's transaction, and an error in the trigger aborts the caller's statement. Trigger work extends the transaction, so locks are held longer and the caller waits for it. External calls cannot be rolled back, so they must not be made from a trigger.

open as a page

Which constructs in a view's defining query make the view non-updatable to the database engine, and what is the underlying reason the engine refuses those writes?

level: middleimportance: must knowfreq 50%

basics

~20 s

Aggregates, GROUP BY/HAVING, DISTINCT, set operations like UNION, window functions, LIMIT, and most multi-table joins. All of them break the one-to-one mapping from a view row back to a single base-table row and column, so the engine cannot decide which row to modify.

open as a page

When a query selects from a view, how does the query optimizer handle the view's definition, and does wrapping a query in a view add runtime cost?

level: middleimportance: must knowfreq 45%

basics

~20 s

The optimizer expands (inlines) the view definition into the calling query and plans the combined statement, so predicates from outside can be pushed inside and unused work can sometimes be eliminated. The view itself adds no runtime cost. Constructs like aggregation, DISTINCT or window functions act as optimization fences that block that merging.

open as a page

When a stored routine is created with the SQL SECURITY DEFINER clause instead of SECURITY INVOKER, what changes at execution time, and how do you deploy such a routine safely?

level: seniorimportance: must knowfreq 44%

basics

~20 s

SECURITY INVOKER runs the body with the caller's privileges; SECURITY DEFINER runs it with the owner's. Definer routines let a low-privileged caller perform a narrow privileged action, but they are a privilege-escalation surface: pin the name-resolution search path, revoke PUBLIC execute, own them by a limited role, and validate every input.

open as a page

A nightly job runs a single UPDATE that touches five million rows and now takes hours; the same UPDATE against a structurally identical table with no triggers finishes in minutes. Explain concretely how row-level triggers produce that cost, and what you would change.

level: seniorimportance: must knowfreq 48%

basics

~20 s

A row-level trigger runs once per affected row, so a set-based update becomes five million procedure invocations, often each with its own query. It also multiplies written bytes, log volume and locks, all inside one long transaction. Fix it with statement-level triggers over transition tables, batching, or doing the derived work in the statement itself.

open as a page

A trigger on table A writes a row into table B, and a trigger on table B writes back into table A. Walk through what happens when an application inserts one row into A, and explain how relational engines limit cascading or recursive trigger firing and how you would design around it.

level: seniorimportance: must knowfreq 50%

basics

~20 s

Each trigger's own DML fires more triggers, so A to B to A can loop. Engines stop it with a nesting-depth cap or by disabling self-recursion by default, and the loop aborts the whole transaction. Fix it with change guards, idempotent writes, or by moving the second hop into application code.

open as a page

Why do teams often say that database triggers make an application harder to debug and maintain, and what practices reduce that pain when you decide to keep them?

level: juniorimportance: should knowfreq 40%

basics

~20 s

Triggers run invisibly: the application code shows one INSERT, but rows change in tables nobody named, and the logic lives outside the application repository. That breaks the trail from symptom to cause. Keeping them manageable means versioning them like code, testing them, and making them discoverable.

open as a page

Why do relational engines require the expression behind a generated column to be deterministic (immutable), and what kinds of expressions get rejected as a result?

level: middleimportance: should knowfreq 28%

basics

~20 s

Because the stored bytes, any index on them, and any replica or restored dump must all agree. If the expression could return different values at different times, recomputation would disagree with what is on disk. So clock, random, session, and cross-row expressions are rejected.

open as a page

You need fast case-insensitive lookups on an email column and fast filtering on a field buried inside a JSON document. How does an indexed generated column solve this, and how does it compare with indexing the expression directly?

level: middleimportance: should knowfreq 36%

basics

~20 s

Add a STORED generated column holding lower(email) or the extracted JSON field, then index that column. Queries filter on the column name and get an index seek. An expression index stores the same computed keys without a visible column, and the planner matches it when the query repeats the expression literally.

open as a page

An overnight job reads rows one at a time from the application, decides something per row, and writes each one back, and it is far too slow. How do round trips and transaction scope explain this, and what are your options short of moving all the logic into the database?

level: middleimportance: should knowfreq 42%

basics

~20 s

Each statement pays a network round trip plus parse and plan cost, so per-row work is latency-bound: a million rows at half a millisecond each is minutes of pure waiting. Fix it by expressing the work as set-based SQL, or batching, before considering a server-side procedure.

open as a page

After inserting a row whose primary key was produced by the database's generator, how do you reliably obtain the value assigned to your row when many sessions insert concurrently?

level: middleimportance: should knowfreq 52%

basics

~20 s

Get it from the insert itself — a RETURNING/OUTPUT clause or the driver's generated-keys API — or from the session-scoped last-value call for your own session. Never query MAX(id) or read shared sequence state: those see other sessions' rows.

open as a page

A stored procedure needs to hand several values and a set of rows back to the calling application. What mechanisms does a relational database offer for that, and what are their tradeoffs?

level: middleimportance: should knowfreq 42%

basics

~20 s

Scalars come back through OUT or INOUT parameters. Row sets come back either as a result set the procedure emits to the client, as a returned cursor handle the caller iterates, or by writing to a temporary table the caller then queries. Functions instead return a value or a table directly.

open as a page

When creating a temporary table, SQL offers ON COMMIT PRESERVE ROWS, ON COMMIT DELETE ROWS, and ON COMMIT DROP. What does each do, and when would you choose each?

level: middleimportance: should knowfreq 36%

basics

~20 s

PRESERVE ROWS keeps the table and its data until the session ends (the usual default). DELETE ROWS keeps the table definition but empties it at every COMMIT. DROP removes the whole table at COMMIT. Choose DROP for one-transaction scratch space, DELETE ROWS to reuse a definition across many transactions, PRESERVE for session-long staging.

open as a page

A table has three separate AFTER INSERT row-level triggers on it, written by three different teams. What determines the order in which they execute, and why do experienced engineers avoid writing triggers whose correctness depends on that order?

level: middleimportance: should knowfreq 42%

basics

~20 s

Order is engine-defined, not portable: some engines order by trigger name, some by creation time, some let you pin only a first and last trigger. Renames, re-creations and dump restores silently change it, so keep triggers order-independent.

open as a page

What does an INSTEAD OF trigger do differently from BEFORE and AFTER triggers, and on what kind of database object is it normally defined?

level: middleimportance: should knowfreq 36%

basics

~20 s

BEFORE and AFTER triggers run around an operation the engine still performs. An INSTEAD OF trigger replaces it: the original INSERT, UPDATE or DELETE never executes, and whatever the trigger body writes is the entire effect. It is normally defined on a view.

open as a page

What does the WITH CHECK OPTION clause on a view definition do, and how does WITH LOCAL CHECK OPTION differ from WITH CASCADED CHECK OPTION?

level: middleimportance: should knowfreq 40%

basics

~20 s

WITH CHECK OPTION makes the engine reject writes that would produce rows the view itself cannot see. LOCAL enforces only the predicates of the view where the clause is written; CASCADED also enforces the predicates of every underlying view in the chain.

open as a page

showing 1–30 of 46