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). Переведём в рубли с двумя знаками:
Заметь разницу: price_cents / 100 даёт целое число, а price_cents / 100.0 — numeric. Это важно для денег: никогда не делай целочисленное деление на копейках, иначе потеряешь рубли.
Конкатенация: || и CONCAT
Стандартный оператор склейки строк — ||. Он чувствителен к NULL: если хоть один операнд NULL, результат — NULL.
Альтернатива — функция CONCAT(), которая игнорирует NULL и трактует их как пустые строки.
Сравни || и CONCAT() на клиентах с NULL в birth_year:
У Ирины (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
Каждая строка обрабатывается независимо. Ветки проверяются сверху вниз; первая истинная — выигрывает.
Классифицируем товары по ценовому диапазону:
Важно: ветки проверяются сверху вниз, первая истинная — победила. Поэтому интервалы не нужно явно прописывать (>= 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 для подстановки значений по умолчанию:
Особенно полезно при конкатенации, чтобы избежать «заражения» NULL’ом:
full_name || ' (' || COALESCE(birth_year::text, '?') || ')'
CAST и :: — приведение типов
PostgreSQL — строго типизированный, и арифметика между несовместимыми типами требует явного приведения. Два синтаксиса:
- Стандартный SQL:
CAST(value AS TYPE) - Postgres-сокращение:
value::TYPE
Приведение типов: разные способы получить timestamp из строки и обратно:
Что важно знать про CAST:
- Приведение строки к числу падает с ошибкой, если строка некорректна (
'foo'::int→ error). Безопаснее — функцияto_numberилиregexp_match. - Приведение
numericкintокругляет (1.7::int→2). - Приведение
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 — обрати внимание на ошибку:
Правильное решение — либо повторить выражение в 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:
В этом запросе сразу всё: CASE с несколькими ветками, EXTRACT(), явный каст к int, и NULLS LAST в ORDER BY (об этом — в уроке 5).
Чек-лист
- В
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. Это особенность порядка выполнения.