A different dialect

SAFE mode: NULL instead of a crash

10 min
What you'll learn
  • to turn garbage into instead of a crash: SAFE_CAST versus CAST
  • to divide without fearing a zero: SAFE_DIVIDE
  • to write the 's short conditionals: IF and IFNULL instead of the long CASE and COALESCE
  • to spot where the SAFE family hides bugs, and to stop a query on purpose: ERROR()

The instrument bay

Today the commandant opens a new section of the archive for you: the colonies' instrument logs. The life-support sensors broadcast their readings straight into the ether, and the receivers stored everything exactly as it came — as strings. Wherever the link broke, garbage was left in the string: an "n/a" marker, fragments, emptiness.

In Postgres you would have cleaned that up in advance — ETL, regular expressions, a . Here there is nobody left to clean: the archive is dead, and nobody will rewrite the data now. The query has to survive dirty data on its own.

QUERY: I searched this for the word SAFE. I found a whole family. It seems the archive knew what kind of data it would have to live with.

An instrument bay: sensor capsules ride a conveyor, some cracked and full of dark static. The gate is split in two — on the left a cracked capsule detonates and the whole line goes dark, on the right it passes through a soft shield, comes out empty, and the line keeps glowing.
A plain conversion kills the entire query on the first garbage value; the SAFE variant yields NULL and the work goes on.

CAST takes everything down, SAFE_CAST does not

The ordinary CAST is the same here as in Postgres — and just as lethal: one uncastable value kills the entire query:

SELECT CAST('н/д' AS INT64) AS reading;
-- Could not cast literal "н/д" to type INT64

On a table of a billion rows that means: the query ran, burned bytes — and died somewhere around the 998 millionth row. The meter, by the way, charged for all of it.

The 's counterpart is SAFE_CAST: the same syntax, but an uncastable value becomes NULL and the query makes it to the end. Look at four readings from a single sensor:

SAFE mode: one dirty row will not kill the reportCAST(x AS INT64)7712"n/a"43the whole query failsSAFE_CAST(x AS INT64)7712NULL43the row survives, value is NULL
One unconvertible value: CAST kills the whole query, SAFE_CAST leaves a NULL in one row
SAFE_CAST turns garbage into NULL. Note: "17.5" did not fit into INT64 either — that is a NULL too, not a rounding.

SAFE_DIVIDE: a zero in the denominator

The second classic of dirty data is division by zero. The ordinary / crashes here too:

SELECT 1 / 0;  -- division by zero

SAFE_DIVIDE(a, b) returns NULL when b is zero or . In Postgres you wrote a / NULLIF(b, 0) — the idiom works here as well, but the gives you a ready-made function.

Let us compute the terminals' conversion: how many of a colonist's product views ended in a purchase. Some colonists have no views at all — and there is your zero in the denominator:

SAFE_DIVIDE: a zero denominator is not an error120 / 403375 / 0division by zeroNULL90 / 3033plain divisionSAFE_DIVIDEa conversion on an empty day computes instead of failing
An empty day in the denominator gives NULL, not a failed report
Colonists 6 and 8 have a zero in the denominator — SAFE_DIVIDE returned NULL, and the query survived.

IF and IFNULL: conditionals the dialect's way

The CASE you know from Postgres works unchanged, but for the two most frequent cases the keeps short forms:

  • IF(cond, a, b) — instead of CASE WHEN cond THEN a ELSE b END;
  • IFNULL(x, default) — instead of a two-argument COALESCE(x, default).
-- Postgres:
CASE WHEN total >= 1000 THEN 'крупный' ELSE 'обычный' END
-- BigQuery:
IF(total >= 1000, 'крупный', 'обычный')

Let us label the supply station's orders:

There are four large orders. Order 106, at 980 credits, fell exactly twenty short of the threshold.

IFNULL gets along with the SAFE family: the chain "compute the dangerous thing → substitute a fallback" is the 's signature move. And it plugs straight into the star from the first lesson:

SELECT * EXCEPT(raw_payload) REPLACE(IFNULL(revenue, 0) AS revenue)
FROM report

— "every column but the junk one, and plug the holes in revenue with a zero". Exactly this move is waiting for you in the practice tasks below. Let us try the chain on conversion:

The NULLs from SAFE_DIVIDE became zeros. Convenient for a report — and dangerous: see the next block.

When SAFE hides a bug

Re-read the output: colonist 6 has a conversion of 0. Now look at their row in the previous cell: 0 views and 1 purchase. That is not "does not convert" — that is a purchase past the storefront: another route to the purchase, or a hole in the logging. SAFE_DIVIDE answered honestly with ("there is nothing to count"), while our IFNULL(..., 0) lied with confidence: "zero".

With it is trickier still: AVG ignores NULL but counts 0. The average conversion "over colonists with NULL" and "over colonists with zeros" are two different numbers, and both look plausible.

The rule: SAFE_ is for garbage you have decided to ignore deliberately.* For impossible states the has the opposite — ERROR():

SELECT IF(views = 0 AND purchases > 0,
          ERROR('purchase without a view: check the logging'),
          SAFE_DIVIDE(purchases, views)) AS conversion
FROM per_user

ERROR() deliberately crashes the query with your message: better to fall loudly than to count nonsense in silence. There is no live cell for ERROR() in this chapter, and that is on purpose: its successful outcome is a red refusal, and in the trainer that is indistinguishable from a broken example. The archive accepts the syntax: while the condition is false, the branch with ERROR() simply never fires — and the one that fires takes the whole query down.

The SAFE. prefix for every other function

The SAFE family is wider than a couple of functions: almost any scalar function can be called with the SAFE. prefix — and instead of an error it returns . The functions in the example are simply the first ones to hand: PARSE_DATE merely tries to read a date of the form YYYY-MM-DD, and there is no need to unpick its mask right now. Without the prefix both lines below crash the query:

SELECT SUBSTR('архив', 0, -2);       -- SUBSTR: length must be positive number
SELECT PARSE_DATE('%F', 'не дата');  -- error parsing [не дата] with format [%F]

The prefix works only with scalar functions: SAFE.SUM(...) is rejected by the archive right at parse time (SAFE is not supported for function sum), and syntactically the prefix cannot be attached to operators such as / — which is why SAFE_DIVIDE and SAFE_CAST are shipped as separate functions.

Important: SAFE. mutes a COMPUTATION error, not a type error. SAFE.SUBSTR(order_id, 2, 2) from the previous lesson will still fall over with No matching signature: wrong argument types are caught before the run, and there is nothing to mute there. Compare three calls — two dangerous ones under the prefix and one that is bound to work:

Two NULLs instead of two crashes — and a parsed date where the input is in order: the prefix does not get in the way of a valid call.
Check yourself
A sensor sent the string 'abc'. What will CAST('abc' AS INT64) and SAFE_CAST('abc' AS INT64) return?
Check yourself
A pipeline computes the average conversion: AVG(IFNULL(SAFE_DIVIDE(purchases, views), 0)). Three colonists have a conversion of 1.0, two more have views = 0 (SAFE_DIVIDE gave NULL). What comes out — and what would have come out without IFNULL?
Key takeaways
  • SAFE_CAST(x AS T) — instead of a crash; the ordinary CAST takes the whole query down
  • SAFE_DIVIDE(a, b) — NULL when the denominator is zero; the replacement for the Postgres a / NULLIF(b, 0)
  • IF(cond, a, b) and IFNULL(x, d) — the short forms of CASE and COALESCE
  • the signature combination: SELECT * EXCEPT(...) REPLACE(IFNULL(col, 0) AS col)
  • SAFE is deliberate ignoring of garbage; for impossible states there is ERROR(); for the other scalar functions there is the SAFE. prefix

Documentation: conversion functions, conditional expressions.

You have taught your queries to survive. But sometimes the archive still answers with a refusal — and its refusals have to be read. The next lesson is the language of BigQuery errors.