В данной статье я хотел бы поговорить немного о неявном приведении типов в Postgres, широко известно, что Postgres не позволяет неявно приводить строковые типы к числовым (как это делает Oracle) и эта добавочная строгость скорее всего правильна (избавляет от многих плавающих ошибок времени исполнения), но кое-какое неявное приведение типов всё-таки остаётся - это приведение внутри числовых типов (int2, int4, int8, numeric) и вот здесь возможны некоторые сюрпризы.Давайте рассмотрим некий синтетический пример Читать далее
В данной статье я хотел бы поговорить о неявном приведении типов в Postgres. Широко известно, что Postgres не позволяет неявно приводить строковые типы к числовым, (как это делает Oracle) и эта добавочная строгость, скорее всего, правильна (избавляет от многих плавающих ошибок времени исполнения), но кое-какое неявное приведение типов всё-таки остаётся - это приведение внутричисловых типов (int2, int4, int8, numeric) и вот здесь возможны некоторые сюрпризы.
Давайте рассмотрим некий синтетический пример

Создаём 2 таблички одна основная doc которую будем "джойнить" с неким словарём
DROP TABLE IF EXISTS doc;
DROP TABLE IF EXISTS dict1;
CREATE TABLE doc (
id_doc BIGINT NOT NULL PRIMARY KEY,
id_2 BIGINT NULL,
id_dict1 NUMERIC, --- здесь отличается тип данных
state NUMERIC NULL,
date_update TIMESTAMP(6) NOT NULL DEFAULT statement_timestamp(),
descr VARCHAR(1024)
);
CREATE INDEX doc_ix1 ON doc (id_2);
CREATE INDEX doc_ix2 ON doc (date_update);
CREATE TABLE dict1 (
id BIGINT PRIMARY KEY, --- здесь отличается тип данных
name VARCHAR(255),
name2 VARCHAR(1024),
name3 VARCHAR(1024)
);
и заполняем таблички случайными данными
INSERT INTO doc
SELECT g AS id_doc,
FLOOR(RANDOM() * (10000 + 1)) AS id2,
FLOOR(RANDOM() * (100 + 1)) AS id_dict1,
MOD(g, 5) AS state,
CURRENT_DATE - (RANDOM() * 365)::INTEGER AS date_update,
REPEAT('a', 1000)
FROM pg_catalog.generate_series(1, 100000) g ;
INSERT INTO dict1
SELECT g AS id,
REPEAT('name' || g, 20),
REPEAT('a' || g, 100),
REPEAT('b' || g, 100)
FROM pg_catalog.generate_series(1, 10000) g order by random();
ANALYZE doc;
ANALYZE dict1;
посмотрим получившийся план выполнения для следующего запроса
EXPLAIN ANALYZE
SELECT *
FROM doc
INNER JOIN dict1 ON doc.id_dict1 = dict1.id
WHERE date_update >= current_date - '1 day'::INTERVAL;Hash Join (cost=1829.30..3922.79 rows=25500 width=2189) (actual time=24.084..25.061 rows=402 loops=1)
Hash Cond: (doc.id_dict1 = (dict1.id)::numeric)
-> Index Scan using doc_ix2 on doc (cost=0.30..562.52 rows=510 width=1036) (actual time=0.012..0.714 rows=407 loops=1)
Index Cond: (date_update >= (CURRENT_DATE - '1 day'::interval))
-> Hash (cost=1704.00..1704.00 rows=10000 width=1153) (actual time=24.021..24.023 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 11727kB
-> Seq Scan on dict1 (cost=0.00..1704.00 rows=10000 width=1153) (actual time=0.006..6.333 rows=10000 loops=1)
Planning Time: 0.325 ms
Execution Time: 25.201 msвидно что при соединении произошло неявное приведение типа для колонки "ведомой" таблицы dict1, см. "Hash Cond: (doc.id_dict1 = (dict1.id)::numeric) ",
что эффективно заблокировало использование индекса на табличке dict1
Однако если поля связывающие 2 таблицы будут определены "противоположным" образом
id_dict1 bigint и id numeric

CREATE TABLE doc (
id_doc bigint NOT NULL PRIMARY KEY,
id_2 bigint NULL,
id_dict1 bigint, -- поле связи
state NUMERIC NULL,
date_update TIMESTAMP(6) NOT NULL DEFAULT statement_timestamp(),
descr VARCHAR(1024)
);
CREATE INDEX doc_ix1 ON doc (id_2);
CREATE INDEX doc_ix2 ON doc (date_update);
CREATE TABLE dict1 (
id numeric PRIMARY KEY, -- поле связи
name VARCHAR(255),
name2 VARCHAR(1024),
name3 VARCHAR(1024)
);то план получится совсем другим, "более правильным"
Nested Loop (cost=0.58..791.07 rows=310 width=2190) (actual time=0.024..0.757 rows=136 loops=1)
-> Index Scan using doc_ix2 on doc (cost=0.30..344.52 rows=310 width=1040) (actual time=0.009..0.296 rows=139 loops=1)
Index Cond: (date_update >= (now() - '1 day'::interval))
-> Index Scan using dict1_pkey on dict1 (cost=0.29..1.44 rows=1 width=1150) (actual time=0.003..0.003 rows=1 loops=139)
Index Cond: (id = (doc.id_dict1)::numeric)
Planning Time: 0.400 ms
Execution Time: 0.796 msвидно, что произошло приведение типа с "другой стороны джойна" со стороны ведущей таблицы и теперь это не привело к подавлению использования индекса
Формально, всё здесь сделано правильно.
1. произошло приведение от более частного типа (int8) к более общему (numeric).
2. "На вход" к ведомой таблице прилетело значение типа неподдерживаемое индексом, и поэтому не использовался индексный доступ.
Просто, при переходе с других СУБД это логичное поведение является в некоторой степени неожиданным.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Диапазонный тип данных в PostgreSQL: ускоряем запросы | 5 | 7 | 26-06-2026 |
| 2 | Кто выгрузил платежи, или Пример расследования инцидента на аудите в Postgres Pro Enterprise | 0 | 7 | 10-07-2026 |
| 3 | [Перевод] Проектирование системы хранения POSTGRES | 0 | 5 | 15-07-2026 |
| 4 | Полиморфные ссылки в PostgreSQL: помогаем СУБД избежать провалов производительности | 0 | 7 | 30-06-2026 |
| 5 | [Перевод] Ограничения целостности с отложенной проверкой в PostgreSQL | 0 | 7 | 06-07-2026 |
| 6 | Книга «PostgreSQL 18 изнутри»: архитектура «слона» под новым капотом | 0 | 5 | 01-07-2026 |
| 7 | Как фильтр Блума ускоряет JOIN'ы в PostgreSQL | 5 | 7 | 17-07-2026 |
| 8 | Postgres community events. Не пора ли начать использовать возможности цифровизации? | 0 | 5 | 11-07-2026 |
| 9 | [Перевод] Проектирование POSTGRES: как задумывалась популярная СУБД | 0 | 5 | 07-07-2026 |
| 10 | In-memory база врёт: 5 расхождений с продовой БД | 0 | 7 | 07-07-2026 |