2016-09-11 6 views
2

Ich versuche, einen so großen Unterschied in der Leistung von zwei Abfragen zu verstehen.PostgreSQL rekursive CTE-Performance-Problem

Nehmen wir an, ich habe zwei Tabellen. erste enthält A-Datensätze für einige Reihe von Domains:

       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) 

Zweite Tabelle Griffe CNAME-Datensätze:

       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) 

Now „einfache“ Problem zu lösen, mit immer alle Domains Ich versuche zeigen die gleiche IP-Adresse, einschließlich CNAME.

Der erste Versuch zu verwenden CTE Art funktioniert gut:

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)   

Es gibt zwei Schleifen im Beispiel oben, also wenn ich eine einfache äußere Join-Abfrage machen, bekomme ich viel bessere Ergebnisse:

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 

Also, der Leistungsunterschied ist etwa 100 mal und das ist die Sache, die mir Sorgen macht. Ich mag die Bequemlichkeit der rekursiven CTE und bevorzuge es, anstatt schmutzige Tricks auf der Anwendungsseite zu tun, aber ich verstehe nicht, warum die Kosten von 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) so hoch ist.

Fehle ich etwas wichtig in Bezug auf CTE oder das Problem ist mit etwas anderem?

Danke!

Update: Ein Freund von mir die Anzahl der betroffenen Zeilen entdeckte ich 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) verpasst, es Gesamtanzahl der Zeilen in der Tabelle entspricht und, wenn ich das richtig verstehe, führt es vollständigen Index Scan ohne Bedingung und ich nicht dorthin gelangen, wo der Zustand fehlt.

Ergebnis: Nach der Anwendung SET LOCAL enable_mergejoin TO false; Ausführungszeit ist viel, viel besser.

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) 

Antwort

2

Die erste Abfrage ist langsam wegen der Indexsuche, wie Sie festgestellt haben.

Der Plan muss den kompletten Index scannen, um dns_cname sortiert nach zu erhalten, was für den Merge-Join benötigt wird. Ein Merge-Join erfordert, dass beide Eingabetabellen nach dem Join-Schlüssel sortiert werden, was entweder mit einem Index-Scan über die gesamte Tabelle (wie in diesem Fall) oder durch einen sequenziellen Scan gefolgt von einer expliziten Sortierung erfolgen kann.

Sie werden feststellen, dass der Planer alle Zeilenzählungen für die CTE-Auswertung stark überschätzt, was wahrscheinlich die Wurzel des Problems ist. Für weniger Zeilen wählt PostgreSQL möglicherweise eine Nested-Loop-Verknüpfung, bei der nicht die gesamte Tabelle dns_cname gescannt werden muss.

Das kann reparierbar sein oder nicht. Eine Sache, die ich sofort sehen kann, ist, dass die Schätzung für den Anfangswert '118.145.5.20' um einen Faktor 139,5 zu hoch ist, was ziemlich schlecht ist.Vielleicht haben Sie das Problem beheben, indem ANALYZE auf dns_cname läuft, vielleicht nach der statistics target für die Spalte zu erhöhen:

ALTER TABLE dns_a ALTER a SET STATISTICS 1000; 

Prüfen Sie, ob das einen Unterschied macht.

Wenn das nicht den Trick macht, können Sie manuell enable_mergejoin und enable_hashjoin auf off setzen und sehen, ob ein Plan mit einem Nested Loop Join wirklich besser ist oder nicht. Wenn Sie diese Parameter nur für diese eine Anweisung ändern können (wahrscheinlich mit SET LOCAL) und ein besseres Ergebnis erhalten, dann ist dies eine weitere Option, die Sie haben.

+0

Das ist erstaunlich, danke, Laurenz. Ich gruppierte dns_a durch einen Index, dns_cname nach cname index, deaktivierte mergejoin und bekam 45 ms bei derselben Abfrage oder 0,5 ms bei warmem Cache. – icuken