Learning Platform
Урок 05.03 · 15 мин
Начальный
ExpressionsCASECOALESCECASTConcatenationComputed columns

SELECT — это не только список колонок

В прошлом курсе по реляционной алгебре мы говорили, что SELECT a, b, c FROM t — это проекция π. Но SQL расширяет проекцию: после SELECT можно писать любое выражение — арифметику, функции, CASE-ветвление, конкатенацию строк, приведение типов. Результат становится новой колонкой в итоговой реляции.

Эти выражения называются вычисляемыми колонками (computed columns). Они не хранятся в базе, а считаются на лету для каждой строки. Это очень дёшево — линия CPU на строку — и почти всегда дешевле, чем тащить «лишние» данные на клиента и считать там.

Арифметика и операторы

Числовые типы поддерживают обычные +, -, *, /, % (остаток), ^ (степень).

Один подводный камень: целочисленное деление. 5 / 2 в PostgreSQL это 2, а не 2.5, потому что оба операнда — INTEGER. Чтобы получить дробное, нужно хоть одно из значений сделать дробным: 5.0 / 2 или 5 / 2.0 или явный каст.

Цены в нашей базе хранятся в копейках (int). Переведём в рубли с двумя знаками:

PostgreSQL

Заметь разницу: price_cents / 100 даёт целое число, а price_cents / 100.0numeric. Это важно для денег: никогда не делай целочисленное деление на копейках, иначе потеряешь рубли.

Конкатенация: || и CONCAT

Стандартный оператор склейки строк — ||. Он чувствителен к NULL: если хоть один операнд NULL, результат — NULL.

Альтернатива — функция CONCAT(), которая игнорирует NULL и трактует их как пустые строки.

Сравни || и CONCAT() на клиентах с NULL в birth_year:

PostgreSQL

У Ирины (id=10) birth_year IS NULL, и pipe_concat будет NULL — вся склейка превращается в NULL. А CONCAT спокойно вернёт 'Ирина Гасанова ()'. Выбор за тобой: иногда явный NULL лучше тихой пустоты.

CASE WHEN — условные выражения

CASE — это if/else if/else для SQL. Используется в SELECT, WHERE, ORDER BY, GROUP BY — везде, где допустимо выражение.

Два синтаксиса:

Searched CASE — каждая ветка со своим условием:

CASE
  WHEN condition1 THEN result1
  WHEN condition2 THEN result2
  ELSE default
END

Simple CASE — сравнение с одним значением:

CASE x
  WHEN 1 THEN 'one'
  WHEN 2 THEN 'two'
  ELSE 'other'
END
CASE WHEN — ветвление в SELECT

Каждая строка обрабатывается независимо. Ветки проверяются сверху вниз; первая истинная — выигрывает.

входной кортежprice_cents = 50000
CASE
WHEN < 10000'cheap'
WHEN < 100000'mid'Эта ветка сработает: 50000 < 100000
ELSE'premium'
результат'mid'

Классифицируем товары по ценовому диапазону:

PostgreSQL

Важно: ветки проверяются сверху вниз, первая истинная — победила. Поэтому интервалы не нужно явно прописывать (>= a AND < b) — достаточно последовательной верхней границы.

Если ни одна ветка не сработала и ELSE опущен, результат — NULL. Это нормально, но лучше всегда писать явный ELSE — снижает число неприятных сюрпризов.

COALESCE — выбрать первое не-NULL

COALESCE(a, b, c, ...) возвращает первый аргумент, который не NULL. Это сокращение для длинного CASE WHEN a IS NOT NULL THEN a WHEN b IS NOT NULL THEN b ....

COALESCE для подстановки значений по умолчанию:

PostgreSQL

Особенно полезно при конкатенации, чтобы избежать «заражения» NULL’ом:

full_name || ' (' || COALESCE(birth_year::text, '?') || ')'

CAST и :: — приведение типов

PostgreSQL — строго типизированный, и арифметика между несовместимыми типами требует явного приведения. Два синтаксиса:

  • Стандартный SQL: CAST(value AS TYPE)
  • Postgres-сокращение: value::TYPE

Приведение типов: разные способы получить timestamp из строки и обратно:

PostgreSQL

Что важно знать про CAST:

  • Приведение строки к числу падает с ошибкой, если строка некорректна ('foo'::int → error). Безопаснее — функция to_number или regexp_match.
  • Приведение numeric к int округляет (1.7::int2).
  • Приведение timestamp к date отбрасывает время.
  • text к boolean понимает 't', 'true', 'yes', '1' как TRUE, а 'f', 'false', 'no', '0' как FALSE.

Псевдонимы — AS

Любая вычисляемая колонка имеет неочевидное автоматическое имя (?column? или имя функции). Чтобы дать понятное имя — AS alias. Слово AS опционально, но настоятельно рекомендуется для читаемости.

SELECT price_cents / 100.0 AS rubles FROM products;

Псевдонимы видны в ORDER BY и GROUP BY, но не видны в WHERE — потому что WHERE обрабатывается раньше, чем SELECT. Это типичная ошибка новичков:

Псевдонимы в SELECT не доступны в WHERE — обрати внимание на ошибку:

PostgreSQL

Правильное решение — либо повторить выражение в WHERE, либо обернуть всё во вложенный запрос:

SELECT * FROM (
  SELECT name, price_cents / 100.0 AS rubles FROM products
) sub
WHERE rubles > 1000;

Углубление: чистые vs нечистые функции

Выражения в SELECT могут включать функции. Различают чистые (deterministic, immutable) — всегда дают одинаковый результат на одних входах — и нечистые (stable, volatile) — могут возвращать разное.

Примеры:

  • LENGTH(name), LOWER(name), price * 2 — чистые
  • NOW(), RANDOM(), CURRENT_USER — нечистые

Это важно для индексов: functional index можно построить только по immutable-выражению. CREATE INDEX ON t (NOW()) бесполезен.

Комплексное выражение: возраст клиента из birth_year, с обработкой NULL:

PostgreSQL

В этом запросе сразу всё: CASE с несколькими ветками, EXTRACT(), явный каст к int, и NULLS LAST в ORDER BY (об этом — в уроке 5).

Проверка знанийKnowledge check
Запрос SELECT name || ' (' || birth_year || ')' FROM customers вернёт NULL для строки с birth_year = NULL. Какие два способа починить это, чтобы строка всё равно отображалась?
ОтветAnswer
Первый способ — заменить || на CONCAT(): CONCAT(name, ' (', birth_year, ')') — функция игнорирует NULL и трактует его как пустую строку. Второй способ — обернуть birth_year в COALESCE: name || ' (' || COALESCE(birth_year::text, '?') || ')'. Выбор зависит от семантики: CONCAT молча подставит пустоту, COALESCE даёт явный плейсхолдер. Третий, более редкий способ — заменить весь NULL-конкат на CASE WHEN birth_year IS NULL THEN name ELSE name || ' (' || birth_year || ')' END.
if/for/set в Jinja dbt — CASE-подобная логика в шаблонах Комбинаторы агрегатных функций в ClickHouse

Чек-лист

  • В SELECT можно писать любое выражение, не только имена колонок.
  • Целочисленное деление: 5 / 2 = 2. Для дробного — 5 / 2.0 или явный каст.
  • || чувствителен к NULL (заражает результат). CONCAT() игнорирует.
  • CASE WHEN ... THEN ... ELSE ... END — выражение, не управляющая конструкция.
  • COALESCE(a, b, ...) — первый не-NULL аргумент.
  • Каст через ::TYPE или CAST(value AS TYPE). Падает на некорректных данных.
  • Псевдонимы AS name видны в ORDER BY, не видны в WHERE. Это особенность порядка выполнения.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чему равен результат запроса SELECT 5 / 2 в PostgreSQL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6