2016-09-11 11 views
2

私は2つのクエリのこのような大きな違いを理解しようとしています。PostgreSQLの再帰CTEパフォーマンスの問題

2つのテーブルがあるとします。 第1は、ドメインの一部セットのレコードが含まれています

       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) 

2つ目のテーブルには、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) 

答えて

2

注意したように、インデックススキャンのために最初のクエリが遅いです。

dns_cnameをマージ結合に必要なcnameでソートするには、完全なインデックスをスキャンする必要があります。マージ結合では、両方の入力テーブルを結合キーでソートする必要があります。これは、完全なテーブル(この場合)の索引スキャンで実行するか、順次スキャンとそれに続く明示的なソートです。

プランナーがCTE評価の行数を大幅に過大評価していることがわかります。これはおそらく問題の根本です。行数が少ない場合、PostgreSQLはテーブル全体をスキャンする必要のない入れ子ループ結合を選択することがありますdns_cname

修正可能かどうか。私がすぐに見ることができることの1つは、初期値'118.145.5.20'の推定値が139.5で非常に高く、かなり悪いことです。あなたは、おそらく列にstatistics targetを増加させた後、dns_cnameANALYZEを実行していることを解決するかもしれません:それは違いを作る場合

ALTER TABLE dns_a ALTER a SET STATISTICS 1000; 

を参照してください。

これで解決できない場合は、手動でenable_mergejoinenable_hashjoinoffに設定して、ネストループ結合の計画が本当に良いかどうかを確認できます。この1つのステートメント(おそらくSET LOCAL)のこれらのパラメータを変更するだけで、より良い結果を得ることができれば、それは別のオプションです。

+0

それは素晴らしいです、ありがとう、ローレンズ。 インデックスでdns_aをクラスタリングし、cnameインデックスでdns_cnameを無効にし、mergejoinを無効にして、同じクエリで45ミリ秒、またはウォームキャッシュで0.5ミリ秒を得ました。 – icuken