Другой диалект

SAFE-режим: NULL вместо падения

10 min
What you'll learn
  • превращать мусор в вместо падения: SAFE_CAST против CAST
  • делить без страха на ноль: SAFE_DIVIDE
  • писать короткие условия : IF и IFNULL вместо длинных CASE и COALESCE
  • замечать, где SAFE-семейство прячет баги, и останавливать запрос намеренно: ERROR()

Приборный отсек

Сегодня комендант открывает тебе новый раздел архива: приборные журналы колоний. Датчики жизнеобеспечения слали показания прямо в эфир, а приёмники складывали всё как есть — строками. Там, где связь рвалась, в строке остался мусор: «н/д», обрывки, пустота.

В Postgres ты вычистил бы такое заранее — ETL, регулярки, . Здесь чистить некому: архив мёртв, данные уже никто не перепишет. Запрос обязан выживать на грязных данных сам.

КВЕРИ: Я поискал в этом слово SAFE. Нашёл целое семейство. Кажется, архив знал, с какими данными ему предстоит жить.

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 падает целиком, SAFE_CAST — нет

Обычный CAST здесь тот же, что в Postgres, — и так же смертелен: одно неприводимое значение убивает весь запрос:

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

На таблице в миллиард строк это значит: запрос работал, жёг байты — и умер где-то на 998-м миллионе строк. Счётчик, кстати, списал всё.

Диалектная пара — SAFE_CAST: тот же синтаксис, но неприводимое значение становится NULL, а запрос доезжает до конца. Посмотри на четыре показания одного датчика:

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 превращает мусор в NULL. Заметь: «17.5» не влезло в INT64 — это тоже NULL, а не округление.

SAFE_DIVIDE: ноль в знаменателе

Вторая классика грязных данных — деление на ноль. Обычное / падает и здесь:

SELECT 1 / 0;  -- division by zero

SAFE_DIVIDE(a, b) возвращает NULL, когда b — ноль или . В Postgres ты писал a / NULLIF(b, 0) — идиома работает и тут, но даёт готовую функцию.

Посчитаем терминалов: сколько просмотров товара у жителя закончилось покупкой. У некоторых жителей просмотров нет вообще — вот и ноль в знаменателе:

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
У жителей 6 и 8 в знаменателе ноль — SAFE_DIVIDE вернул NULL, и запрос выжил.

IF и IFNULL: условные по-диалектному

CASE из Postgres работает без изменений, но для двух самых частых случаев держит короткие формы:

  • IF(cond, a, b) — вместо CASE WHEN cond THEN a ELSE b END;
  • IFNULL(x, default) — вместо COALESCE(x, default) на два аргумента.
-- Postgres:
CASE WHEN total >= 1000 THEN 'крупный' ELSE 'обычный' END
-- BigQuery:
IF(total >= 1000, 'крупный', 'обычный')

Разметим заказы станции снабжения:

Крупных заказов четыре. Заказу 106 на 980 кредитов не хватило до порога ровно двадцати.

IFNULL дружит с SAFE-семейством: цепочка «посчитай опасное → подставь запасное» — фирменный приём . И он встраивается прямо в звёздочку из первого урока:

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

— «все колонки, кроме мусорной, а дыры в revenue заткни нулём». Ровно этот приём ждёт тебя в задачах тренажёра ниже. Примерим цепочку на :

NULL от SAFE_DIVIDE стали нулями. Удобно для отчёта — и опасно: смотри следующий блок.

Когда SAFE прячет баг

Перечитай вывод: у жителя 6 0. А теперь его строка в предыдущей ячейке: 0 просмотров и 1 покупка. Это не «не конвертится» — это покупка мимо витрины: другой путь к покупке или дыра в логировании. SAFE_DIVIDE честно ответил («считать нечего»), а наш IFNULL(..., 0) уверенно соврал: «ноль».

С ещё коварнее: AVG игнорирует NULL, но учитывает 0. Средняя конверсия «по жителям с NULL» и «по жителям с нулями» — два разных числа, и оба выглядят правдоподобно.

Правило: SAFE_ — для мусора, который ты решил игнорировать осознанно.* Для невозможных состояний у есть противоположность — ERROR():

SELECT IF(views = 0 AND purchases > 0,
          ERROR('покупка без просмотра: проверь логирование'),
          SAFE_DIVIDE(purchases, views)) AS conversion
FROM per_user

ERROR() намеренно роняет запрос с твоим сообщением: лучше упасть громко, чем молча посчитать ерунду. Живой ячейки на ERROR() в главе нет намеренно: её успешный исход — красный отказ, а в тренажёре это неотличимо от сломанного примера. Синтаксис архив принимает: пока условие ложно, ветка с ERROR() просто не срабатывает — сработавшая роняет запрос целиком.

Префикс SAFE. для остальных функций

SAFE-семейство шире пары функций: почти любую скалярную функцию можно вызвать с префиксом SAFE. — и вместо ошибки она вернёт . Функции в примере взяты как первые попавшиеся: PARSE_DATE просто пытается прочитать дату вида ГГГГ-ММ-ДД, разбирать её маску сейчас не нужно. Без префикса обе строчки ниже роняют запрос:

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

Префикс работает только со скалярными функциями: SAFE.SUM(...) архив отвергает прямо при разборе (SAFE is not supported for function sum), а к операторам вроде / префикс не приставить синтаксически — поэтому SAFE_DIVIDE и SAFE_CAST и оформлены отдельными функциями.

Важное: SAFE. глушит ошибку ВЫЧИСЛЕНИЯ, а не ошибку типов. SAFE.SUBSTR(order_id, 2, 2) из прошлого урока всё равно упадёт с No matching signature: неверные типы аргументов ловятся до запуска, глушить там нечего. Сравни три вызова — два опасных под префиксом и один заведомо исправный:

Два NULL вместо двух падений — и разобранная дата там, где входные данные в порядке: префикс не мешает исправному вызову.
Check yourself
Датчик прислал строку 'abc'. Что вернут CAST('abc' AS INT64) и SAFE_CAST('abc' AS INT64)?
Check yourself
Пайплайн считает среднюю конверсию: AVG(IFNULL(SAFE_DIVIDE(purchases, views), 0)). У трёх жителей конверсия 1.0, ещё у двоих views = 0 (SAFE_DIVIDE дал NULL). Что выйдет — и что вышло бы без IFNULL?
Key takeaways
  • SAFE_CAST(x AS T) вместо падения; обычный CAST роняет весь запрос
  • SAFE_DIVIDE(a, b) — NULL при нуле в знаменателе; замена постгресовому a / NULLIF(b, 0)
  • IF(cond, a, b) и IFNULL(x, d) — короткие формы CASE и COALESCE
  • фирменная связка: SELECT * EXCEPT(...) REPLACE(IFNULL(col, 0) AS col)
  • SAFE — осознанное игнорирование мусора; для невозможных состояний — ERROR(); для прочих скалярных функций — префикс SAFE.

Документация: функции преобразования, условные выражения.

Ты научил запросы выживать. Но иногда архив всё же отвечает отказом — и его отказы надо уметь читать. Следующий урок — язык ошибок BigQuery.