【问题标题】:PostgreSQL recursive CTE performance issuePostgreSQL 递归 CTE 性能问题
【发布时间】:2016-09-11 22:02:46
【问题描述】:

我试图了解两个查询在性能上的巨大差异。

假设我有两张桌子。 第一个包含一组域的 A 记录:

                                Table "public.dns_a"
 Column |          Type          | Modifiers | Storage  | Stats target | Description
--------+------------------------+-----------+----------+--------------+-------------
 name   | character varying(125) |           | extended |              |             
 a      | inet                   |           | main     |              |             
Indexes:
    "dns_a_a_idx" btree (a)
    "dns_a_name_idx" btree (name varchar_pattern_ops)  

第二张表处理 CNAME 记录:

                              Table "public.dns_cname"
 Column |          Type          | Modifiers | Storage  | Stats target | Description
--------+------------------------+-----------+----------+--------------+-------------
 name   | character varying(256) |           | extended |              |             
 cname  | character varying(256) |           | extended |              |             
Indexes:
    "dns_cname_cname_idx" btree (cname varchar_pattern_ops)
    "dns_cname_name_idx" btree (name varchar_pattern_ops)  

现在我正在尝试解决“简单”问题,让所有域都指向同一个 IP 地址,包括 CNAME。

第一次尝试使用 CTE 效果不错:

EXPLAIN ANALYZE WITH RECURSIVE names_traverse AS (
    (
        SELECT name::varchar(256), NULL::varchar(256) as cname, a FROM dns_a WHERE a = '118.145.5.20'
    )
    UNION ALL
        SELECT c.name, c.cname, NULL::inet as a FROM names_traverse nt, dns_cname c WHERE c.cname=nt.name
)
SELECT * FROM names_traverse; 

                                                                               QUERY PLAN

------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 CTE Scan on names_traverse  (cost=3051757.20..4337044.86 rows=64264383 width=1064) (actual time=0.037..1697.444 rows=199 loops=1)
   CTE names_traverse
     ->  Recursive Union  (cost=0.57..3051757.20 rows=64264383 width=45) (actual time=0.036..1697.395 rows=199 loops=1)
           ->  Index Scan using dns_a_a_idx on dns_a  (cost=0.57..1988.89 rows=1953 width=24) (actual time=0.035..0.064 rows=14 loops=1)
                 Index Cond: (a = '118.145.5.20'::inet)
           ->  Merge Join  (cost=4377.00..176448.06 rows=6426243 width=45) (actual time=498.101..848.648 rows=92 loops=2)
                 Merge Cond: ((c.cname)::text = (nt.name)::text)
                 ->  Index Scan using dns_cname_cname_idx on dns_cname c  (cost=0.56..69958.06 rows=2268434 width=45) (actual time=4.732..688.456 rows=2219973 loops=2)
                 ->  Materialize  (cost=4376.44..4474.09 rows=19530 width=516) (actual time=0.039..0.084 rows=187 loops=2)
                       ->  Sort  (cost=4376.44..4425.27 rows=19530 width=516) (actual time=0.037..0.053 rows=100 loops=2)
                             Sort Key: nt.name USING ~<~
                             Sort Method: quicksort  Memory: 33kB
                             ->  WorkTable Scan on names_traverse nt  (cost=0.00..390.60 rows=19530 width=516) (actual time=0.001..0.007 rows=100 loops=2)
 Planning time: 0.130 ms
 Execution time: 1697.477 ms
(15 rows)         

上面的例子中有两个循环,所以如果我做一个简单的外连接查询,我会得到更好的结果:

EXPLAIN ANALYZE 
SELECT * 
FROM dns_a a 
LEFT JOIN dns_cname c1 ON (c1.cname=a.name) 
LEFT JOIN dns_cname c2 ON (c2.cname=c1.name) 
WHERE a.a='118.145.5.20';

                                                                     QUERY PLAN

----------------------------------------------------------------------------------------------------------------------------------------------------
 Nested Loop Left Join  (cost=1.68..65674.19 rows=1953 width=114) (actual time=1.086..12.992 rows=189 loops=1)
   ->  Nested Loop Left Join  (cost=1.12..46889.57 rows=1953 width=69) (actual time=1.085..2.154 rows=189 loops=1)
         ->  Index Scan using dns_a_a_idx on dns_a a  (cost=0.57..1988.89 rows=1953 width=24) (actual time=0.022..0.055 rows=14 loops=1)
               Index Cond: (a = '118.145.5.20'::inet)
         ->  Index Scan using dns_cname_cname_idx on dns_cname c1  (cost=0.56..19.70 rows=329 width=45) (actual time=0.137..0.148 rows=13 loops=14)
               Index Cond: ((cname)::text = (a.name)::text)
   ->  Index Scan using dns_cname_cname_idx on dns_cname c2  (cost=0.56..6.33 rows=329 width=45) (actual time=0.057..0.057 rows=0 loops=189)
         Index Cond: ((cname)::text = (c1.name)::text)
 Planning time: 0.452 ms
 Execution time: 13.012 ms
(10 rows)

Time: 13.787 ms 

所以,性能差异大约是 100 倍,这让我担心。 我喜欢递归 CTE 的便利性,并且更喜欢使用它而不是在应用程序端做脏把戏,但我不明白为什么 Index Scan using dns_cname_cname_idx on dns_cname c (cost=0.56..69958.06 rows=2268434 width=45) (actual time=4.732..688.456 rows=2219973 loops=2) 的成本如此之高。

我是否遗漏了有关 CTE 的重要内容或问题出在其他方面?

谢谢!

更新:我的一个朋友发现了我错过的受影响行数Index Scan using dns_cname_cname_idx on dns_cname c (cost=0.56..69958.06 rows=2268434 width=45) (actual time=4.732..688.456 rows=2219973 loops=2),它等于表中的总行数,如果我理解正确,它会执行全索引扫描而不条件,我不知道错过了条件。

结果:应用SET LOCAL enable_mergejoin TO false;后,执行时间好很多。

EXPLAIN ANALYZE WITH RECURSIVE names_traverse AS (
    (
        SELECT name::varchar(256), NULL::varchar(256) as cname, a FROM dns_a WHERE a = '118.145.5.20'
    )
    UNION ALL
        SELECT c.name, c.cname, NULL::inet as a FROM names_traverse nt, dns_cname c WHERE c.cname=nt.name
)
SELECT * FROM names_traverse;
                                                                        QUERY PLAN                                                                        
----------------------------------------------------------------------------------------------------------------------------------------------------------
 CTE Scan on names_traverse  (cost=4746432.42..6527720.02 rows=89064380 width=1064) (actual time=0.718..45.656 rows=199 loops=1)
   CTE names_traverse
     ->  Recursive Union  (cost=0.57..4746432.42 rows=89064380 width=45) (actual time=0.717..45.597 rows=199 loops=1)
           ->  Index Scan using dns_a_a_idx on dns_a  (cost=0.57..74.82 rows=2700 width=24) (actual time=0.716..0.717 rows=14 loops=1)
                 Index Cond: (a = '118.145.5.20'::inet)
           ->  Nested Loop  (cost=0.56..296507.00 rows=8906168 width=45) (actual time=11.276..22.418 rows=92 loops=2)
                 ->  WorkTable Scan on names_traverse nt  (cost=0.00..540.00 rows=27000 width=516) (actual time=0.000..0.013 rows=100 loops=2)
                 ->  Index Scan using dns_cname_cname_idx on dns_cname c  (cost=0.56..7.66 rows=330 width=45) (actual time=0.125..0.225 rows=1 loops=199)
                       Index Cond: ((cname)::text = (nt.name)::text)
 Planning time: 0.253 ms
 Execution time: 45.697 ms
(11 rows)

【问题讨论】:

    标签: postgresql common-table-expression


    【解决方案1】:

    如您所述,由于索引扫描,第一个查询很慢。

    该计划必须扫描完整的索引才能获得按cname 排序的dns_cname,这是合并连接所必需的。合并连接要求两个输入表都按连接键排序,这可以通过对整个表进行索引扫描(如本例中)或通过顺序扫描然后显式排序来完成。

    您会注意到规划器严重高估了 CTE 评估的所有行数,这可能是问题的根源。对于较少的行,PostgreSQL 可能会选择一个嵌套循环连接,它不必扫描整个表 dns_cname。

    这可能是可修复的,也可能是不可修复的。我可以立即看到的一件事是初始值'118.145.5.20' 的估计值太高了 139.5 倍,这非常糟糕。您可以通过在dns_cname 上运行ANALYZE 来解决这个问题,也许在增加列的statistics target 之后:

    ALTER TABLE dns_a ALTER a SET STATISTICS 1000;
    

    看看这是否有影响。

    如果这不起作用,您可以手动将 enable_mergejoin 和 enable_hashjoin 设置为 off 并查看具有嵌套循环连接的计划是否真的更好。如果您可以只为这一条语句更改这些参数(可能使用SET LOCAL)并通过这种方式获得更好的结果,那是您的另一种选择。

    【讨论】:

    • 太棒了,谢谢你,劳伦兹。我通过索引对 dns_a 进行聚类,通过 cname 索引对 dns_cname 进行聚类,禁用 mergejoin 并在同一查询中获得 45 毫秒或使用热缓存获得 0.5 毫秒。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-04-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多