如何处理由于范围类型完全相等而导致的错误查询计划?


28

我正在执行更新,其中我需要对tstzrange变量进行完全相等的处理。约100万行被修改,查询耗时约13分钟。的结果EXPLAIN ANALYZE可以在此处看到,实际结果与查询计划者估算的结果有很大不同。问题在于索引扫描开启t_range期望返回一行。

这似乎与以下事实有关:范围类型的统计信息与其他类型的统计信息存储方式不同。综观pg_stats为列图,n_distinct是-1和其它字段(例如most_common_valsmost_common_freqs)是空的。

但是,必须在t_range某处存储统计信息。一个非常相似的更新,其中我在t_range上使用“内”而不是完全相等,需要大约4分钟的时间来执行,并且使用完全不同的查询计划(请参阅此处)。第二个查询计划对我来说很有意义,因为将使用临时表中的每一行以及历史记录表的大部分。更重要的是,查询计划人员为上的过滤器预测了大约正确的行数t_range

的分布t_range有点不寻常。我正在使用此表存储另一个表的历史状态,并且对另一个表的更改会在大型转储中一次全部发生,因此没有许多不同的值t_range。以下是与的每个唯一值相对应的计数t_range

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

t_range以上不同的计数已经完成,因此基数约为3M(其中1M会受到任一更新查询的影响)。

为什么查询1的性能比查询2差得多?就我而言,查询2是一个很好的替代品,但是如果确实需要精确的范围相等性,我如何才能使Postgres使用更智能的查询计划?

带索引的表定义(删除不相关的列):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

查询1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

查询2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1更新999753行,而Q2更新999753 + 36791 = 1036544(即,临时表使得与时间范围条件匹配的每一行都被更新)。

我响应@ypercube的评论尝试了此查询:

查询3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

查询计划和结果(请参阅此处)介于前两个案例之间(约6分钟)。

2016/02/05编辑

1.5年后不再可以访问数据,我创建了具有相同结构(无索引)和相似基数的测试表。jjanes的答案建议原因可能是用于更新的临时表的顺序。由于无法访问track_io_timing(使用Amazon RDS),因此无法直接检验假设。

  1. 总体结果要快得多(好几倍)。我猜这是因为删除了索引,这与Erwin的答案一致。

  2. 在此测试案例中,查询1和2基本上花费相同的时间,因为它们都使用了合并联接。也就是说,我无法触发导致Postgres选择哈希联接的任何原因,因此我不清楚为什么Postgres首先选择性能较差的哈希联接。


1
如果将相等条件转换(a = b)为两个“包含”条件(a @> b AND b @> a)怎么办?计划会改变吗?
ypercubeᵀᴹ

@ypercube:该计划发生了很大变化,尽管它仍然不是很理想-参见我的编辑#2。
2014年

1
另一个想法是(lower(t_range),upper(t_range))自从检查是否相等后,就在上面添加常规btree索引。
ypercubeᵀᴹ

Answers:


9

执行计划中时间上的最大差异是在顶部节点,即UPDATE本身。这表明在更新期间,您大部分时间都将花费在IO上。您可以通过打开track_io_timing并运行查询来验证这一点EXPLAIN (ANALYZE, BUFFERS)

不同的计划显示的行将以不同的顺序进行更新。一个是trip_id有序的,而另一个是以它们在物理上出现在临时表中的任何顺序。

要更新的表似乎具有与trip_id列相关的物理顺序,并且以此顺序更新行会导致具有预读/顺序读取的有效IO模式。虽然临时表的物理顺序似乎会导致很多随机读取。

如果您可以在order by trip_id创建临时表的语句中添加,则可能会为您解决问题。

在计划UPDATE操作时,PostgreSQL不考虑IO排序的影响。(与SELECT操作不同,它考虑了它们)。如果PostgreSQL更聪明,它将意识到一个计划会产生更有效的顺序,或者它将一个显式的排序节点插入到更新及其子节点之间,以便更新将按ctid顺序馈入行。

您认为PostgreSQL在估计范围上相等联接的选择性方面做得不好,这是正确的。但是,这仅与您的基本问题相切。对更新的选择部分进行更有效的查询可能会偶然地使行以更好的顺序输入到正确的更新中,但是如果这样的话,这很大程度上取决于运气。


不幸的是,我无法进行修改track_io_timing,并且(因为已经一年半了!)我无法再访问原始数据。但是,我通过创建具有相同架构和相似大小(几百万行)的表并运行两个不同的更新来测试您的理论,其中一个更新的临时更新表与原始表的排序方式相同,另一个对更新表进行排序准随机的。不幸的是,两个更新花费的时间大致相同,这意味着更新表的顺序不会影响此查询。
abeboparebop '16

7

我不完全确定为什么相等谓词的选择性被tstzrange列上的GiST索引如此高估。虽然这本身仍然很有趣,但这似乎与您的特定情况无关。

由于您UPDATE修改了所有现有3M行的三分之一(!),因此索引根本无济于事。相反,除了表外,增量更新索引将增加您的大量成本UPDATE

只需保留简单的查询1即可。一种简单而彻底的解决方案将索引放在UPDATE。之前。如果您出于其他目的需要它,请在后面重新创建它UPDATE。这仍然要比在大数据期间保持索引更快UPDATE

对于UPDATE所有行的三分之一,可能还需要删除所有其他索引,然后在之后重新创建它们UPDATE。唯一的缺点是:您需要其他特权和表上的排他锁(如果使用,仅需要片刻CREATE INDEX CONCURRENTLY)。

@ypercube的想法是使用btree而不是GiST索引,原则上似乎很好。但不是所有行的三分之一(开头没有索引是好的),也不是仅对(lower(t_range),upper(t_range)),因为tstzrange不是离散范围类型。

大多数离散范围类型具有规范形式,这使“相等”的概念更简单:规范形式的值的上下限对其进行定义。文档:

离散范围类型应具有规范化功能,该功能应了解元素类型所需的步长。规范化函数负责将范围类型的等效值转换为具有相同的表示形式,尤其是始终如一的包含或排除范围。如果未指定规范化函数,则即使格式不同的范围实际上可能表示同一组值,也始终将其视为不相等。

内置范围类型int4rangeint8rangedaterange都使用规范形式,包括下限,但不包括上限;即[)。但是,用户定义的范围类型可以使用其他约定。

情况并非如此tstzrange,为了平等,需要考虑上下限的包容性。可能的btree索引必须处于打开状态:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

而且查询必须在WHERE子句中使用相同的表达式。

可能会试图将整个值转换为索引text(cast(t_range AS text))-但此表达式不是,IMMUTABLE因为timestamptz值的文本表示取决于当前timezone设置。您需要将其他步骤放到IMMUTABLE产生规范形式的包装函数中,并在该函数上创建功能索引。

附加措施/替代方案

如果shape_dist_traveled已经具有与tt.shape_dist_traveled多个更新行相同的值(并且您不依赖于UPDATE类似触发器的任何副作用...),则可以通过排除空更新来加快查询速度:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

当然,适用于性能优化的所有一般建议。Postgres Wiki是一个很好的起点。

VACUUM FULL对您而言,这将是毒药,因为一些无效的元组(或预留的空间FILLFACTOR)对UPDATE性能很有帮助。

有了这么多更新的行,并且如果您能负担得起(没有并发访问或其他依赖性),那么编写一个全新的表而不是就地更新可能会更快。相关答案中的说明:

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.