使用 && 加入 TSTZRANGE 时,Postgresql 9.4 查询变得越来越慢
Posted
技术标签:
【中文标题】使用 && 加入 TSTZRANGE 时,Postgresql 9.4 查询变得越来越慢【英文标题】:Postgresql 9.4 query gets progressively slower when joining TSTZRANGE with && 【发布时间】:2016-03-03 10:44:37 【问题描述】:我正在运行的查询随着记录的添加而逐渐变慢。 记录是通过自动化过程(bash 调用 psql)不断添加的。 我想纠正这个瓶颈;但是,我不知道我最好的选择是什么。
这是 pgBadger 的输出:
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"
可能不包含相同"FooID"
或"ParentID"
的重叠"Timespan"
值。 我创建了一个触发器,它在任何INSERT
、@987654338 之后触发@ 或 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% 的工作都在从 "cte"
到 "Bar"
的 JOIN
中(通过 "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"
添加到"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 模块已安装)
【问题讨论】:
使用 CTE 有什么特别的原因吗?在 PostgreSQL 中,CTE 是一个优化器栅栏,可以防止一些优化。我会尝试没有。 @DavidAldridge - CTE 稍后在查询中是必需的,每个EXPLAIN ANALYSE
只表示相关部分;但是,就您而言,结果是相同的......瓶颈在从 CTE(或重写的子查询)到表 "Bar"
通过 "Foo"
的 JOIN 中。
由于所有这些假表和索引名称,很难理解解释计划中发生的事情。
"ParentID" int4 NOT NULL REFERENCES "Parents" ("ParentID"),
此处显示了 parent_id 的支持索引:create index on "Foo" ("parentID");
没有。只有 FK 的“目标”需要至少有一个 UNIQUE 约束。 [但你仍然需要摆脱 cte,恕我直言]
【参考方案1】:
Exclusion constraint
此外,表
"Bar"
可能不包含重叠的"Timespan"
相同"FooID"
或"ParentID"
的值。我创建了一个触发器 在任何INSERT
、UPDATE
或DELETE
之后触发 重叠范围。
我建议你改用排除约束,这样更简单、更安全、更快捷:
您需要先安装附加模块btree_gist
。请参阅此相关答案中的说明和解释:
并且您需要在"Bar"
表中冗余地包含"ParentID"
,这将是一个很小的代价。表定义可能如下所示:
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");
相关:
Preventing adjacent/overlapping entries with EXCLUDE in PostgreSQL我选择了UNIQUE ("ParentID", "FooID")
而不是相反的原因,因为在任一表中都有另一个以"FooID"
开头的索引:
旁白:I never use double-quoted CaMeL-case identifiers 在 Postgres 中。我这里只是为了符合你的布局。
避免冗余列
如果您不能或不会冗余地包含"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 to check values from a remotely related table (via join etc.) Does PostgreSQL support "accent insensitive" collations?然后在排除约束中使用:
CONSTRAINT bar_parent_timerange_excl
EXCLUDE USING gist (f_parent_of_foo("FooID") WITH =, "Timerange" WITH &&)
虽然节省了一个冗余的int4
列,但验证约束的成本会更高,并且整个解决方案取决于更多的先决条件。
处理冲突
您可以将 INSERT
和 UPDATE
包装到 plpgsql 函数中,并从排除约束 (23P01 exclusion_violation
) 中捕获可能的异常以某种方式处理它。
INSERT ...
EXCEPTION
WHEN exclusion_violation
THEN -- handle conflict
完整的代码示例:
Handling EXCEPTION and return result from function在 Postgres 9.5 中处理冲突
在 Postgres 9.5 中,您可以使用新的“UPSERT”实现直接处理INSERT
。 The documentation:
可选的
ON CONFLICT
子句指定了一个替代操作 引发唯一违反或排除约束违反错误。 对于建议插入的每一行,插入 继续进行,或者,如果 arbiter 约束或索引由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;
此示例将检查限制为给定的排除约束。 (为此,我在上面的表定义中明确命名了约束。)其他可能的异常没有被捕获。
【讨论】:
+1 表示复合外键(在这种情况下我从未想过)。EXCLUDE
是可取的,但我之前(过早地)排除了它。在我最初的问题中,触发的触发器不只是检查冲突......它使用冲突的记录(和业务规则)来计算冲突的修复(本质上是UPDATE
违规记录)。使用EXCLUDE
约束时,是否可以在发现冲突时返回违规记录? ...也许使用 pl/pgsql 和 EXCEPTION WHEN...
@losthorse:考虑一下我在上面添加的替代方案。
我在问题的末尾添加了您的建议的影响。以上是关于使用 && 加入 TSTZRANGE 时,Postgresql 9.4 查询变得越来越慢的主要内容,如果未能解决你的问题,请参考以下文章
laravel加入验证码类几种方法 && Laravel引入第三方库的方法
ArrayList&LinkedList&Map&Arrays