Запрос Postgresql 9.4 становится все медленнее при присоединении к TSTZRANGE с &&
я использую запрос, который становится все медленнее, по мере добавления записей. записи добавляются непрерывно через автоматизированный процесс (bash calling psql). я хотел бы исправить это горлышко бутылки; однако, я не знаю, что мой лучший вариант.
Hour Count Duration Avg duration
00 9,990 10m3s 60ms <---ignore this hour
02 1 60ms 60ms <---ignore this hour
03 4,638 1m54s 24ms <---queries begin with table empty
04 30,991 55m49s 108ms <---first full hour of queries running
05 13,497 58m3s 258ms
06 9,904 58m32s 354ms
07 10,542 58m25s 332ms
08 8,599 58m42s 409ms
09 7,360 58m52s 479ms
10 6,661 58m57s 531ms
11 6,133 59m2s 577ms
12 5,601 59m6s 633ms
13 5,327 59m9s 666ms
14 4,964 59m12s 715ms
15 4,759 59m14s 746ms
16 4,531 59m17s 785ms
17 4,330 59m18s 821ms
18 939 13m16s 848ms
структура таблицы выглядит так:
CREATE TABLE "Parent" (
"ParentID" SERIAL PRIMARY KEY,
"Details1" VARCHAR
);
стол "Parent"
имеет один-ко-многим отношения с таблица "Foo"
:
CREATE TABLE "Foo" (
"FooID" SERIAL PRIMARY KEY,
"ParentID" int4 NOT NULL REFERENCES "Parent" ("ParentID"),
"Details1" VARCHAR
);
стол "Foo"
имеет отношение один ко многим с таблицей "Bar"
:
CREATE TABLE "Bar" (
"FooID" int8 NOT NULL REFERENCES "Foo" ("FooID"),
"Timerange" tstzrange NOT NULL,
"Detail1" VARCHAR,
"Detail2" VARCHAR,
CONSTRAINT "Bar_pkey" PRIMARY KEY ("FooID", "Timerange")
);
CREATE INDEX "Bar_FooID_Timerange_idx" ON "Bar" USING gist("FooID", "Timerange");
кроме того, в таблице "Bar"
может не содержать перекрытия "Timespan"
значение "FooID"
или "ParentID"
. я создал триггер, который срабатывает после любого INSERT
, UPDATE
или DELETE
это предотвращает перекрытие диапазонов.
на триггер включает в себя раздел этот взгляд как для этого:
WITH
"cte" AS (
SELECT
"Foo"."FooID",
"Foo"."ParentID",
"Foo"."Details1",
"Bar"."Timespan"
FROM
"Foo"
JOIN "Bar" ON "Foo"."FooID" = "Bar"."FooID"
WHERE
"Foo"."FooID" = 1234
)
SELECT
"Foo"."FooID",
"Foo"."ParentID",
"Foo"."Details1",
"Bar"."Timespan"
FROM
"cte"
JOIN "Foo" ON
"cte"."ParentID" = "Foo"."ParentID"
AND "cte"."FooID" <> "Foo"."FooID"
JOIN "Bar" ON
"Foo"."FooID" = "Bar"."FooID"
AND "cte"."Timespan" && "Bar"."Timespan";
результаты EXPLAIN ANALYSE
:
Nested Loop (cost=7258.08..15540.26 rows=1 width=130) (actual time=8.052..147.792 rows=1 loops=1)
Join Filter: ((cte."FooID" <> "Foo"."FooID") AND (cte."ParentID" = "Foo"."ParentID"))
Rows Removed by Join Filter: 76
CTE cte
-> Nested Loop (cost=0.68..7257.25 rows=1000 width=160) (actual time=1.727..1.735 rows=1 loops=1)
-> Function Scan on "fn_Bar" (cost=0.25..10.25 rows=1000 width=104) (actual time=1.699..1.701 rows=1 loops=1)
-> Index Scan using "Foo_pkey" on "Foo" "Foo_1" (cost=0.42..7.24 rows=1 width=64) (actual time=0.023..0.025 rows=1 loops=1)
Index Cond: ("FooID" = "fn_Bar"."FooID")
-> Nested Loop (cost=0.41..8256.00 rows=50 width=86) (actual time=1.828..147.188 rows=77 loops=1)
-> CTE Scan on cte (cost=0.00..20.00 rows=1000 width=108) (actual time=1.730..1.740 rows=1 loops=1)
**** -> Index Scan using "Bar_FooID_Timerange_idx" on "Bar" (cost=0.41..8.23 rows=1 width=74) (actual time=0.093..145.314 rows=77 loops=1)
Index Cond: ((cte."Timespan" && "Timespan"))
-> Index Scan using "Foo_pkey" on "Foo" (cost=0.42..0.53 rows=1 width=64) (actual time=0.004..0.005 rows=1 loops=77)
Index Cond: ("FooID" = "Bar"."FooID")
Planning time: 1.490 ms
Execution time: 147.869 ms
(**** выделено мной)
это, кажется, показывает, что 99% выполняемой работы находится в JOIN
С "cte"
to "Bar"
(via "Foo"
) ... но он уже использует соответствующий индекс... все еще слишком медленно.
и я побежал:
SELECT
pg_size_pretty(pg_relation_size('"Bar"')) AS "Table",
pg_size_pretty(pg_relation_size('"Bar_FooID_Timerange_idx"')) AS "Index";
результаты:
Table | Index
-------------|-------------
283 MB | 90 MB
делает индекс этого размера (относительно таблицу) предлагают много с точки зрения производительности? Я рассматривал sudo-раздел, где индекс заменяется несколькими частичными индексами... возможно, частичные будут иметь меньше для поддержания (и чтения), и производительность улучшится. Я никогда не видел, как это делается, просто идея. Если это вариант, я не могу придумать хороший способ ограничить сегменты, учитывая, что это будет на TSTZRANGE
значение.
я также думаю, что добавление "ParentID"
to "Bar"
ускорит процесс, но я не хочу денормализовать.
какие еще варианты у меня есть?
влияние изменений, рекомендованных Erwin Brandstetter
на пике производительности (час 18:00), процесс добавления 14.5 записей в секунду последовательно... до 1,15 записи в секунду.
это было результатом:
- добавлять
"ParentID"
в таблице"Bar"
- добавление ограничения внешнего ключа к
"Foo" ("ParentID", "FooID")
- добавлять
EXCLUDE USING gist ("ParentID" WITH =, "Timerange" WITH &&) DEFERRABLE INITIALLY DEFERRED
(модуль btree_gist уже установлен)
1 ответов
ограничение отчуждения
кроме того, в таблице
"Bar"
может не содержать перекрытия"Timespan"
значения для того же"FooID"
или"ParentID"
. Я создал триггер что стреляет после любогоINSERT
,UPDATE
илиDELETE
что мешает перекрывающиеся диапазоны.
я предлагаю вам вместо этого использовать ограничение исключения, которое намного проще, безопаснее и быстрее:
необходимо установить дополнительный модуль btree_gist
первый. См. инструкции и объяснение в этом соответствующем ответе:
и вам нужно включить "ParentID"
в таблице "Bar"
избыточно, что будет небольшой ценой для оплаты. Определения таблиц могут выглядеть следующим образом:
CREATE TABLE "Foo" (
"FooID" serial PRIMARY KEY
"ParentID" int4 NOT NULL REFERENCES "Parent"
"Details1" varchar
CONSTRAINT foo_parent_foo_uni UNIQUE ("ParentID", "FooID") -- required for FK
);
CREATE TABLE "Bar" (
"ParentID" int4 NOT NULL,
"FooID" int4 NOT NULL REFERENCES "Foo" ("FooID"),
"Timerange" tstzrange NOT NULL,
"Detail1" varchar,
"Detail2" varchar,
CONSTRAINT "Bar_pkey" PRIMARY KEY ("FooID", "Timerange"),
CONSTRAINT bar_foo_fk
FOREIGN KEY ("ParentID", "FooID") REFERENCES "Foo" ("ParentID", "FooID"),
CONSTRAINT bar_parent_timerange_excl
EXCLUDE USING gist ("ParentID" WITH =, "Timerange" WITH &&)
);
я также изменил тип данных "Bar"."FooID"
С to int8
int4
. Он ссылается "Foo"."FooID"
, который является serial
, то есть int4
. Используйте соответствующий тип int4
(или просто integer
) по нескольким причинам, одна из них-производительность.
вам больше не нужен триггер (по крайней мере, не для этой задачи), и вы не создаете индекс больше, так как он создается неявно ограничением исключения."Bar_FooID_Timerange_idx"
индекс btree на ("ParentID", "FooID")
, скорее всего, будет полезно, хотя:
CREATE INDEX bar_parentid_fooid_idx ON "Bar" ("ParentID", "FooID");
по теме:
я выбрал UNIQUE ("ParentID", "FooID")
, а не наоборот, так как есть еще один индекс с ведущими "FooID"
в таблице:
в сторону: я никогда не использую двойные кавычки Верблюд-дело идентификаторы в Postgres. Я делаю это только для того, чтобы соответствовать вашему плану.
избегайте избыточного столбца
если вы не можете или не включать "Bar"."ParentID"
избыточно, есть еще один rogue путь-при условии, что "Foo"."ParentID"
is никогда не обновляется!--58-->. Убедитесь в этом, например, с помощью триггера.
вы можете поддельные IMMUTABLE
функция:
CREATE OR REPLACE FUNCTION f_parent_of_foo(int)
RETURNS int AS
'SELECT "ParentID" FROM public."Foo" WHERE "FooID" = '
LANGUAGE sql IMMUTABLE;
I schema-квалифицированное имя таблицы для убедитесь, предполагая, что public
. Адаптируйтесь к вашей схеме.
Подробнее:
- ограничение для проверки значений из удаленно связанной таблицы (через join etc.)
- работает PostgreSQL "акцент нечувствительным" сортировки?
затем используйте его в ограничении исключения:
CONSTRAINT bar_parent_timerange_excl
EXCLUDE USING gist (f_parent_of_foo("FooID") WITH =, "Timerange" WITH &&)
при сохранении одного избыточного :
обратите внимание, что ограничения, исключения не поддерживаются с
ON CONFLICT DO UPDATE
.
но вы все еще можете использовать ON CONFLICT DO NOTHING
, таким образом избегая возможных exclusion_violation
исключения. Просто проверьте, были ли обновлены какие-либо строки, что дешевле:
INSERT ...
ON CONFLICT ON CONSTRAINT bar_parent_timerange_excl DO NOTHING;
IF NOT FOUND THEN
-- handle conflict
END IF;
этот пример ограничивает проверку заданным ограничением исключения. (Я назвал ограничение явно для этой цели в определении таблицы выше.) Другие возможные исключения не уловлены.