Postgres基于窗口的查询优化/规划不好(分区依据(,分组依据?))-1000x加速



我们正在运行Postgres 9.3.5。(2014年7月)我们有一些相当复杂的数据仓库/报告设置(ETL、物化视图、索引、聚合、分析函数等)

我现在发现的可能很难在优化器中实现(?),但它在性能上产生了巨大的差异(只有与我们的查询具有巨大相似性的示例代码才能降低不必要的复杂性):

create view foo as
select
  sum(s.plan) over w_pyl as pyl_plan,      -- money planned to spend in this pot/loc/year
  sum(s.booked) over w_pyl as pyl_booked,  -- money already booked in this pot/loc/year
  -- money already booked in this pot/loc the years before (stored as sum already)
  last_value(s.booked_prev_years) over w_pl as pl_booked_prev_years,    
  -- update 2014-10-08: maybe the following additional selected columns
  -- may be implementation-/test-relevant since they could potentially be determined
  -- by sorting within the partition:
  min(s.id) over w_pyl,
  max(s.id) over w_pyl,
  -- ... anything could follow here ...
  x.*,
  s.*
from
  pot_location_year x  -- may be some materialized view or (cache/regular) table
  left outer join  spendings s 
    on (s.pot = x.pot and s.loc = x.loc and s.year = x.year)
window
  w_pyl  as (partition by  x.pot, x.year, x.loc)
  w_pl   as (partition by  x.pot, x.loc  order by x.year)

我们有两个相关的索引:

pot_location_year_idx__p_y_l  -- on pot, year, loc
pot_location_year_idx__p_l_y  -- on pot, loc, year

现在我们为一些测试查询运行一个解释

explain select * from foo fetch first 100 rows only

这向我们展示了一些非常糟糕的性能,因为使用了pyl索引,其中结果集必须不必要地排序两次:-(最外层的WindowAgg/Sort步骤对ply进行排序,因为这对我们的last_value(..) as pl_booked_prev_years是必要的):

 Limit  (cost=289687.87..289692.12 rows=100 width=512)
   ->  WindowAgg  (cost=289687.87..292714.85 rows=93138 width=408)
         ->  Sort  (cost=289687.87..289920.71 rows=93138 width=408)
               Sort Key: x.pot, x.loc, x.year
               ->  WindowAgg  (cost=1.25..282000.68 rows=93138 width=408)
                     ->  Nested Loop Left Join  (cost=1.25..278508.01 rows=93138 width=408)
                           Join Filter: ...
                           ->  Nested Loop Left Join  (cost=0.83..214569.60 rows=93138 width=392)
                                 ->  Index Scan using pot_location_year_idx__p_y_l on pot_location_year x  (cost=0.42..11665.49 rows=93138 width=306)
                                 ->  Index Scan using ...  (cost=0.41..2.17 rows=1 width=140)
                                       Index Cond: ...
                           ->  Index Scan using ...  (cost=0.41..0.67 rows=1 width=126)
                                 Index Cond: ...

因此,显而易见的问题是,计划器应该选择现有的ply索引,而不必排序两次

幸运的是,我发现我可以通过确保其他视图分区/windows的列顺序更同质(尽管在语义上没有必要)来给规划者一个(隐含的)提示。

下面的更改现在返回了我最初期望得到的内容(ply索引的使用):

...
window
  -- w_pyl  as (partition by  x.pot, x.year, x.loc)  -- showstopper (from above)
     w_pyl  as (partition by  x.pot, x.loc, x.year)  -- speedy
     w_pl   as (partition by  x.pot, x.loc  order by x.year)

性能提高1000倍的结果:

 Limit  (cost=1.25..308.02 rows=100 width=512)
   ->  WindowAgg  (cost=1.25..284794.82 rows=93138 width=408)
         ->  WindowAgg  (cost=1.25..282000.68 rows=93138 width=408)
               ->  Nested Loop Left Join  (cost=1.25..278508.01 rows=93138 width=408)
                     Join Filter: ...
                     ->  Nested Loop Left Join  (cost=0.83..214569.60 rows=93138 width=392)
                           ->  Index Scan using pot_location_year_idx__p_l_y on pot_location_year x  (cost=0.42..11665.49 rows=93138 width=306)
                           ->  Index Scan using ...  (cost=0.41..2.17 rows=1 width=140)
                                 Index Cond: ...
                     ->  Index Scan using ...  (cost=0.41..0.67 rows=1 width=126)
                           Index Cond: ...

更新2014-10-09:

Tom Lane-2写了这篇文章(主要的postgres开发人员之一),与我在2013-02年面临的另一个(可能相关的)窗口函数问题有关,与第9.2.2页:

没有那么多的情报在关于窗口函数的系统中,。所以你必须写如果您希望进行此优化。

因此,关于窗口功能、数据仓库功能等主题的一些(有争议的)一般性想法可以在这里考虑:

上面的陈述很好地强化了我的假设,当我决定在一般项目和DWH环境中进行一些Oracle->Postgres迁移时,花更多时间和金钱这样做的风险会很高。(尽管所研究的功能似乎已经足够了。)

与Oracle相比,我更喜欢重要领域的Postgres,比如代码的语法和清晰度以及其他方面(我想即使是源代码以及可维护性(在各个方面)也要好得多),但Oracle显然在资源优化、支持和工具领域是更先进的参与者,当您在处理典型CRUD管理之外更复杂的数据库功能时。

我想,从长远来看,开源Postgres(以及EnterpriseDB充值)将在这些领域迎头赶上,但这至少需要10年的时间,而且可能只有在谷歌等大型无私的全球玩家大力推动的情况下。)

1从某种意义上说,如果推送区域保持"免费",这些公司的利益肯定在其他地方(也许随机添加了一些广告行-我想我们可以在这里和那里接受它;)


更新2014-10-13:

正如我在上面的更新(2014-10-09)中所链接的,当你想用约束/过滤器查询上面的视图时(在pot_id上),优化问题及其解决方案以非常相似的方式进行(在上面的修复之后):

explain select * foo where pot_id = '12345' fetch first 100 rows only

 Limit  (cost=1.25..121151.44 rows=100 width=211)
   ->  Subquery Scan on foo  (cost=1.25..279858.20 rows=231 width=211)
         Filter: ((foo.pot_id)::text = '12345'::text)
         ->  WindowAgg  (cost=1.25..277320.53 rows=203013 width=107)
               ->  WindowAgg  (cost=1.25..271230.14 rows=203013 width=107)
                     ->  Nested Loop Left Join  (cost=1.25..263617.16 rows=203013 width=107)
                           ->  Merge Left Join  (cost=0.83..35629.02 rows=203013 width=91)
                                 Merge Cond: ...
                                 ->  Index Scan using pot_location_year_idx__p_l_y on pot_location_year x  (cost=0.42..15493.80 rows=93138 width=65)
                                 ->  Materialize  (cost=0.41..15459.42 rows=33198 width=46)
                                       ->  Index Scan using ...  (cost=0.41..15376.43 rows=33198 width=46)
                           ->  Index Scan using ...  (cost=0.42..1.11 rows=1 width=46)
                                 Index Cond: ...

正如上面链接中所建议的,如果你想在窗口聚合之前"按下"约束/过滤器,你必须在视图本身中明确地执行它,这对于这种类型的查询将是有效的,然后第100行的速度将提高1000倍:

 create view foo as
 ...
 where pot_id='12345'
 ...

 Limit  (cost=1.25..943.47 rows=100 width=211)
   ->  WindowAgg  (cost=1.25..9780.52 rows=1039 width=107)
         ->  WindowAgg  (cost=1.25..9751.95 rows=1039 width=107)
               ->  Nested Loop Left Join  (cost=1.25..9715.58 rows=1039 width=107)
                     ->  Nested Loop Left Join  (cost=0.83..1129.47 rows=1039 width=91)
                           ->  Index Scan using pot_location_year_idx__p_l_y on pot_location_year x (cost=0.42..269.77 rows=106 width=65)
                                 Index Cond: ((pot_id)::text = '12345'::text)
                           ->  Index Scan using ...  (cost=0.41..8.10 rows=1 width=46)
                                 Index Cond: ...
                     ->  Index Scan using ...  (cost=0.42..8.25 rows=1 width=46)
                           Index Cond: ...

经过更多的视图参数化工作2,这种方法将有助于加速约束这些列的某些查询,但在更通用的foo视图使用和查询优化方面仍然相当不灵活。

2:您可以将这样的视图"参数化",将其(其SQL)放入(集返回)表函数(Oracle相当于流水线表函数)中。关于这方面的更多细节可以在上面的论坛链接中找到。