skip to content

What determines the data type of a CASE expression whose branches return different types?

level: seniorimportance: nice to knowfreq 30%

answer

  1. The whole expression yields one column
  2. Branches must agree on something
  3. Not decided per row
  4. Related numeric types widen; unrelated ones fail
  5. CAST makes the contract explicit

basics

~20 s

A CASE yields one value of one type, so the engine derives a single result type from all THEN and ELSE results. Branches must be type-compatible; unrelated types such as text and timestamp are a type error in strictly typed engines.

solid answer

~50 s

A `CASE` is a value expression producing **one column of one declared type**, so the engine unifies the types of every `THEN` result and the `ELSE` result into a single result type. Compatible numeric types widen to the more general one (an `INTEGER` branch alongside a `DECIMAL` branch gives a decimal result), and character types generally unify to a common character type. Unrelated families — say a text literal in one branch and a `TIMESTAMP` column in another — cannot be unified, and strictly typed engines raise a type error; dynamically typed SQLite is happy to store either. The portable habit is to `CAST` every branch to the type you actually want rather than letting the engine choose, especially where a character result's declared length or a numeric result's scale is derived from the branch types rather than from the values returned. That derived type is what a `CREATE TABLE AS SELECT` or a view definition will bake in.

code

sql · 11 lines
sql
-- rejected by a strictly typed engine: character vs timestamp branches
SELECT order_id,
       CASE WHEN shipped_at IS NULL THEN 'pending' ELSE shipped_at END
FROM orders;

-- one deliberate common type
SELECT order_id,
       CASE WHEN shipped_at IS NULL THEN 'pending'
            ELSE CAST(shipped_at AS VARCHAR(30))
       END AS shipped_label
FROM orders;

go deeper

for a junior

Know that all branches of a CASE must return compatible types, because the expression produces one column with one type.

for a middle

Explain the derivation: types are unified across all THEN and ELSE results, related numerics widen, and unrelated families are a type error before any row is read.

for a senior

Show that you pin types with CAST where the result feeds a view, a CTAS or a client contract, and that you keep typed data typed rather than mixing display strings into it.

for a principal

Own the boundary question: presentation markers such as 'pending' belong in the rendering layer, and letting derived expression types define schema columns is a contract you should be deliberate about.

## One expression, one type SQL is a typed language at the column level: every column of a result set has one declared data type, for every row. A `CASE` is an expression that can produce a column, so it must resolve to a single type too. The engine derives that type from the **set of result expressions** — all the `THEN` results plus the `ELSE` result — and applies it to the whole CASE. It does not vary per row, and it is not simply the type of whichever branch happened to fire. ## Compatible branches: widening When the branch types belong to the same family, the engine picks a type that can hold all of them: ```sql CASE WHEN discounted THEN 0.5 ELSE 1 END ``` One branch is exact numeric with a fractional part, the other an integer; the result is numeric with a scale, not an integer. This matters because the *outcome* changes: if you expected an integer result you may be surprised by `1.0` in the output, and if you expected a decimal you must ensure at least one branch supplies the scale. The same widening logic applies among character types, where the result is a character type long enough for the branches. A related subtlety: the declared length or precision of the result generally follows the **branch types**, not the values actually returned on a given row. A branch returning a literal declared as a short character type can therefore constrain a result that another branch would have filled with something longer. Engines differ in how aggressively they widen, so if the exact declared type matters — a view definition, a `CREATE TABLE AS SELECT`, a client that reads metadata — pin it with an explicit `CAST` rather than trusting the derivation. ## Incompatible branches: a type error ```sql SELECT order_id, CASE WHEN shipped_at IS NULL THEN 'pending' ELSE shipped_at END FROM orders; ``` Here one branch is character data and the other a timestamp. There is no reasonable common type, so a strictly typed engine rejects the statement — at parse or plan time, before any row is examined, which is a mercy: the failure is deterministic rather than data-dependent. Fix it by deciding what the column means and casting to that: ```sql SELECT order_id, CASE WHEN shipped_at IS NULL THEN 'pending' ELSE CAST(shipped_at AS VARCHAR(30)) END AS shipped_label FROM orders; ``` If you instead want a real timestamp column with a missing marker, the honest answer is that the marker must be NULL — a timestamp column cannot hold the word `'pending'`. That is often the better design: keep the typed column typed, and derive the label separately for display. SQLite is the notable exception to the strictness: its type system attaches affinities to columns rather than enforcing expression types, so the mixed-branch CASE runs and yields values of different storage classes per row. Portable code should not rely on that. ## The NULL branch A bare `NULL` literal in a branch — or the implicit `ELSE NULL` — carries no type of its own, so it takes the type derived from the other branches. That is usually what you want. Where every result is NULL, or where a NULL branch meets an untyped parameter marker, engines may be unable to infer anything and will ask you to be explicit; `CAST(NULL AS DATE)` resolves it. ## Where the derived type escapes the query The derived type is not a local detail. It becomes: - the column type of a view built over the query; - the column type created by `CREATE TABLE AS SELECT`; - the type reported through result-set metadata, which client drivers use to choose how to materialise the value; - the type used when the CASE result is compared or joined against something else, which can introduce an implicit conversion. So an unpinned CASE type propagates: a query that today produces a wide-enough character column can start truncating or start returning a different numeric scale after someone edits an unrelated branch. Explicit casts in the branches make the contract visible where it is written. ## Practical rules Write branches that already share a type wherever you can — mapping codes to labels, or values to ranks, keeps the whole CASE in one family. When branches genuinely differ, cast each of them, not just the odd one out, so a reader can see the intended result type without knowing the engine's precedence table. Prefer keeping typed data typed and moving presentation strings to the display layer; a column that is sometimes a date and sometimes the word `'pending'` is a formatting decision that has leaked into the data. And when a CASE feeds a table or a view definition, treat the cast as mandatory: that is the point where the derived type stops being a query detail and becomes schema.

  • What type does a bare NULL literal in a THEN branch take?
    It has no type of its own and takes the type derived from the other branches — usually exactly what you want, and it is also why the implicit `ELSE NULL` does not disturb the result type. If no branch supplies a type, or a NULL meets an untyped parameter marker, the engine may be unable to infer one; `CAST(NULL AS DATE)` resolves it explicitly.
  • When does the derived type become more than a query-local detail?
    Whenever it escapes into schema or a contract: a view definition, a `CREATE TABLE AS SELECT`, or result-set metadata a client driver reads to choose how to materialise the value. At those points an unpinned type can change silently when someone edits an unrelated branch, so cast the branches explicitly and treat the result type as part of the interface.
  • Do all engines reject a CASE mixing a text branch with a timestamp branch?
    Strictly typed engines do, at parse or plan time rather than per row. SQLite is the notable exception: its dynamic typing attaches affinity to columns rather than enforcing expression types, so the statement runs and different rows carry different storage classes. Portable code should not depend on that — cast the branches to one type explicitly.

saying these in an interview costs you the question

  • Saying the result type is whichever branch fired for that row
  • Thinking a CASE column can hold a different type per row
  • Assuming any two types silently coerce without error
  • Ignoring that a mixed numeric CASE changes the result scale
  • Letting a view or CTAS inherit an underived, unpinned CASE type

context