Запрос Postgresql 9.4 становится все медленнее при присоединении к TSTZRANGE с &&

я использую запрос, который становится все медленнее, по мере добавления записей. записи добавляются непрерывно через автоматизированный процесс (bash calling psql). я хотел бы исправить это горлышко бутылки; однако, я не знаю, что мой лучший вариант.

это выход из pgBadger: Chart of queries executed per hour

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

Chart of queries executed per hour

на пике производительности (час 18:00), процесс добавления 14.5 записей в секунду последовательно... до 1,15 записи в секунду.

это было результатом:

  1. добавлять "ParentID" в таблице "Bar"
  2. добавление ограничения внешнего ключа к "Foo" ("ParentID", "FooID")
  3. добавлять 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" С int8 to 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. Адаптируйтесь к вашей схеме.

Подробнее:

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

   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;

этот пример ограничивает проверку заданным ограничением исключения. (Я назвал ограничение явно для этой цели в определении таблицы выше.) Другие возможные исключения не уловлены.