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

标签 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 中遇到了一个有趣的性能问题。

...老实说,我不太确定这是一个“错误”还是只是用户(即我)对 CTE/子查询和/或使用 generate_series 函数的理解。

我一直在使用 CTE 编写一些高级且较长的查询。我一直在使用一种技术,我将一个静态变量(例如日期)放入一个主 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左右。

在这一点上我一无所知,有人有任何理论吗? (...还是错误?)

最佳答案

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

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 来覆盖这个猜测。


顺便说一句:自 PG-11(左右)以来,限制(cte 总是执行一次)已被删除。如果没有副作用。

关于sql - 具有 generate_series 性能问题的 PostgreSQL CTE/子查询,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36460682/

相关文章:

java - 创建唯一的 listing_no (Java/PostgreSQL)

postgresql - 如何在PostgreSQL中通过引用表A为表B添加外键和通过引用表B为表A添加外键?

sql - CTE、ROW_NUMBER 和 ROWCOUNT

php - PHP页面中的SQL请求

database - PostgreSQL 表 ID 不会自动递增

mysql - 加入条件困难的地方

sql-server - 带订单树的递归 SQL 查询

postgresql - 如何在动态 sql 查询中使用 WITH block

php - mySQL 给出不正确的结果

sql - 按基于时间的距离进行行聚合