通过性能改善分区?

时间:2014-09-30 17:31:17

标签: postgresql query-optimization database-performance window-functions

考虑下表:

 foo | bar
-----+-----
  3  |  1
  8  |  1
  2  |  1
  8  |  5
  6  |  5
  5  |  5
  4  |  5
  5  |  7
  4  |  7

foo包含任何内容。列bar 几乎排序,并且公共bar值的行彼此跟随。表中包含约170万行,每个不同bar值约15行。

我发现PARTITION BY非常缓慢而且我想知道我是否可以采取任何措施来改善其效果?

我尝试CREATE INDEX bar_idx ON foobar(bar),但它对性能没有影响(IRL在表的另一列上已经存在主键)。我正在使用PostgreSQL 9.3.5。

以下是使用和不使用EXPLAIN ANALYZE的简单查询的PARTITION BY

> EXPLAIN ANALYZE SELECT count(foo) OVER (PARTITION BY bar) FROM foobar;
                                                           QUERY PLAN                                                       
--------------------------------------------------------------------------------------------------------------------------------
 WindowAgg  (cost=262947.92..293133.35 rows=1724882 width=8) (actual time=2286.082..3504.372 rows=1724882 loops=1)
   ->  Sort  (cost=262947.92..267260.12 rows=1724882 width=8) (actual time=2286.068..2746.047 rows=1724882 loops=1)
         Sort Key: bar
         Sort Method: external merge  Disk: 27176kB
         ->  Seq Scan on foobar  (cost=0.00..37100.82 rows=1724882 width=8) (actual time=0.019..441.827 rows=1724882 loops=1)
 Total runtime: 3606.695 ms
(6 lignes)

> EXPLAIN ANALYZE SELECT foo FROM foobar;
                                                     QUERY PLAN                                                 
--------------------------------------------------------------------------------------------------------------------
 Seq Scan on foobar  (cost=0.00..37100.82 rows=1724882 width=4) (actual time=0.014..385.931 rows=1724882 loops=1)
 Total runtime: 458.776 ms
(2 lignes)

第一项改进,增加work_mem:

在大多数情况下,正如hbn所建议的那样增加work_mem应该会有所帮助。在我的情况下,我正在使用SSD,因此切换到RAM(将work_mem增加到1 GB)只会将处理时间缩短1.5:

> EXPLAIN (ANALYZE, BUFFERS) SELECT foo OVER (PARTITION BY bar) FROM foobar;
                                                           QUERY PLAN                                                           
--------------------------------------------------------------------------------------------------------------------------------
 WindowAgg  (cost=215781.92..245967.35 rows=1724882 width=8) (actual time=933.575..1931.656 rows=1724882 loops=1)
   Buffers: shared hit=2754 read=17098
   ->  Sort  (cost=215781.92..220094.12 rows=1724882 width=8) (actual time=933.558..1205.314 rows=1724882 loops=1)
         Sort Key: bar
         Sort Method: quicksort  Memory: 130006kB
         Buffers: shared hit=2754 read=17098
         ->  Seq Scan on foobar  (cost=0.00..37100.82 rows=1724882 width=8) (actual time=0.023..392.446 rows=1724882 loops=1)
               Buffers: shared hit=2754 read=17098
 Total runtime: 2051.494 ms
(9 lignes)

使用CLUSTER进行第二次改进:

我尝试了this post的一些建议 - 增加的统计数据对我的情况没有显着影响。唯一有帮助或尚未处于活动状态的是使用CLUSTER以索引的物理顺序重写表格”(您可能更喜欢pg_repack,阅读原文交的):

> CLUSTER foobar USING bar_idx;
CLUSTER
> EXPLAIN (ANALYZE, BUFFERS) SELECT count(foo) OVER (PARTITION BY bar) FROM foobar;
                                                                  QUERY PLAN                                                                  
----------------------------------------------------------------------------------------------------------------------------------------------
 WindowAgg  (cost=0.43..150079.25 rows=1724882 width=8) (actual time=0.031..1372.416 rows=1724882 loops=1)
   Buffers: shared hit=64 read=24503
   ->  Index Scan using bar_idx on foobar  (cost=0.43..124206.02 rows=1724882 width=8) (actual time=0.018..581.665 rows=1724882 loops=1)
         Buffers: shared hit=64 read=24503
 Total runtime: 1484.974 ms
(5 lignes)

第三项改进,表格的子集:

在我的情况下,我最终需要在此表中选择另一个表,因此将表的子集创建为自己的表似乎是有意义的:

CREATE TABLE subfoobar AS (SELECT * FROM foobar WHERE bar IN (SELECT DISTINCT bar FROM othertable) ORDER BY bar);

新表只有700k行而不是170万行,并且查询时间似乎(在bar上重新创建索引之后)大致成比例,因此增益很大:

> EXPLAIN (ANALYZE, BUFFERS) SELECT count(foo) OVER (PARTITION BY bar) FROM subfoobar;
                                                                      QUERY PLAN                                                                       
-------------------------------------------------------------------------------------------------------------------------------------------------------
 WindowAgg  (cost=0.42..37455.61 rows=710173 width=8) (actual time=0.025..543.437 rows=710173 loops=1)
   Buffers: shared hit=10290
   ->  Index Scan using bar_sub_idx on subfoobar  (cost=0.42..26803.02 rows=710173 width=8) (actual time=0.015..222.211 rows=710173 loops=1)
         Buffers: shared hit=10290
 Total runtime: 590.063 ms
(5 lignes)

第四项改进,摘要表:

由于IRL窗口函数在查询中多次涉及,查询本身将被执行多次(数据挖掘),并且分区上的聚合结果将始终相同,我决定选择更多有效的方法:我将所有这些值提取到一个新的“汇总表”中(不确定我的定义是否与“官方”匹配?)。

在我们的简单示例中,这将给出

CREATE TABLE summary_foobar AS SELECT DISTINCT ON (bar) count(foo) OVER (PARTITION BY bar) AS cfoo, bar FROM foobar;

实际上,正如hbn在评论中所建议的,最好是创建一个MATERIALIZED VIEW而不是新表,以便我们可以随时使用REFRESH MATERIALIZED VIEW summary_foobar;更新它:

CREATE MATERIALIZED VIEW summary_foobar AS SELECT DISTINCT ON (bar) count(foo) OVER (PARTITION BY bar) AS cfoo, bar FROM foobar;

然后,将初始查询应用于我的真实案例表:

> EXPLAIN (ANALYZE, BUFFERS) SELECT cfoo FROM subfoobar,summary_foobar WHERE subfoobar.bar=summary_foobar.bar;
                                                          QUERY PLAN                                                      
------------------------------------------------------------------------------------------------------------------------------
 Hash Join  (cost=1254.64..28939.67 rows=424685 width=73) (actual time=9.893..268.704 rows=370393 loops=1)
   Hash Cond: (subfoobar.bar = summary_foobar.bar)
   Buffers: shared hit=8916
   ->  Seq Scan on subfoobar  (cost=0.00..15448.73 rows=710173 width=4) (actual time=0.003..70.850 rows=710173 loops=1)
         Buffers: shared hit=8347
   ->  Hash  (cost=873.73..873.73 rows=30473 width=77) (actual time=9.872..9.872 rows=30473 loops=1)
         Buckets: 4096  Batches: 1  Memory Usage: 3347kB
         Buffers: shared hit=569
         ->  Seq Scan on summary_foobar  (cost=0.00..873.73 rows=30473 width=77) (actual time=0.003..4.569 rows=30473 loops=1)
               Buffers: shared hit=569
 Total runtime: 286.910 ms [~550 ms if using foobar instead of subfoobar]
(11 lignes)

总而言之,对于我的实际案例查询,我从每个查询5000+毫秒下降到大约150毫秒(由于WHERE条款而少于示例。)

2 个答案:

答案 0 :(得分:2)

您可能需要增加work_mem。您的查询使用磁盘排序。它使用27MB - 尝试将work_mem设置为64MB左右,然后查看它是如何执行的。

您可以按会话或事务以及postgresql.conf设置它。

SET work_mem TO '64MB';

将为您当前的会话设置它。

显然,合理的值取决于您的计算机中有多少RAM以及您期望的并发连接数。

答案 1 :(得分:0)

更新2014-10-28:在这种情况下使用基于(数据)功能的indizes是不可能的,因为我要学习: - /(感谢 a_horse_with_no_name ):

  • 索引定义函数必须为IMMUTABLE

    • http://www.postgresql.org/docs/9.3/static/sql-createindex.html

        

      索引定义中使用的所有函数和运算符必须是" immutable",也就是说,它们的结果必须仅依赖于它们的参数,而不是任何外部影响(例如另一个表的内容或者当前时间)。此限制可确保明确定义索引的行为。要在索引表达式或WHERE子句中使用用户定义的函数,请记住在创建函数时将该函数标记为不可变。

    • 它给我造成了混淆,因为它的不同于( STABLE 索引扫描条件 ,正如{{3}所解释的那样}

所以我的初始方法应该只适用于手动构建和索引的表列count_bar_by_foo,该列由触发器更新(对于包含相同foo值的所有行) ...... 相当难看,如果没有真正有用/必要/可避免


原来的错误答案......

如果查询非常重要,您可以创建基于count(bar) over (partition by foo) 基于函数的索引,这可能在磁盘上非常小(在RAM中) )并快速搜索。

也许一个人必须把它用作自编写的sql函数,如count_by_foo(bar),以使其实际工作(我做过不检查create index条件的语法限制 - 这个想法很重要。)