将TSTZRANGE加入&&

时间:2015-11-29 20:32:11

标签: sql postgresql database-design postgresql-9.4 postgresql-performance

我正在运行一个在添加记录时逐渐变慢的查询。 通过自动过程连续添加记录(bash调用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"值。我创建了一个触发器触发在阻止重叠范围的任何INSERTUPDATEDELETE之后。

触发器包含一个 类似的部分

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""Bar"(通过"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-partition,其中索引被几个部分索引替换...也许部分将更少维护(和读取)并且性能会提高。我从未见过这样做,只是一个想法。如果这是一个选项,我无法想出任何限制段的好方法,因为这将是TSTZRANGE值。

我还认为将"ParentID"添加到"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 个答案:

答案 0 :(得分:2)

Exclusion constraint

  

此外,表格"Bar"可能不包含重叠"Timespan"   相同"FooID""ParentID"的值。我创建了一个触发器   在任何阻止的INSERTUPDATEDELETE之后触发   重叠范围。

我建议您使用排除约束,这样更简单,更安全,更快捷:

您需要先安装附加模块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 更改为int4。它引用"Foo"."FooID",即serial,即int4。使用匹配类型 int4 (或仅integer)有几个原因,其中一个是性能。

您不再需要触发器(至少不需要此任务),并且您不再创建索引 "Bar_FooID_Timerange_idx" ,因为它是由隐式创建的排除约束。

("ParentID", "FooID")上的btree索引最有可能是有用的,但是:

CREATE INDEX bar_parentid_fooid_idx ON "Bar" ("ParentID", "FooID");

相关:

我之所以选择UNIQUE ("ParentID", "FooID")而不是相反,因为在任一个表中都有另一个带有前导"FooID"的索引:

旁边:Postgres中的I never use double-quoted CaMeL-case identifiers。我这里只是为了遵守你的布局。

避免冗余列

如果您不能或不会冗余地包含"Bar"."ParentID",则会有另一种流氓方式 - 条件是"Foo"."ParentID" 永不更新。例如,使用触发器确保这一点。

你可以伪造IMMUTABLE函数:

CREATE OR REPLACE FUNCTION f_parent_of_foo(int)
  RETURNS int AS
'SELECT "ParentID" FROM public."Foo" WHERE "FooID" = $1'
  LANGUAGE sql IMMUTABLE;

我对表名进行了模式限定,以确保public。适应您的架构。

更多:

然后在排除约束中使用它:

   CONSTRAINT bar_parent_timerange_excl
      EXCLUDE USING gist (f_parent_of_foo("FooID") WITH =, "Timerange" WITH &&)

在保存一个冗余int4列时,验证的约束会更加昂贵,整个解决方案依赖于更多的前提条件。

处理冲突

您可以将INSERTUPDATE包装到plpgsql函数中,并从排除约束(23P01 exclusion_violation)中捕获可能的异常以便以某种方式处理它。

INSERT ...

EXCEPTION
    WHEN exclusion_violation
    THEN  -- handle conflict

完整的代码示例:

处理Postgres 9.5中的冲突

在Postgres 9.5 中,您可以直接使用新的“UPSERT”实施来处理INSERTThe documentation:

  

可选的ON CONFLICT子句指定了替代操作   提出唯一违规或排除约束违规错误。   对于建议插入的每个单独行,要么插入   继续,或者,如果由仲裁者约束或索引指定   违反conflict_target,替代conflict_action   拍摄。 ON CONFLICT DO NOTHING只是避免插入一行   替代行动。 ON CONFLICT DO UPDATE更新现有行   与提议插入的行冲突,作为替代行动。

然而:

  

请注意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;

此示例将检查限制为给定的排除约束。 (我在上面的表定义中明确地为此目的命名了约束。)未捕获其他可能的异常。