Ответ 1
Ваша функция может быть радикально упрощена.
Настройка
Я предлагаю вам преобразовать столбец paths.path
в тип данных geography
(или, по крайней мере, geometry
). path
является родным типом Postgres и не очень хорошо работает с функциями PostGIS и пространственными индексами. Вам нужно было бы сделать path::geometry
или path::geometry::geography
(в результате LINESTRING
внутренне), чтобы он работал с функциями PostGIS, такими как ST_Intersects()
.
Мой ответ основан на этих адаптированных таблицах:
CREATE TABLE paths (
id uuid PRIMARY KEY
, path geography NOT NULL
);
CREATE TABLE geographies (
id uuid PRIMARY KEY
, geography geography NOT NULL
, fk_id text NOT NULL
);
Все работает с типом данных geometry
для обоих столбцов. geography
, как правило, более точный, но и более дорогой. Что использовать? Читайте здесь FAQ по PostGIS.
Решение 1: оптимизирована функция
CREATE OR REPLACE FUNCTION public.function_name(_fk_ids text[])
RETURNS TABLE(id uuid, type text) AS
$func$
DECLARE
_row_ct int;
_loop_ct int := 0;
BEGIN
CREATE TEMP TABLE _geo ON COMMIT DROP AS -- dropped at end of transaction
SELECT DISTINCT ON (g.id) g.id, g.geography, _loop_ct AS loop_ct -- dupes possible?
FROM geographies g
WHERE g.fk_id = ANY(_fk_ids);
GET DIAGNOSTICS _row_ct = ROW_COUNT;
IF _row_ct = 0 THEN -- no rows found, return empty result immediately
RETURN; -- exit function
END IF;
CREATE TEMP TABLE _path ON COMMIT DROP AS
SELECT DISTINCT ON (p.id) p.id, p.path, _loop_ct AS loop_ct
FROM _geo g
JOIN paths p ON ST_Intersects(g.geography, p.path); -- no dupes yet
GET DIAGNOSTICS _row_ct = ROW_COUNT;
IF _row_ct = 0 THEN -- no rows found, return _geo immediately
RETURN QUERY SELECT g.id, text 'geo' FROM _geo g;
RETURN;
END IF;
ALTER TABLE _geo ADD CONSTRAINT g_uni UNIQUE (id); -- required for UPSERT
ALTER TABLE _path ADD CONSTRAINT p_uni UNIQUE (id);
LOOP
_loop_ct := _loop_ct + 1;
INSERT INTO _geo(id, geography, loop_ct)
SELECT DISTINCT ON (g.id) g.id, g.geography, _loop_ct
FROM _paths p
JOIN geographies g ON ST_Intersects(g.geography, p.path)
WHERE p.loop_ct = _loop_ct - 1 -- only use last round!
ON CONFLICT ON CONSTRAINT g_uni DO NOTHING; -- eliminate new dupes
EXIT WHEN NOT FOUND;
INSERT INTO _path(id, path, loop_ct)
SELECT DISTINCT ON (p.id) p.id, p.path, _loop_ct
FROM _geo g
JOIN paths p ON ST_Intersects(g.geography, p.path)
WHERE g.loop_ct = _loop_ct - 1
ON CONFLICT ON CONSTRAINT p_uni DO NOTHING;
EXIT WHEN NOT FOUND;
END LOOP;
RETURN QUERY
SELECT g.id, text 'geo' FROM _geo g
UNION ALL
SELECT p.id, text 'path' FROM _path p;
END
$func$ LANGUAGE plpgsql;
Вызов:
SELECT * FROM public.function_name('{foo,bar}');
Много быстрее, чем у вас.
Основные моменты
-
Вы используете запросы на весь набор, а не только последние дополнения к набору. Это становится все медленнее с каждым циклом без необходимости. Я добавил счетчик циклов (
loop_ct
) в избежать избыточной работы. -
Обязательно иметь пространственные значения GiST индексы на
geographies.geography
иpaths.path
:CREATE INDEX geo_geo_gix ON geographies USING GIST (geography); CREATE INDEX paths_path_gix ON paths USING GIST (path);
Так как Postgres 9.5 индексирование только сканирование будет вариантом для индексов GiST. Вы можете добавить
id
в качестве второго столбца индекса. Выгода зависит от многих факторов, вам придется протестировать. Тем не менее, для типаuuid
не существует подходящего оператора GiST-класса. Он будет работать сbigint
после установки расширения btree_gist: -
Иметь подходящий индекс на
g.fk_id
. Опять же, индекс многоколонок на(fk_id, id, geography)
может заплатить, если вы можете получить от него только индексные проверки. Значение по умолчанию btree index,fk_id
должно быть первым столбцом индекса. Особенно, если вы часто запускаете запрос и редко обновляете таблицу, а строки таблицы намного шире, чем индекс. -
Вы можете инициализировать переменные во время объявления. Требуется только один раз после перезаписи.
-
ON COMMIT DROP
автоматически отбрасывает временные таблицы в конце транзакции. Таким образом, я удалил явные таблицы. Но вы получаете исключение, если вы дважды вызываете функцию в одной транзакции. В этой функции я бы проверял существование таблицы temp и использовалTRUNCATE
в этом случае. Связанный: -
Используйте
GET DIAGNOSTICS
, чтобы получить счетчик строк вместо запуска другого запроса для счетчика.Вам не нужно рассчитывать вообще после перезаписи. Дешево проверитьFOUND
.
На самом деле вам нужноGET DIAGNOSTICS
.CREATE TABLE
не устанавливаетFOUND
(как указано в руководстве). У меня была вашаINSERT
в моей оригинальной (проверенной) функции, которая устанавливаетFOUND
, следовательно, надзор. Исправлено. -
Быстрее добавить индекс или ограничение PK/UNIQUE после заполнения таблицы. И не раньше, чем мы действительно нуждаемся в этом.
-
ON CONFLICT ... DO ...
- это более простой и дешевый способ для UPSERT с Postgres 9.5.Для простой формы команды вы просто указываете столбцы или выражения индекса (например,
ON CONFLICT (id) DO ...
) и пусть Postgres выполняют уникальный вывод индекса для определения ограничения или индекса арбитра. Позже я оптимизировал, предоставив ограничение напрямую. Но для этого нам нужно фактическое ограничение - уникального индекса недостаточно. Исправлено. Подробности в руководстве здесь. -
Это может помочь временным таблицам
ANALYZE
вручную, чтобы помочь Postgres найти лучший план запроса. (Но я не думаю, что вам это нужно в вашем случае.) -
_geo_ct - _geographyLength > 0
- неудобный и дорогой способ сказать_geo_ct > _geographyLength
. Но это полностью исчезло. -
Не указывайте имя языка. Просто
LANGUAGE plpgsql
. -
Функциональный параметр
varchar[]
для массиваfk_id
, но вы позже прокомментировали:Это поле
bigint
, которое представляет географическую область (это фактически вычисленныйs2cell
id на уровне 15).Я не знаю
s2cell
id на уровне 15, но в идеале вы передаете массив соответствующего типа данных, или если это не опция по умолчаниюtext[]
.Также, поскольку вы прокомментировали:
Всегда есть только 13
fk_id
.Это кажется идеальным вариантом использования для параметра функции
VARIADIC
. Таким образом, ваше определение функции будет:CREATE OR REPLACE FUNCTION public.function_name(_fk_ids VARIADIC text[]) ...
Подробнее:
Решение 2: простой SQL с рекурсивным CTE
Трудно обернуть rCTE вокруг двух чередующихся циклов, но возможно с некоторой тонкостью SQL:
WITH RECURSIVE cte AS (
SELECT g.id, g.geography::text, NULL::text AS path, text 'geo' AS type
FROM geographies g
WHERE g.fk_id = ANY($kf_ids) -- your input array here
UNION
SELECT p.id, g.geography::text, p.path::text
, CASE WHEN p.path IS NULL THEN 'geo' ELSE 'path' END AS type
FROM cte c
LEFT JOIN paths p ON c.type = 'geo'
AND ST_Intersects(c.geography::geography, p.path)
LEFT JOIN geographies g ON c.type = 'path'
AND ST_Intersects(g.geography, c.path::geography)
WHERE (p.path IS NOT NULL OR g.geography IS NOT NULL)
)
SELECT id, type FROM cte;
Это все. Вам нужны те же самые индексы, что и выше. Вы можете перевести его в функцию SQL или PL/pgSQL для повторного использования.
Дополнительные дополнительные точки
-
Приведение в
text
необходимо, потому что типgeography
не является "хешируемым" (тот же дляgeometry
). (Подробнее см. эту открытую проблему PostGIS.) Обходите ее, выбравtext
. Строки уникальны только в силу(id, type)
, мы можем игнорировать столбцыgeography
для этого. Вернитесь кgeography
для соединения. Не стоит дорожать слишком много. -
Нам нужно два
LEFT JOIN
, чтобы не исключать строки, потому что на каждой итерации только одна из двух таблиц может вносить больше строк.
Последнее условие гарантирует, что мы еще не закончили:WHERE (p.path IS NOT NULL OR g.geography IS NOT NULL)
Это работает, потому что дублирующие выводы исключаются из временного промежуточная таблица. Руководство:
Для
UNION
(но неUNION ALL
) отбросить повторяющиеся строки и строки, которые дублируйте любую предыдущую строку результатов. Включите все остальные строки в результат рекурсивного запроса, а также поместить их во временный промежуточная таблица.
Итак, что быстрее?
rCTE, вероятно, быстрее, чем функция для небольших наборов результатов. Временные таблицы и индексы в функции означают значительно больше накладных расходов. Однако для больших наборов результатов функция может быть быстрее. Только тестирование с вашей фактической настройкой может дать вам окончательный ответ. *
* См. обратную связь OP в комментарии.