如何处理由范围类型完全相等引起的错误查询计划?

abe*_*bop 29 postgresql performance postgresql-9.3 range-types query-performance

我正在执行更新,我需要一个tstzrange变量完全相等。修改了大约 100 万行,查询需要大约 13 分钟。的结果EXPLAIN ANALYZE可以在这里看到,实际结果与查询计划器估计的结果有很大的不同。问题是索引扫描t_range期望返回单行。

这似乎与范围类型的统计信息与其他类型的统计信息的存储方式有关。pg_stats查看列的视图,n_distinct是 -1,其他字段(例如most_common_valsmost_common_freqs)为空。

但是,必须在t_range某处存储统计信息。我在 t_range 上使用 'within' 而不是完全相等的极其相似的更新需要大约 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
Run Code Online (Sandbox Code Playgroud)

t_range上面distinct的计数是完整的,所以基数是~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)
Run Code Online (Sandbox Code Playgroud)

查询 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;
Run Code Online (Sandbox Code Playgroud)

查询 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;
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

查询计划和结果(参见此处)介于前两种情况(约 6 分钟)之间。

2016/02/05 编辑

1.5 年后不再访问数据,我创建了一个具有相同结构(没有索引)和类似基数的测试表。jjanes 的回答提出,原因可能是用于更新的临时表的顺序。我无法直接测试假设,因为我无权访问track_io_timing(使用 Amazon RDS)。

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

  2. 在这个测试案例中,查询 1 和查询 2 花费的时间基本相同,因为它们都使用了合并连接。也就是说,我无法触发任何导致 Postgres 选择散列连接的原因,所以我不清楚 Postgres 一开始为什么选择性能不佳的散列连接。

jja*_*nes 9

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

不同的计划以不同的顺序呈现要更新的行。一个是trip_id有序的,另一个是它们碰巧出现在临时表中的顺序。

正在更新的表的物理顺序似乎与 trip_id 列相关,按此顺序更新行会导致具有预读/顺序读取的高效 IO 模式。而临时表的物理顺序似乎会导致大量随机读取。

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

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

您是正确的,PostgreSQL 在估计范围上的等式连接的选择性方面做得很差。但是,这仅与您的基本问题有切线相关。对更新的选择部分进行更有效的查询可能会意外地以更好的顺序将行馈入更新正确,但如果是这样,这主要取决于运气。


Erw*_*ter 7

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

由于您UPDATE修改了所有现有 3M 行的三分之一 (!),因此索引根本没有帮助。相反,除了表之外,增量更新索引会给您的UPDATE.

只需保留简单的Query 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))
Run Code Online (Sandbox Code Playgroud)

并且查询必须在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;
Run Code Online (Sandbox Code Playgroud)

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

VACUUM FULL对你来说是毒药,因为一些死元组(或由 保留的空间FILLFACTOR)对UPDATE性能有益。

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