Postgres 9.6 индекс только сканирование по функциональному индексу логически возможно, но не выполнено

Я прочитал о функциональных индексах и индексах только в документах/вики, опубликованных Postgres.

Теперь у меня есть запрос вроде:

SELECT(xpath('/document/uuid/text()', xmldata))[1]::text,  
      (xpath('/document/title/text()', xmldata))[1]::text
FROM xmltable
WHERE(xpath('/document/uuid/text()', xmldata))[1]::text = 'some-uuid-xxxx-xxxx'

и индекс:

CREATE INDEX idx_covering_index on xmltable using btree (
    ((xpath('/document/uuid/text()', xmldata))[1]::text),     
    ((xpath('/document/title/text()', xmldata))[1]::text)
)

Этот индекс является, глядя на него логически, индексом покрытия и должен включать индекс-только-сканирование, поскольку все запрошенные значения содержатся в индексе (uuid и title)

Теперь я знаю, что Postgres распознает только индексы покрытия по функциональным индексам, если столбцы, используемые в вызовах функций, также содержатся

например:.

SELECT to_upper(column1) from table where id >10

1) не может быть охвачен этим индексом:

CREATE INDEX idx_covering_index on xmltable using btree (id, to_upper(column1));

2), но может быть покрыт этим:

CREATE INDEX idx_covering_index on xmltable using btree (column1, id, to_upper(column1));

что приводит к обследованию только по индексу.

Если я сейчас попробую это с моей установкой xml:

CREATE INDEX idx_covering_index on xmltable using btree (xmldata,
    ((xpath('/document/uuid/text()', xmldata))[1]::text),     
    ((xpath('/document/title/text()', xmldata))[1]::text)
)

Я получаю сообщение об ошибке:

тип данных xml не имеет класса оператора по умолчанию для метода доступа "btree"

достаточно справедливо, к сожалению, обычно используемые "text_ops" или "text_pattern_ops" не принимают "xml" в качестве входных данных - таким образом, отображая мой индекс - хотя он будет охватывать все значения - неспособно поддерживать только индексы.

Можно ли это обрабатывать таким образом, чтобы обеспечить возможность сканирования только по индексу?

@EDIT1:

Я знаю, что postgres не может использовать индекс, показанный в 1) как индекс покрытия, но может использовать индекс вроде 2)

Я также пробовал с очень простыми таблицами, чтобы проверить это поведение, и я также помню, что прочитал это, но я не могу, чтобы жизнь меня помнила, где.

create table test (
    id serial primary key,
    quote text
)



insert into test (number, quote) values ('I do not know any clever quotes');
insert into test (number, quote) values ('I am sorry');



CREATE INDEX idx_test_functional on test using btree ((regexp_replace(quote, '^I ', 'BillDoor ')));
set enable_seqscan = off;

analyze test;

explain select quote from test where regexp_replace(quote, '^I ', 'BillDoor ') = 'BillDoor do not know any clever quotes'

--> "Index Scan using idx_test_functional on test  (cost=0.13..8.15 rows=1 width=27)"

drop index idx_test_functional;
CREATE INDEX idx_test_functional on test using btree (quote, (regexp_replace(quote, '^I ', 'BillDoor ')));

analyze test;

explain select quote from test where regexp_replace(quote, '^I ', 'BillDoor ') = 'BillDoor do not know any clever quotes'

--> "Index Only Scan using idx_test_functional on test  (cost=0.13..12.17 rows=1 width=27)"

@EDIT2:

Полное определение таблицы xmltable:

id serial primary key (clustered),
xmldata xml (only data used to filter queries)
history xml (never queried or read, just kept in case of legal inquiry)
fileinfo text (seldom quieried, sometimes retrieved)
"timestamp" timestamp (mainly for legal inquiries too)

Таблица содержит приблизительно: 500 000 записей, размер xmldata составляет от 350 до 800 байт, история намного больше, но редко извлекается и никогда не используется в фильтрах.

Для записи, чтобы получить реальные результаты, я всегда запускал analyze xmltable после создания или удаления индекса

полный план выполнения запроса:

explain analyze select (xpath('/document/uuid/text()', d.xmldata))[1]::text as uuid
from xmltable as d
where
(xpath('/document/uuid/text()', d.xmldata))[1]::text = 'some-uuid-xxxx-xxxx' and (xpath('/document/genre/text()', d.xmldata))[1]::text = 'bio'

охватываемых этими индексами:

create index idx_genre on xmltable using btree (((xpath('/document/genre/text()', xmldata))[1]::text));

create index idx_uuid on xmltable using btree (((xpath('/document/uuid/text()', xmldata))[1]::text)); 

create index idx_uuid_genre on xmltable using btree (((xpath('/document/uuid/text()', xmldata))[1]::text), ((xpath('/document/genre/text()', xmldata))[1]::text));

сначала приводит к:

"Index Scan using idx_genre on xmldata d  (cost=0.42..6303.05 rows=18154 width=32)"
"  Index Cond: (((xpath('/document/genre/text()'::text, xmldata, '{}'::text[]))[1])::text = 'bio'::text)"
"  Filter: (((xpath('/document/uuid/text()'::text, xmldata, '{}'::text[]))[1])::text = 'some-uuid-xxxx-xxxx'::text)"

достаточно справедливо, подумал я, просто для тестирования я заставил его использовать индекс моего охвата:

drop index idx_uuid;
drop index idx_genre;

и теперь я получаю:

"Bitmap Heap Scan on xmltable d  (cost=551.13..16025.51 rows=18216 width=32)"
"  Recheck Cond: ((((xpath('/document/genre/text()'::text, xmldata, '{}'::text[]))[1])::text = 'bio'::text) AND (((xpath('/document/uuid/text()'::text, xmldata, '{}'::text[]))[1])::text = 'some-uuid-xxxx-xxxx'::text))"
"  ->  Bitmap Index Scan on idx_uuid_genre  (cost=0.00..546.58 rows=18216 width=0)"
"        Index Cond: ((((xpath('/document/genre/text()'::text, xmldata, '{}'::text[]))[1])::text = 'bio'::text) AND (((xpath('/document/uuid/text()'::text, xmldata, '{}'::text[]))[1])::text = 'some-uuid-xxxx-xxxx'::text))"

Я также попытался переключить позиции uuid и жанра в индекс, тот же план выполнения.

Ответ 1

EDIT FINAL: Почему это невозможно

Согласно документации: postgresql может выполнять индексирование сканирования, когда тип индекса поддерживает это (т.е. btree всегда поддерживает это, GiST и SpGiST только для некоторых конкретных операторов, а GIN вообще не способен). И можно восстановить исходное индексированное значение из индекса.

Второе требование - самое интересное.

В случае столбцов это просто (a, b), и ваш индекс может восстановить исходное сохраненное значение.

И в случае функций для работы функционального индекса вы должны создать индекс, имеющий исходные значения. Это означает, что индекс (f1(a), f2(b)) снова появится в таблице, потому что вы не сможете восстановить индексированные данные (a, b) из этих значений. Предлагаемое разработчиками решение - создать индекс (f1(a), f2(b), a, b), в этом случае планировщик запросов сможет определить, что можно запустить проверку только для индекса, потому что индекс содержит исходные данные.

И, вернувшись к вашему вопросу, создать индекс только для сканирования в столбце xml невозможно: нет операторов для поддержки сопоставления данных xml, имеющих решающее значение для btree. Не существует определения операторов сравнения для данных xml. и поэтому вы не можете использовать этот столбец в любом виде индекса, но вам нужно его в вашем индексировании только для проверки, чтобы подсказка оптимизатора запросов выполняла только проверку по индексу.

EDIT: (решение, как добиться проверки только индекса для определенных выражений xpath)

Если вы знаете, что эти данные будут использоваться часто, я бы рекомендовал разрешить эту проблему с помощью триггерной функции и создать еще 2 поля и покрыть их индексом. Что-то вроде этого:

ALTER TABLE public.xmltable ADD COLUMN xpath_uuid character varying(36);
ALTER TABLE public.xmltable ADD COLUMN xpath_title character varying(100);


CREATE INDEX idx_covering_materialized_xml_data
  ON public.xmltable
  USING btree
  (xpath_uuid COLLATE pg_catalog."default", xpath_title COLLATE pg_catalog."default");

CREATE OR REPLACE FUNCTION public.introduce_xml_materialization()
  RETURNS trigger AS
$BODY$BEGIN 

NEW.xpath_uuid = (xpath('/document/uuid/text()', NEW.xmldata))[1]::text;
NEW.xpath_title = (xpath('/document/title/text()', NEW.xmldata))[1]::text;

RETURN NEW; 
END;$BODY$
  LANGUAGE plpgsql STABLE
  COST 100;



CREATE TRIGGER index_xml_data
  BEFORE INSERT OR UPDATE
  ON public.xmltable
  FOR EACH ROW
  EXECUTE PROCEDURE public.introduce_xml_materialization();

и вы можете сделать это просто:

SELECT  xpath_uuid, xpath_title
  FROM public.xmltable
  where xpath_uuid = ' uuid1 '

который покажет вам только просмотр по индексу:

"Index Only Scan using idx_covering_materialized_xml_data on xmltable  (cost=0.14..8.16 rows=1 width=308)"
"  Index Cond: (xpath_uuid = ' uuid1 '::text)"

Этот подход будет оптимальным, если предположить, что данные читаются больше, чем написано. Из стоимости вставки или обновления она, как правило, такая же, как создание функционального индекса в выражениях xpath.

ОРИГИНАЛЬНЫЙ ОТВЕТ: (может быть интересным для тех, кто хочет настроить оптимизатор запросов)

Хорошо, проблема в том, что ваш оптимизатор запросов считает, что вызов функции xPath прост. т.е. как вызов простого математического оператора, а его стоимость равна 1. В этом случае оптимизатор запросов думает, что его легче извлечь из таблицы и вычислить еще раз, а затем просто проверить только индекс.

ЕСЛИ вы увеличиваете стоимость вызова xpath, чтобы сказать, что оптимизатор запросов увидит, что такой вызов значительно сложнее (это на самом деле правда) и попытается выполнить проверку только по индексу. В моей тестовой настройке я выполнил

update pg_proc set procost=1 where proname='xpath';  

и план выполнения

"Bitmap Heap Scan on xmltable  (cost=4.17..11.30 rows=3 width=64)"
"  Recheck Cond: (((xpath('/document/uuid/text()'::text, xmldata, '{}'::text[]))[1])::text = 'some-uuid-xxxx-xxxx'::text)"
"  ->  Bitmap Index Scan on idx_covering_index_3  (cost=0.00..4.17 rows=3 width=0)"
"        Index Cond: (((xpath('/document/uuid/text()'::text, xmldata, '{}'::text[]))[1])::text = 'some-uuid-xxxx-xxxx'::text)"

Но когда я делаю

update pg_proc set procost=1000 where proname='xpath';

План выполнения переключается на сканирование только по индексу

"Index Scan using idx_covering_index_3 on xmltable  (cost=0.15..31.20 rows=3 width=64)"
"  Index Cond: (((xpath('/document/uuid/text()'::text, xmldata, '{}'::text[]))[1])::text = 'some-uuid-xxxx-xxxx'::text)"

И на моем томе (т.е. нет данных) минимальная стоимость запроса только для индекса значительно меньше, чем в исходном индексе + сканирование таблицы, при этом максимальная стоимость больше. Таким образом, для вас, чтобы обмануть запрос, может потребоваться установить еще более высокие значения стоимости вызова xpath.

Надеюсь, это поможет, и из любопытства просто покажут нам преимущества использования запросов только по индексу.