Learning Platform
Урок 04.05 · 16 мин
Начальный
Implicit castsType coercionType systemIndex usagePostgreSQL strict typing

Что такое implicit cast

В предыдущих уроках мы видели explicit cast — CAST(x AS T) и x::T. Это явное приведение типа, которое программист пишет руками.

Implicit cast — это то же приведение, но добавленное СУБД автоматически. Когда мы пишем WHERE price = 100, операторы = ждут оба аргумента одного типа, а priceINTEGER, 100 — тоже целочисленный литерал, всё хорошо. А если priceNUMERIC, а 100 — INT? PostgreSQL добавит неявное приведение INT → NUMERIC, чтобы операция стала корректной.

В одних случаях implicit cast — благо: код читаемее, литералы не нужно везде типизировать. В других — это источник тонких багов: молча отключённые индексы, странная семантика сравнения, проблемы переноса кода между СУБД.

PostgreSQL — строго типизирован

В отличие от MySQL и SQLite, PostgreSQL очень строго относится к типам. Многие сравнения, которые в MySQL «просто работают», в PostgreSQL дают ошибку.

Сравнение строгости

MySQL приводит всё ко всему, лишь бы запрос выполнился. PostgreSQL отказывается там, где нет очевидного безопасного пути.

MySQLpermissive
'1' = 1TRUE
'abc' = 0TRUE (!)Преобразует строку в число, ведущие нечисла → 0. Источник кошмарных багов.
'1.5' + 'foo'1.5
PostgreSQLstrict
'1' = 1ошибкаoperator does not exist: text = integer
'1'::int = 1TRUE
'1.5' + 'foo'ошибка типов

Попробуй сравнить TEXT с INT напрямую — получишь ошибку:

PostgreSQL

Сообщение: operator does not exist: unknown = integer или text = integer. PostgreSQL не делает «угадай тип».

Но если мы укажем литерал по-человечески:

Цифровой литерал и приведение через ::int:

PostgreSQL

Работает. Разница в том, что мы явно говорим, как трактовать строку.

Когда implicit cast всё-таки происходит

Есть несколько ситуаций, где PostgreSQL молча приводит типы:

  1. Литералы «неизвестного» типа. Когда ты пишешь '1' без типизации, PostgreSQL присваивает ему специальный тип unknown и сам решает, во что его привести по контексту. Например, WHERE x = '1' — PostgreSQL увидит, что x это INTEGER, и приведёт '1' к INTEGER. Это типичный сценарий и обычно работает корректно.

  2. Числовые типы между собой. INT → BIGINT, INT → NUMERIC, NUMERIC → DOUBLE PRECISION, и подобные расширения идут автоматически. От «уже` типа к «вмещающему».

  3. Внутри функций. Многие встроенные функции имеют перегрузки и сами приводят типы.

Implicit cast числовых типов:

PostgreSQL

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, второй приводит:

PostgreSQL

В первом запросе у нас уже 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:

PostgreSQL

without_context будет text, потому что PostgreSQL не нашёл, в какой контекст этот литерал воткнуть. В WHERE date_col = '2024-01-01' он бы стал DATE.

Boolean — особый случай

Булев тип не приводится к числовому неявно. WHERE active = 1 — ошибка. Нужно WHERE active = TRUE или просто WHERE active.

Булевы значения:

PostgreSQL

Заметь:

PostgreSQL соглашается на множество строковых форм
для приведения к boolean: 'yes'/'no', 'true'/'false', 't'/'f', 'on'/'off', '1'/'0'. Это удобно, но только при явном ::boolean.

TEXT и числовые сравнения

Чаще всего ошибка возникает, когда числовая колонка в исходной системе хранилась как строка (например, импорт из CSV без проверки типов).

Сравнение TEXT-чисел даёт лексикографический порядок:

PostgreSQL

Первый запрос вернёт '1', '10', '100', '2', '20' — лексикографический (строковый) порядок. Второй — 1, 2, 10, 20, 100 — нормальный числовой. Если ты импортируешь данные и сортируешь без приведения — получишь «1, 10, 100, 2» и часами будешь искать баг.

Хорошие практики

  1. Колонки делай правильного типа сразу. phone TEXT, не phone BIGINT. Год — INT, не TEXT. Деньги — BIGINT в центах.
  2. Приводи литералы, не колонки. WHERE id = '5'::int, не WHERE id::text = '5'. Первое использует индекс, второе — нет.
  3. При сомнении — pg_typeof. Он покажет, к какому типу пришло выражение.
  4. EXPLAIN — твой друг. Если индекс не используется, посмотри: возможно, в WHERE прячется неявный cast колонки.
  5. CAST лучше :: в публичных контрактах — он стандартный SQL, легче переносить код.
Проверка знанийKnowledge check
Колонка user_id BIGINT с индексом. Разработчик пишет WHERE user_id = '42'. Будет ли использован индекс?
ОтветAnswer
Да, индекс будет использован. PostgreSQL увидит, что user_id — BIGINT, а '42' — это литерал «неизвестного» типа (unknown), и приведёт его к BIGINT по контексту. Сравнение идёт «колонка BIGINT с константой BIGINT» — индекс работает. Это безопасный случай implicit cast: приведение идёт ОТ литерала К типу колонки. Опасный случай — наоборот: WHERE user_id::text = '42'. Тут уже над колонкой выполняется функция, и обычный индекс не помогает (понадобится функциональный индекс на (user_id::text), но это бессмысленно). Правило: литералы приводим, колонки трогать не надо.
Selectivity estimation: как неправильный тип убивает оценки Singular tests в dbt: проверяем типы и кастование

Чек-лист

  • PostgreSQL строго типизирован — '1' = 1 это ошибка, не TRUE.
  • Implicit cast работает для литералов «неизвестного» типа: PostgreSQL выводит тип из контекста колонки.
  • Числовые типы между собой расширяются автоматически (INT → BIGINT → NUMERIC → DOUBLE PRECISION).
  • Cast колонки ломает индекс. Cast литерала — нет.
  • pg_typeof(...) — отладочная функция, показывает реальный тип выражения.
  • '1', '10', '2' сортируются лексикографически. Если ждёшь числовой порядок — приводи к INT.
  • Boolean — особый случай: явно к числу не приводится, в отличие от MySQL.

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

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Что произойдёт в PostgreSQL при выполнении `SELECT '1' = 1`?

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

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

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

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