Если сумма в отчёте вдруг обрезалась до целого, а деление выдало 2 вместо 2.5 — почти всегда дело в типе данных, а не в кривом запросе. CAST(x AS type) (и короткая форма x::type в PostgreSQL) берёт значение одного типа и превращает в другой: строку в число, число в дату, целое в дробное. Дальше — как делать это осознанно и где чаще всего спотыкаются даже те, кто пишет SQL каждый день.
Как работает CAST(x AS type) и оператор ::type?
CAST(значение AS тип) — это стандарт ANSI SQL, он работает в любой СУБД. PostgreSQL к нему добавляет короткий оператор ::, который делает ровно то же самое.
-- Стандартный синтаксис — работает везде
SELECT CAST('42' AS integer) AS num;
-- Короткая запись PostgreSQL — тот же результат
SELECT '42'::integer AS num;
Приводить можно литералы, столбцы и целые выражения. Единственная ловушка :: — приоритет: оператор «прилипает» к ближайшему значению. Поэтому сложное выражение оборачивают в скобки.
-- Каст применится только к count(*), а не ко всему выражению
SELECT sum(amount) / count(*)::numeric FROM payments; -- скорее всего не то, что вы хотели
-- Явные скобки убирают двусмысленность
SELECT (sum(amount) / count(*))::numeric FROM payments;
Если сомневаетесь — ставьте скобки, это дешевле, чем потом искать, почему цифра «поехала».
Почему 5/2 равно 2, а не 2.5?
Потому что оба операнда — целые числа, а деление целого на целое даёт целое: остаток просто отбрасывается. Чтобы получить дробь, достаточно привести хотя бы один операнд к numeric или float.
SELECT 5 / 2; -- 2
SELECT 5::numeric / 2; -- 2.5
SELECT CAST(5 AS numeric) / 2; -- то же самое
На практике это ломает подсчёт долей и конверсий. Классика — считаем долю оплативших, а получаем ноль, потому что оплативших меньше, чем всего, и целочисленное деление округляет вниз.
-- Без CAST conversion почти всегда 0
SELECT
COUNT(*) FILTER (WHERE is_paid) AS paid,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE is_paid)::numeric
/ COUNT(*) AS conversion
FROM users;
Те же грабли ждут в продуктовых метриках — от DAU до retention: почти всё это доли и агрегаты, где один забытый ::numeric превращает 0.37 в 0. Привыкайте кастовать числитель заранее, а не искать потом, куда делись проценты.
Как превратить строку в число и число в дату?
Строку в число приводят напрямую, если внутри чистая цифра:
SELECT '1990'::int; -- 1990
SELECT '12.50'::numeric; -- 12.50
Если в данных десятичная запятая вместо точки (частый гость в выгрузках из 1С и Excel), сначала меняем символ:
SELECT REPLACE('12,50', ',', '.')::numeric; -- 12.50
Со строкой-датой два пути. ISO-формат YYYY-MM-DD приводится кастом сразу, всё остальное — через to_date с явной маской:
SELECT '2026-01-15'::date; -- ок
SELECT to_date('15.01.2026', 'DD.MM.YYYY'); -- ок
Число в дату напрямую не кастуется, но это нужно постоянно: время событий часто хранят как Unix-таймстамп. Здесь работает to_timestamp:
-- Unix-время (число) → метка времени → дата
SELECT to_timestamp(1768521600); -- timestamptz
SELECT to_timestamp(1768521600)::date; -- только дата
А если дата лежит числом вида 20260115, гоняем её через текст:
SELECT to_date(20260115::text, 'YYYYMMDD'); -- 2026-01-15
И самый частый каст в жизни аналитика — отрезать время у таймстампа, чтобы сгруппировать события по дням:
SELECT created_at::date AS day, COUNT(*)
FROM events
GROUP BY created_at::date
ORDER BY day;
Как правильно округлять с ROUND над numeric?
Короткий ответ: перед ROUND(x, 2) приводите значение к numeric. В PostgreSQL двухаргументный ROUND (значение и число знаков) определён только для numeric, но не для double precision. Поэтому невинный запрос падает.
-- Ошибка: function round(double precision, integer) does not exist
SELECT ROUND(AVG(amount), 2) FROM payments; -- если amount хранится как float
-- Правильно
SELECT ROUND(AVG(amount)::numeric, 2) FROM payments;
Причина в том, что AVG над double precision возвращает double precision, а такого ROUND в PostgreSQL просто нет. Над integer или numeric тот же AVG вернёт numeric, и всё сработает без каста — но полагаться на тип столбца рискованно. Я привык всегда писать ::numeric перед округлением до знаков: одна лишняя строка экономит полчаса на «почему-то не работает».
Ещё нюанс: ROUND в PostgreSQL округляет половину вверх (0.5 → 1), а не по «банковскому» правилу. Для денег это обычно то, что нужно. Но если сверяете цифры с отчётом из другого инструмента, где округление банковское, расхождение в копейку — это как раз оно, а не ошибка в запросе.
Что делать с ошибками приведения, если в данных мусор?
'100 руб'::int не превратится в 100 — запрос упадёт с invalid input syntax for type integer, и вместе с ним умрёт вся выборка. В PostgreSQL нет встроенного «безопасного» каста, поэтому мусор чистят заранее.
Пустая строка — тоже мусор, её ловят через NULLIF:
SELECT ''::int; -- ошибка
SELECT NULLIF('', '')::int; -- NULL, без ошибки
Если в колонке смесь чисел и текста, фильтруем регуляркой до каста либо вырезаем всё лишнее:
-- Берём только строки, похожие на число
SELECT raw_amount::numeric
FROM staging_payments
WHERE raw_amount ~ '^\d+(\.\d+)?$';
-- Или вычищаем всё, кроме цифр и точки
SELECT NULLIF(regexp_replace(raw_amount, '[^0-9.]', '', 'g'), '')::numeric
FROM staging_payments;
В других диалектах для этого есть готовое: TRY_CAST в SQL Server, SAFE_CAST в BigQuery, toFloat64OrNull в ClickHouse — все они возвращают NULL вместо ошибки. Если готовите данные в Python, тот же смысл у pd.to_numeric(df['amount'], errors='coerce'): аргумент coerce и есть безопасный аналог TRY_CAST. Потренироваться на «грязных» выгрузках удобно в Python-тренажёре.
Неявное приведение и его сюрпризы
СУБД нередко кастует за вас молча, и именно в этом тихом касте живут самые злые баги. Первый — сравнение чисел, которые лежат в текстовой колонке. Строки сравниваются посимвольно, а не по величине.
SELECT '2' > '10'; -- true! посимвольно '2' больше '1'
SELECT 2 > 10; -- false
Поэтому order_no или user_id, сохранённые как текст, в ORDER BY выстроятся как 1, 10, 11, 2, 20. Лечится приведением: ORDER BY order_no::int.
Второй сюрприз — даты-строки. Условие WHERE created_at > '2026-01-01' работает корректно, только если дата в тексте лежит в ISO-формате. В формате DD.MM.YYYY строки '15.01.2026' и '01.02.2026' сравнятся по первому символу — '1' > '0' — и январь окажется «больше» февраля.
Ещё стоит помнить, что PostgreSQL строже MySQL. MySQL охотно проглотит '123abc' и молча превратит в 123, спрятав ошибку данных; PostgreSQL в такой ситуации честно ругнётся. Ни то, ни другое поведение не «безопасно» — безопасен только явный CAST, где вы сами решаете, что делать с мусором.
Почему CAST в WHERE может замедлить запрос?
Потому что приведение самого столбца в условии мешает базе пользоваться индексом. Если обернуть индексированную колонку в CAST, планировщик уже не может сопоставить её с индексом и уходит в полный перебор таблицы (seq scan).
-- Приведение столбца ломает индекс — seq scan по всей таблице
SELECT * FROM events
WHERE created_at::date = '2026-01-15';
-- Диапазон по «сырому» столбцу — индекс работает
SELECT * FROM events
WHERE created_at >= '2026-01-15'
AND created_at < '2026-01-16';
Правило простое: кастуйте константу или литерал, а индексированный столбец оставляйте нетронутым. На маленькой таблице разницы не заметите, а на миллионах строк первый вариант превращает миллисекунды в секунды.
Чем отличается CAST в PostgreSQL, MySQL и ClickHouse?
База одна — CAST(x AS type) понимают все. Дальше начинаются диалекты.
- PostgreSQL: плюс короткий
::type, строгая типизация, имена типовinteger,numeric,text,date,timestamp,boolean. - MySQL и MariaDB: оператора
::нет, толькоCAST(...)илиCONVERT(x, type). Важная деталь: к целому приводят какSIGNED, а неINT.
-- MySQL: привести строку к целому — через SIGNED
SELECT CAST('42' AS SIGNED);
- ClickHouse:
CAST(x, 'Int32')плюс семейство функцийtoInt32,toDateи безопасные варианты вродеtoInt32OrNull. - SQL Server:
CAST,CONVERT(type, x, style)и отдельноTRY_CAST.
Мой совет — держать в голове ANSI-форму CAST(x AS type) как базу, которая никогда не подведёт, а :: использовать в PostgreSQL для краткости. Разбор синтаксиса по каждому оператору есть в справочнике по SQL.
Как CAST выручает на собеседовании и в работе?
Вопросы про типы обожают на собеседованиях: «почему 5/2 равно 2», «как округлить среднее до копеек», «как из Unix-времени получить дату». Отвечать на них лучше не по памяти, а руками — тогда ответ звучит уверенно. Собери базовую практику в SQL-тренажёре, прогони тестовые задания с реальными выгрузками и посмотри, как эти вопросы задают вживую. Если готовишься под конкретный дедлайн, помогает план подготовки к собеседованию — там разложено, что учить в первую очередь. А системно, с нуля, тему типов и приведения проще всего закрыть в курсе по SQL.
Если захочется решать без лимитов — открыть все 425 SQL-задач, настоящий PostgreSQL прямо в браузере и разборы к каждому решению — это в Pro. Но и на бесплатном тарифе хватит, чтобы перестать бояться CAST и больше никогда не удивляться, почему деление выдало не то.
Связанная тема — SQL с нуля. Часть 7: NULL, COALESCE, CAST, CASE.