具有generate_series性能问题的PostgreSQL CTE /子查询

时间:2016-04-06 19:31:30

标签: sql postgresql common-table-expression postgresql-9.3 postgresql-9.4

=====根据反馈更新=====

由于一些初步问题需要澄清,这是一个非常简单的版本。

WITH my_var AS ( 
    SELECT date '2016-01-01' as a_date          
    --, generate_series(1, 40) as numbers       
    )
Select generate_series(1, 100000) as numbers, my_var.a_date from my_var 

execution time: 411ms

"CTE Scan on my_var  (cost=0.01..5.03 rows=1000 width=4)"
"  CTE my_var"
"    ->  Result  (cost=0.00..0.01 rows=1 width=0)"

现在,如果我们取消注释

中的generate_series
WITH my_var AS ( 
    SELECT date '2016-01-01' as a_date          
    , generate_series(1, 40) as numbers     
)
Select generate_series(1, 100000) as numbers, my_var.a_date from my_var 

execution time: 16201ms

"CTE Scan on my_var  (cost=5.01..5022.51 rows=1000000 width=4)"
"  CTE my_var"
"    ->  Result  (cost=0.00..5.01 rows=1000 width=0)"

这里的观点是,如果generate_series(1,40)只应执行一次,为什么查询完成需要这么长时间。在这种情况下,我甚至没有使用'数字'在主查询中设置,它仍然需要和延长的时间来完成。

=====原始查询=====

我使用子查询和/或CTE在PostgreSQL 9.x中遇到了一个有趣的性能问题。

......说实话,我不太确定这是否是一个" bug"或者只是用户(即Me)对CTE /子查询和/或使用generate_series函数的理解。

我一直在使用CTE编写一些高级和更长的查询。我一直在使用一种技术,我将一个静态变量(如日期)放入主CTE中,过滤掉所有其他查询。当您需要使用不同的参数运行它时,我们的想法是通过长查询进行一组更改而不是大量更改。

这方面的一个例子是:

WITH dates AS ( 
   SELECT 
      date '2013-01-01' AS start_date, 
      date_trunc('month', current_date) AS end_date
)
SELECT * from dates, sometable where somedate between start_date and end_date

execution time:  ~650ms

所以,我的理解是CTE运行一次,但在遇到性能问题之后,这显然不是正在发生的事情。例如,如果我修改CTE以包含generate_series:

WITH dates AS ( 
   SELECT 
      date '2013-01-01' AS start_date, 
      date_trunc('month', current_date) AS end_date,
      generate_series(1, 10) AS somelist
)
SELECT * from dates, sometable where somedate between start_date and end_date 
   and myval in (somelist)

execution time:  ~23000ms

由于一些严重的性能问题(慢了几千倍),我起初认为generate_series()正在为somelist分配" generate_series"函数,然后作为主查询中某些表中每一行的子查询执行。所以为了确认这一点我修改了查询如下:

WITH dates AS ( 
   SELECT 
      date '2013-01-01' AS start_date, 
      date_trunc('month', current_date) AS end_date--,
      --generate_series(1, 10) AS somelist
)
SELECT * from dates, sometable where somedate between start_date and end_date 
   and myval in (generate_series(1, 10))


execution time:  ~700ms

令我惊讶的是,这个速度相对较快(仅慢了10%)。 generate_series和子查询显然不是问题。

然后转到原始查询并添加了generate_series但从未在主查询中使用过它。这是查询。

WITH dates AS ( 
   SELECT 
      date '2013-01-01' AS start_date, 
      date_trunc('month', current_date) AS end_date,
      generate_series(1, 10) AS somelist
)
SELECT * from dates, sometable where somedate between start_date and end_date

execution time:  ~23000ms

这显然是吸烟枪...但我不知道为什么或者那件事真的发生了什么。以下是我的问题:

总之,在CTE或子查询中使用generate_series会消耗大量的时间/资源(即使结果未被使用)。我在Postgres v9.3和v9.5中得到了相同的结果。我跑来跑去的桌子有大约1400万行。结果集仅约275K。

此时我一无所知,有没有人有任何理论? (......或者它是一个错误?)

1 个答案:

答案 0 :(得分:0)

实验(我遗漏了日期,因为它们只是额外的标量常量)

EXPLAIN
WITH my_cte_b AS (
        SELECT generate_series(1, 40) as b_number
        )
, my_cte_c AS (
        SELECT generate_series(1, 1000) AS c_number
        )
Select
        my_cte_b.b_number
        , my_cte_c.c_number
FROM my_cte_b
JOIN my_cte_c ON (1=1)
        ;

结果:

                                 QUERY PLAN                            
------------------------------------------------------------------
 Nested Loop  (cost=5.01..10020.01 rows=1000000 width=8)
   CTE my_cte_b
     ->  Result  (cost=0.00..2.50 rows=1000 width=0)
   CTE my_cte_c
     ->  Result  (cost=0.00..2.50 rows=1000 width=0)
   ->  CTE Scan on my_cte_b  (cost=0.00..10.00 rows=1000 width=4)
   ->  CTE Scan on my_cte_c  (cost=0.00..10.00 rows=1000 width=4)
(7 rows)

但是EXPLAIN ANALYZE给出了正确的结果:

-----------------------------
 Nested Loop  (cost=5.01..10020.01 rows=1000000 width=8) (actual time=0.029..8.953 rows=40000 loops=1)
   CTE my_cte_b
     ->  Result  (cost=0.00..2.50 rows=1000 width=0) (actual time=0.013..0.019 rows=40 loops=1)
   CTE my_cte_c
     ->  Result  (cost=0.00..2.50 rows=1000 width=0) (actual time=0.002..0.095 rows=1000 loops=1)
   ->  CTE Scan on my_cte_b  (cost=0.00..10.00 rows=1000 width=4) (actual time=0.021..0.040 rows=40 loops=1)
   ->  CTE Scan on my_cte_c  (cost=0.00..10.00 rows=1000 width=4) (actual time=0.000..0.104 rows=1000 loops=40)
 Planning time: 0.042 ms
 Execution time: 25.206 ms
(9 rows)

,所以问题似乎在估计中,而不是在执行中。

作为奖励:你可以通过在CTE中放置LIMIT xx提示(或者傻瓜)规划者:

EXPLAIN ANALYZE
WITH my_cte_b AS (
        SELECT generate_series(1, 40) as b_number
        LIMIT 40
        )
, my_cte_c AS (
        SELECT generate_series(1, 1000) AS c_number
        LIMIT 10000
        )
Select
        my_cte_b.b_number
        , my_cte_c.c_number
FROM my_cte_b
JOIN my_cte_c ON (1=1)
        ;

                                                   QUERY PLAN                                                   
----------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=2.60..408.00 rows=40000 width=8) (actual time=0.019..9.347 rows=40000 loops=1)
   CTE my_cte_b
     ->  Limit  (cost=0.00..0.10 rows=40 width=0) (actual time=0.008..0.018 rows=40 loops=1)
           ->  Result  (cost=0.00..2.50 rows=1000 width=0) (actual time=0.006..0.013 rows=40 loops=1)
   CTE my_cte_c
     ->  Limit  (cost=0.00..2.50 rows=1000 width=0) (actual time=0.002..0.241 rows=1000 loops=1)
           ->  Result  (cost=0.00..2.50 rows=1000 width=0) (actual time=0.002..0.134 rows=1000 loops=1)
   ->  CTE Scan on my_cte_b  (cost=0.00..0.40 rows=40 width=4) (actual time=0.012..0.036 rows=40 loops=1)
   ->  CTE Scan on my_cte_c  (cost=0.00..10.00 rows=1000 width=4) (actual time=0.000..0.112 rows=1000 loops=40)
 Planning time: 0.096 ms
 Execution time: 10.693 ms
(11 rows)

我的结论:计划者没有关于CTE的统计数据(他们不包含对物理表的任何引用),只是猜测(1000)。通过在CTE中放置一个LIMIT可以推翻这个猜测。