В данной статье я хотел бы поговорить о неявном приведении типов в 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. "На вход" к ведомой таблице прилетело значение типа неподдерживаемое индексом, и поэтому не использовался индексный доступ.
Просто, при переходе с других СУБД это логичное поведение является в некоторой степени неожиданным.

Комментарии (2)


  1. ya_ne_znau
    30.06.2026 23:06

    Думаю, в статье не хватает объяснения, почему колонка "id_dict1", по смыслу намекающая на связь с колонкой "id" таблицы "dict1" и использующейся в таком виде, не является при этом ключом (foreign key, забыл слово) к соответствующей таблице.

    При этом документация postgresql накладывает явное ограничение на соответствие типов этих колонок, что, собственно, убирает проблему хоть и не для всех случаев, но как минимум для рассмотренного.