Что такое implicit cast
В предыдущих уроках мы видели explicit cast — CAST(x AS T) и x::T. Это явное приведение типа, которое программист пишет руками.
Implicit cast — это то же приведение, но добавленное СУБД автоматически. Когда мы пишем WHERE price = 100, операторы = ждут оба аргумента одного типа, а price — INTEGER, 100 — тоже целочисленный литерал, всё хорошо. А если price — NUMERIC, а 100 — INT? PostgreSQL добавит неявное приведение INT → NUMERIC, чтобы операция стала корректной.
В одних случаях implicit cast — благо: код читаемее, литералы не нужно везде типизировать. В других — это источник тонких багов: молча отключённые индексы, странная семантика сравнения, проблемы переноса кода между СУБД.
PostgreSQL — строго типизирован
В отличие от MySQL и SQLite, PostgreSQL очень строго относится к типам. Многие сравнения, которые в MySQL «просто работают», в PostgreSQL дают ошибку.
MySQL приводит всё ко всему, лишь бы запрос выполнился. PostgreSQL отказывается там, где нет очевидного безопасного пути.
Попробуй сравнить TEXT с INT напрямую — получишь ошибку:
Сообщение: operator does not exist: unknown = integer или text = integer. PostgreSQL не делает «угадай тип».
Но если мы укажем литерал по-человечески:
Цифровой литерал и приведение через ::int:
Работает. Разница в том, что мы явно говорим, как трактовать строку.
Когда implicit cast всё-таки происходит
Есть несколько ситуаций, где PostgreSQL молча приводит типы:
-
Литералы «неизвестного» типа. Когда ты пишешь
'1'без типизации, PostgreSQL присваивает ему специальный типunknownи сам решает, во что его привести по контексту. Например,WHERE x = '1'— PostgreSQL увидит, чтоxэто INTEGER, и приведёт'1'к INTEGER. Это типичный сценарий и обычно работает корректно. -
Числовые типы между собой. INT → BIGINT, INT → NUMERIC, NUMERIC → DOUBLE PRECISION, и подобные расширения идут автоматически. От «уже` типа к «вмещающему».
-
Внутри функций. Многие встроенные функции имеют перегрузки и сами приводят типы.
Implicit cast числовых типов:
pg_typeof — удобная функция для отладки: она показывает, к какому типу PostgreSQL свёл результат выражения. Полезно, когда планировщик ведёт себя не так, как ожидал.
Ловушка: implicit cast ломает индекс
Это самый коварный сценарий. Допустим, у тебя колонка phone TEXT с индексом, и ты пишешь:
SELECT * FROM users WHERE phone = 79991234567;
PostgreSQL увидит несовместимость TEXT vs INT и попробует привести левую часть, чтобы они совпали. То есть фактически он выполнит:
SELECT * FROM users WHERE phone::bigint = 79991234567;
Это
phone::text, а сравнение идёт по phone::bigint. Планировщик вынужден делать full scan.
В нашей схеме индексов с такой проблемой нет, но симулировать поведение легко:
Сравни два запроса — один использует «нативный» TEXT, второй приводит:
В первом запросе у нас уже email это TEXT, литерал '[email protected]' приведётся к TEXT, и индекс по email сработает. Во втором мы явно вызываем email::text — для PostgreSQL это становится выражением, и обычный индекс по email уже не подойдёт (нужен был бы функциональный индекс по (email::text), что бессмысленно — но иллюстрирует точку).
Правило: всегда сравнивай колонку с литералом совместимого типа, а не приводи колонку. Если уж нужно приведение — приводи литерал.
Где implicit cast спасает: типизированные литералы
Самая частая «магия» — литералы в WHERE. Они работают, потому что PostgreSQL умеет «вывести» тип из контекста:
WHERE birth_year = 1998 -- 1998 — INT, birth_year — INT, всё совпадает
WHERE signup_date = '2024-01-01' -- '2024-01-01' приводится к DATE, потому что signup_date — DATE
WHERE country IN ('RU', 'DE') -- литералы → TEXT
WHERE price_cents > 100000 -- 100000 → INT
Здесь implicit cast работает в нашу пользу: мы не пишем '2024-01-01'::date каждый раз. Но если контекст неоднозначен (например, в SELECT без WHERE), pg_typeof может удивить:
Литерал без контекста — тип unknown:
without_context будет text, потому что PostgreSQL не нашёл, в какой контекст этот литерал воткнуть. В WHERE date_col = '2024-01-01' он бы стал DATE.
Boolean — особый случай
Булев тип не приводится к числовому неявно. WHERE active = 1 — ошибка. Нужно WHERE active = TRUE или просто WHERE active.
Булевы значения:
Заметь:
'yes'/'no', 'true'/'false', 't'/'f', 'on'/'off', '1'/'0'. Это удобно, но только при явном ::boolean.
TEXT и числовые сравнения
Чаще всего ошибка возникает, когда числовая колонка в исходной системе хранилась как строка (например, импорт из CSV без проверки типов).
Сравнение TEXT-чисел даёт лексикографический порядок:
Первый запрос вернёт '1', '10', '100', '2', '20' — лексикографический (строковый) порядок. Второй — 1, 2, 10, 20, 100 — нормальный числовой. Если ты импортируешь данные и сортируешь без приведения — получишь «1, 10, 100, 2» и часами будешь искать баг.
Хорошие практики
- Колонки делай правильного типа сразу.
phone TEXT, неphone BIGINT. Год —INT, неTEXT. Деньги —BIGINTв центах. - Приводи литералы, не колонки.
WHERE id = '5'::int, неWHERE id::text = '5'. Первое использует индекс, второе — нет. - При сомнении —
pg_typeof. Он покажет, к какому типу пришло выражение. EXPLAIN— твой друг. Если индекс не используется, посмотри: возможно, вWHEREпрячется неявный cast колонки.CASTлучше::в публичных контрактах — он стандартный SQL, легче переносить код.
Чек-лист
- PostgreSQL строго типизирован —
'1' = 1это ошибка, не TRUE. - Implicit cast работает для литералов «неизвестного» типа: PostgreSQL выводит тип из контекста колонки.
- Числовые типы между собой расширяются автоматически (INT → BIGINT → NUMERIC → DOUBLE PRECISION).
- Cast колонки ломает индекс. Cast литерала — нет.
pg_typeof(...)— отладочная функция, показывает реальный тип выражения.'1', '10', '2'сортируются лексикографически. Если ждёшь числовой порядок — приводи к INT.- Boolean — особый случай: явно к числу не приводится, в отличие от MySQL.