В данной статье я хотел бы поговорить о неявном приведении типов в 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. "На вход" к ведомой таблице прилетело значение типа неподдерживаемое индексом, и поэтому не использовался индексный доступ.
Просто, при переходе с других СУБД это логичное поведение является в некоторой степени неожиданным.
ya_ne_znau
Думаю, в статье не хватает объяснения, почему колонка "id_dict1", по смыслу намекающая на связь с колонкой "id" таблицы "dict1" и использующейся в таком виде, не является при этом ключом (foreign key, забыл слово) к соответствующей таблице.
При этом документация postgresql накладывает явное ограничение на соответствие типов этих колонок, что, собственно, убирает проблему хоть и не для всех случаев, но как минимум для рассмотренного.