skip to content

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%

answer

  1. contract you assert, engine never verifies
  2. IMMUTABLE = forever, STABLE = within a statement, VOLATILE = per row
  3. immutable unlocks expression indexes + constant folding
  4. lying downward = silently wrong index entries
  5. defaults are the pessimistic ones

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.

solid answer

~60 s

The declaration is a **contract you assert**, not something the engine verifies. Three practical tiers: - **IMMUTABLE / deterministic** — the result depends only on the arguments. No table reads, no clock, no session settings. The planner may evaluate the call once at plan time and fold it into a constant, cache it, or use the function in an **index expression** or a persisted generated column, because a stored index entry stays correct forever. - **STABLE** — the result is constant *within a single statement* for the same arguments, typically because it reads tables under one snapshot. Safe for index scans and predicate pushdown within a statement, but not for indexing. - **VOLATILE** — anything goes: `random()`, `now()` in some engines, writes, session state. The engine must call it once per row and cannot cache, fold, or reorder freely. Defaults matter: PostgreSQL defaults to VOLATILE, MySQL's routines default to NOT DETERMINISTIC. Getting it wrong in the safe direction costs performance; lying in the unsafe direction produces wrong results and corrupt expression indexes that only surface much later.

code

sql · 10 lines
sql
CREATE FUNCTION norm_email(t text) RETURNS text
  IMMUTABLE
AS $$ SELECT lower(btrim(t)) $$ LANGUAGE sql;

CREATE UNIQUE INDEX users_email_uq ON users (norm_email(email));

CREATE FUNCTION active_tier(p_user bigint) RETURNS text
  STABLE
AS $$ SELECT tier FROM subscription WHERE user_id = p_user $$ LANGUAGE sql;
-- CREATE INDEX ... (active_tier(id)) would be rejected: reads tables, may change

go deeper

for a junior

Recall the definition: it tells the database whether the same inputs always give the same answer, and the database uses that to skip recomputation.

for a middle

Name the three tiers with an example each, state that the engine trusts and never checks the claim, and mention constant folding and expression indexes as the payoff.

for a senior

Lead with the correctness failure — an over-optimistic label plus an expression index yields silently wrong results — and mention time-zone and collation dependencies as the usual culprits.

for a principal

Treat the volatility, cost, row-estimate, and parallel-safety declarations as a small contract surface between application code and the planner, and discuss enforcing correct labelling via review and migration policy since the engine cannot.

## The problem the declaration solves The optimizer would love to be clever with function calls: evaluate one once and reuse it, hoist it out of a loop, turn `WHERE f(x) = 5` into an index lookup, or store `f(x)` in an index so a query never recomputes it. All of those are only legal if the function behaves like a *mathematical* function — same inputs, same output. SQL cannot inspect an arbitrary routine body and prove that. So the engine asks the author to declare it, and then trusts the declaration completely. ## The tiers **Immutable / deterministic.** The result is a pure function of the arguments, forever. `upper(text)`, `a + b`, a formatting routine. It may not read tables, may not call the clock, may not depend on session settings such as time zone or collation-affecting locale, and may not write. Because the answer never changes, the engine can: - fold the call to a constant at planning time when arguments are constants; - evaluate once and reuse for repeated identical arguments; - allow the expression in an **expression index** or a stored generated column, because an entry computed today must still match tomorrow; - use it inside CHECK constraints, since a row that passed validation must still pass on re-check. **Stable.** The result is constant for the duration of a single statement, given the same arguments, but may differ between statements. The canonical case is a function that reads tables: within one statement it sees one snapshot, so it is consistent there. Also anything sensitive to session state that does not change mid-statement, such as the current time zone. The engine may use a stable function in an index *scan* — `WHERE indexed_col = stable_f(1)` can be evaluated once and turned into a lookup key — but it may **not** store the result in an index. **Volatile.** No promise at all. `random()`, sequence generators, functions that write, functions that read data they themselves changed. The engine must evaluate them once per row in the order the plan dictates, and cannot cache, fold, or move them across the plan. ## What the wrong declaration costs you Declaring something *more* volatile than it really is is merely slow: repeated evaluation, no index-expression eligibility, no constant folding. This is why the safe defaults exist — PostgreSQL's default is VOLATILE, MySQL's is NOT DETERMINISTIC. Declaring something *less* volatile than it really is is a correctness bug, and it is quiet. The classic disaster: mark a function IMMUTABLE, build an expression index on it, then change what the function returns — a lookup table changes, a time-zone-dependent conversion drifts, someone edits the body. The index now holds values that no longer match what the function would produce. Queries using the index return one answer, queries that recompute return another, and nothing errors. Recovering requires rebuilding every index and generated column that depends on the function. Subtler versions of the same trap: a timestamp-to-date conversion that silently depends on the session time zone; a text function whose output depends on collation or locale; a function that "only reads a config table that never changes" — until it does. ## Adjacent knobs Some engines pair volatility with other planner hints on routines: an estimated **cost** per call, an estimated **row count** for set-returning functions, a **leakproof**-style flag saying the function cannot leak argument values through error messages (which controls whether it may be pushed below a security barrier), and a **parallel-safety** class saying whether the routine may run in a parallel worker. They are separate promises with the same shape: the author asserts, the planner exploits, and a false assertion is on the author. ## Replication also cares With statement-based replication, a non-deterministic routine replayed on a replica produces different data than on the primary — which is why MySQL historically warned or refused when non-deterministic routines were created under binlog formats that replay statements. Row-based replication removes that concern by shipping the resulting rows instead. ## How to answer in an interview Say the declaration is a contract, name the three tiers with an example each, state at least two optimizations it unlocks (constant folding / caching, and index expressions), and finish on the failure mode: an over-optimistic declaration plus an expression index equals silently wrong results. That sequence covers everything the question is testing.

  • Why is a function that converts a timestamp to a date a classic mislabelling trap?
    Because the conversion usually depends on the session time zone, so the same input can yield different dates in different sessions, which makes it stable at best rather than immutable. If it is declared immutable and used in an expression index, entries built by a session in one time zone disagree with lookups from another, and queries silently miss rows. The fix is to make the conversion take an explicit time zone argument so it really is a pure function of its inputs.
  • What breaks if you edit the body of a function that already backs an expression index?
    Existing index entries keep the values produced by the old body, while any new or re-computed rows use the new one, so the index and the table disagree. Queries that use the index return different results from queries that scan and recompute, with no error raised. After any such change, every dependent index and stored generated column must be rebuilt.

It is a label on a jar: the kitchen trusts "shelf-stable" and stops refrigerating it. Mislabel it and nobody notices until people get sick.

saying these in an interview costs you the question

  • Believing the engine verifies determinism rather than trusting the declaration.
  • Marking table-reading functions IMMUTABLE because "the table rarely changes".
  • Thinking the classification only affects speed and never correctness.
  • Assuming a function is deterministic by default — the common defaults are the volatile ones.
  • Confusing determinism with purity of side effects; a function can be side-effect-free and still non-deterministic (e.g. reads the clock).

context