SQLCASTтипы данных

CAST в SQL: преобразование типов данных без сюрпризов

2026-07-12 9 мин

Если сумма в отчёте вдруг обрезалась до целого, а деление выдало 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) понимают все. Дальше начинаются диалекты.

-- MySQL: привести строку к целому — через SIGNED
SELECT CAST('42' AS SIGNED);

Мой совет — держать в голове 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.

Отработай SQL на практике
545 SQL-задач с автопроверкой — первые открыты без регистрации.
SQL-тренажёр →