【问题标题】:Improving PARTITION BY performance?通过性能提高分区?
【发布时间】:2014-11-25 10:17:14
【问题描述】:

考虑下表:

 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);

新表只有 70 万行而不是 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;

实际上,正如 cmets 中的 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 子句,少于示例)。

【问题讨论】:

  • 很高兴您设法找到优化事物的方法。如果您使用 >=9.3,您可能会发现为汇总表使用物化视图很有用。使用CREATE MATERIALIZED summary_foobar AS SELECT DISTINCT ON (bar) count(foo) OVER (PARTITION BY bar) AS cfoo, bar FROM foobar; 之类的内容创建。你可以用同样的方式查询,但是用REFRESH MATERIALIZED VIEW summary_foobar;刷新内容很容易,而不是删除和重新创建/截断和重新插入。
  • 抱歉,我在创建命令中错过了“VIEW” - s/b CREATE MATERIALIZED VIEW summary_foobar AS SELECT DISTINCT ON (bar) count(foo) OVER (PARTITION BY bar) AS cfoo, bar FROM foobar;
  • 很好的提示,谢谢!我会用它来更新答案。

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


【解决方案1】:

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

您可以在每个会话或事务以及 postgresql.conf 中设置它。

SET work_mem TO '64MB';

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

显然,一个合理的值取决于您的机器中有多少 RAM 以及您希望拥有的并发连接数。

【讨论】:

  • 好建议,我没有注意到EXPLAIN ANALYZE 提到它在磁盘上工作。无论如何,实际上我在提交问题后已经将work_mem 设置为 1 GB(顺便说一句,获取当前work_mem 的命令:show work_mem)。但是我的磁盘是 SSD,所以在 RAM 上工作只会减少 1.5 的处理时间。
  • 几乎快两倍?现在有时间安排的计划是怎样的?
  • 能否在分析输出中也包含缓冲区?
  • foo 是 text / varchar 列吗?如果是,它的排序规则是什么(或者数据库排序规则,如果它是默认的)?
  • 虽然表有文本列(排序规则fr_FR.UTF-8),但在本题测试的查询中foo 是实数,bar 是整数。
【解决方案2】:

2014 年 10 月 28 日更新:在这种情况下无法使用基于(数据)函数的索引,因为我要去学习 :-/(感谢 a_horse_with_no_name) :

  • 索引定义函数必须是IMMUTABLE

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

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

    • 它让我感到困惑,因为它不同于 (STABLE) 索引扫描条件,正如 here 所解释的那样

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


原来的错误答案...

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

也许必须将其放入并用作 self-written-sql-function like count_by_foo(bar) 才能使其真正工作(我没有检查create index 条件的语法限制 - 这个想法很重要)。

【讨论】:

  • 不能基于窗口函数创建索引。
  • 非常感谢您的提示。更新了我的答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-18
  • 1970-01-01
  • 1970-01-01
  • 2021-09-22
相关资源
最近更新 更多