Verlangsamung der SQL-Abfrage von 1 Sekunde auf 11 Minuten - warum?

7

Frage: Ich portiere die folgende Abfrage (Auflisten von Tabellen nach Fremdschlüsselabhängigkeiten) nach PostGreSql.

WITH Fkeys AS (

    SELECT DISTINCT 
         OnTable       = OnTable.name
        ,AgainstTable  = AgainstTable.name 
    FROM sysforeignkeys fk 

        INNER JOIN sysobjects onTable 
            ON fk.fkeyid = onTable.id 

        INNER JOIN sysobjects againstTable  
            ON fk.rkeyid = againstTable.id 

    WHERE 1=1
        AND AgainstTable.TYPE = 'U'
        AND OnTable.TYPE = 'U'
        -- ignore self joins; they cause an infinite recursion
        AND OnTable.Name <> AgainstTable.Name
    )

,MyData AS (

    SELECT 
         OnTable = o.name 
        ,AgainstTable = FKeys.againstTable 
    FROM sys.objects o 

    LEFT JOIN FKeys
        ON o.name = FKeys.onTable 

    WHERE (1=1) 
        AND o.type = 'U' 
        AND o.name NOT LIKE 'sys%' 
    )

,MyRecursion AS (

    -- base case
    SELECT  
         TableName    = OnTable
        ,Lvl        = 1
    FROM MyData
    WHERE 1=1
        AND AgainstTable IS NULL 

    -- recursive case
    UNION ALL 

    SELECT 
         TableName = OnTable 
        ,Lvl       = r.Lvl + 1 
    FROM MyData d 
        INNER JOIN MyRecursion r 
            ON d.AgainstTable = r.TableName 
)
SELECT 
     Lvl = MAX(Lvl)
    ,TableName
    --,strSql = 'delete from [' + tablename + ']'
FROM 
    MyRecursion
GROUP BY
    TableName

ORDER BY lvl

/*
ORDER BY 

     2 ASC
    ,1 ASC

*/

Mit information_schema sieht die Abfrage folgendermaßen aus:

WITH Fkeys AS 
(
    SELECT DISTINCT 
         KCU1.TABLE_NAME AS OnTable 
        ,KCU2.TABLE_NAME AS AgainstTable 
    FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC 

    LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU1 
        ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
        AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
        AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 

    LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU2 
        ON KCU2.CONSTRAINT_CATALOG =  RC.UNIQUE_CONSTRAINT_CATALOG  
        AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA 
        AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME 
        AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION 

    WHERE (1=1)
    AND KCU1.TABLE_NAME <> KCU2.TABLE_NAME 
)

,MyData AS 
( 
    SELECT 
         TABLE_NAME AS OnTable  
        ,FKeys.againstTable AS AgainstTable
    FROM INFORMATION_SCHEMA.TABLES 

    LEFT JOIN FKeys
        ON TABLE_NAME = FKeys.onTable  

    WHERE (1=1) 
        AND TABLE_TYPE = 'BASE TABLE'
        AND TABLE_NAME NOT IN ('sysdiagrams', 'dtproperties') 
)

,MyRecursion AS 
(
    -- base case
    SELECT  
         OnTable AS TableName 
        ,1 AS Lvl 
    FROM MyData
    WHERE 1=1
    AND AgainstTable IS NULL 

    -- recursive case
    UNION ALL 

    SELECT 
         OnTable AS TableName
        ,r.Lvl + 1 AS Lvl 
    FROM MyData d 

    INNER JOIN MyRecursion r 
        ON d.AgainstTable = r.TableName 
)

SELECT 
     MAX(Lvl) AS Lvl 
    ,TableName
    --,strSql = 'delete from [' + tablename + ']'
FROM 
    MyRecursion
GROUP BY
    TableName

ORDER BY lvl

/*
ORDER BY 

     2 ASC
    ,1 ASC

*/

Meine Frage ist jetzt:

In SQL Server (getestet auf 2008 R2): Warum springt die Abfrage beim Ersetzen von 1 Sekunde auf 11 Minuten?

SELECT DISTINCT 
     OnTable       = OnTable.name
    ,AgainstTable  = AgainstTable.name 
FROM sysforeignkeys fk 

    INNER JOIN sysobjects onTable 
        ON fk.fkeyid = onTable.id 

    INNER JOIN sysobjects againstTable  
        ON fk.rkeyid = againstTable.id 

WHERE 1=1
    AND AgainstTable.TYPE = 'U'
    AND OnTable.TYPE = 'U'
    -- ignore self joins; they cause an infinite recursion
    AND OnTable.Name <> AgainstTable.Name

mit

SELECT DISTINCT 
     KCU1.TABLE_NAME AS OnTable 
    ,KCU2.TABLE_NAME AS AgainstTable 
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC 

LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU1 
    ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
    AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
    AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 

LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU2 
    ON KCU2.CONSTRAINT_CATALOG =  RC.UNIQUE_CONSTRAINT_CATALOG  
    AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA 
    AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME 
    AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION 

WHERE (1=1)
AND KCU1.TABLE_NAME <> KCU2.TABLE_NAME 

???

Soweit ich das beurteilen kann, gibt es wirklich keinen signifikanten Geschwindigkeitsunterschied, wenn nur die Teilabfragen separat ausgeführt werden. Auch die Ergebnismenge ist völlig gleich (ich habe jede Zeile in Excel überprüft), obwohl die Reihenfolge unterschiedlich ist.

Unterhalb der funktionierenden PostGreSQL-Version (fertig in 35 ms bei genau demselben Datenbankinhalt [75 Tabellen] ...)
- Keine Garantie -

WITH RECURSIVE Fkeys AS 
(
    SELECT DISTINCT 
         KCU1.TABLE_NAME AS OnTable 
        ,KCU2.TABLE_NAME AS AgainstTable 
    FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC 

    LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU1 
        ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
        AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
        AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 

    LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU2 
        ON KCU2.CONSTRAINT_CATALOG =  RC.UNIQUE_CONSTRAINT_CATALOG  
        AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA 
        AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME 
        AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION 
)

,MyData AS 
( 
    SELECT 
         TABLE_NAME AS OnTable  
        ,FKeys.againstTable AS AgainstTable
    FROM INFORMATION_SCHEMA.TABLES 

    LEFT JOIN FKeys
        ON TABLE_NAME = FKeys.onTable  

    WHERE (1=1) 
        AND TABLE_TYPE = 'BASE TABLE'
        AND TABLE_SCHEMA = 'public'
        --AND TABLE_NAME NOT IN ('sysdiagrams', 'dtproperties') 
)


,MyRecursion AS 
(
    -- base case
    SELECT  
         OnTable AS TableName 
        ,1 AS Lvl 
    FROM MyData
    WHERE 1=1
    AND AgainstTable IS NULL 

    -- recursive case
    UNION ALL 

    SELECT 
         OnTable AS TableName
        ,r.Lvl + 1 AS Lvl 
    FROM MyData d 

    INNER JOIN MyRecursion r 
        ON d.AgainstTable = r.TableName 
)

SELECT 
     MAX(Lvl) AS Lvl 
    ,TableName
    --,strSql = 'delete from [' + tablename + ']'
FROM 
    MyRecursion
GROUP BY
    TableName

ORDER BY lvl


/*
ORDER BY 

     2 ASC
    ,1 ASC

*/

Es scheint auch so

AND KCU1.TABLE_NAME <> KCU2.TABLE_NAME

ist überflüssig, wenn information_schema verwendet wird, daher sollte es eigentlich schneller sein.

Dilemma
quelle
7
Ich habe den Ausführungsplan für Ihre Hauptabfrage überprüft und mich gefickt, es ist ein Chaos. oi41.tinypic.com/2zz0q41.jpg Ich denke, das ist dein Problem.
ta.speot.is
@ ta.speot.is: Stimmt, aber ich hatte gehofft, den Grund für das Durcheinander zu kennen. Da PG in 35 ms fertig ist -> Fehlerbericht bei MS einreichen.
Quandary
1
@BrandonMoore - Hiermit können Sie programmatisch Klauseln hinzufügen, ohne überprüfen zu müssen, ob Sie auch eine hinzufügen sollten where.
Lieven Keersmaekers
1
@Brandon Moore: Wie Lieven sagt (nur solange jede zusätzliche Bedingung vorliegt und wenn es so wäre oder wäre, müsste es 1 = 2 sein), aber hauptsächlich für mich, um schnell eine Bedingung ohne auskommentieren zu können Generieren eines Syntaxfehlers zu Testzwecken.
Dilemma
2
Wenn Sie sich nur den Ausführungsplan für die Teilabfragen ansehen, sieht es so aus, als ob das Verbinden auf dem idFeld (nicht überraschend) effizienter ist als das Verbinden auf den Zeichenfolgen NAME, SCHEMAusw. Übrigens sollten Sie das sys.objectsusw. verwenden, nicht das veraltetesysobjects
Martin Smith

Antworten:

12

Ich würde wahrscheinlich die INFORMATION_SCHEMAAnsichten hier aufgeben und sys.stattdessen die neuen Ansichten (im Gegensatz zu den abwärtskompatiblen) verwenden oder zumindest die Ergebnisse der zuerst JOINin einer indizierten Tabelle materialisieren .

Rekursive CTEs erhalten in SQL Server immer den gleichen Grundplan, bei dem jede Zeile einer Stapelspool hinzugefügt und einzeln verarbeitet wird. Dies bedeutet, dass die Verknüpfung zwischen REFERENTIAL_CONSTRAINTS RC, KEY_COLUMN_USAGE KCU1, KEY_COLUMN_USAGE KCU2so oft wie das Ergebnis der folgenden Abfrage erfolgt SELECT COUNT(*) FROM MyRecursion.

Was ich in Ihrem Fall (ab der Ausführungszeit von 11 Minuten) annehme, ist wahrscheinlich viele tausend Mal so, dass Sie den rekursiven Teil benötigen, um so effizient wie möglich zu sein. Ihre Abfrage wird die folgende Art von Dingen tausende Male ausführen.

   SELECT  
           KCU1.TABLE_CATALOG,
           KCU1.TABLE_SCHEMA,
           KCU1.TABLE_NAME
    FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC 
    INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU1 
        ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
        AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
        AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 
    INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU2 
        ON KCU2.CONSTRAINT_CATALOG =  RC.UNIQUE_CONSTRAINT_CATALOG  
        AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA 
        AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME 
        AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION 
    WHERE KCU2.TABLE_NAME = 'FOO' 

(Randnotiz: Beide Versionen Ihrer Abfrage geben falsche Ergebnisse zurück, wenn derselbe Tabellenname in verschiedenen Schemas verwendet wird.)

Wie Sie sehen können, ist der Plan dafür ziemlich schrecklich.

Planen

Vergleichen Sie dies mit dem Plan für Ihre sysAbfrage, der etwas einfacher ist.

SELECT OnTable = OnTable.name, 
       AgainstTable = AgainstTable.name 
FROM   sysforeignkeys fk 
       INNER JOIN sysobjects OnTable 
         ON fk.fkeyid = OnTable.id 
       INNER JOIN sysobjects AgainstTable 
         ON fk.rkeyid = AgainstTable.id 
WHERE  AgainstTable.name = 'FOO' 

Plan 2

Möglicherweise können Sie die Zwischenmaterialisierung fördern, ohne #tempexplizit eine Tabelle zu erstellen, indem Sie die Definition von MyDatain ändern

MyData AS 
( 
    SELECT TOP 99.999999 PERCENT
         TABLE_NAME AS OnTable  
        ,Fkeys.AgainstTable AS AgainstTable
    FROM INFORMATION_SCHEMA.TABLES 

    LEFT JOIN Fkeys
        ON TABLE_NAME = Fkeys.OnTable  

    WHERE (1=1) 
        AND TABLE_TYPE = 'BASE TABLE'
        AND TABLE_NAME NOT IN ('sysdiagrams', 'dtproperties') 
        ORDER BY TABLE_NAME
)

Beim Testen Adventureworks2008auf meinem Computer wurde die Laufzeit von ca. 10 Sekunden auf 250 ms gesenkt (nachdem der erste Lauf aus dem Weg war, da das Kompilieren des Plans 2 Sekunden dauerte). Es fügt dem Plan eine eifrige Spool hinzu, die das Ergebnis des Joins beim ersten rekursiven Aufruf materialisiert und es dann bei nachfolgenden Aufrufen wiedergibt. Dieses Verhalten kann jedoch nicht garantiert werden, und Sie möchten möglicherweise die Anforderung für Connect-Elemente verbessern. Geben Sie einen Hinweis an, um die Zwischenmaterialisierung von CTEs oder abgeleiteten Tabellen zu erzwingen

Ich würde mich sicherer fühlen, wenn ich die #tempTabelle explizit wie folgt erstelle, als mich auf dieses Verhalten zu verlassen.

CREATE TABLE #MyData
(
OnTable SYSNAME,
AgainstTable NVARCHAR(128) NULL,
UNIQUE CLUSTERED (AgainstTable, OnTable)
);

WITH Fkeys AS 
(
    SELECT DISTINCT 
         KCU1.TABLE_NAME AS OnTable 
        ,KCU2.TABLE_NAME AS AgainstTable 
    FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS RC 

    LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU1 
        ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
        AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
        AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME 

    LEFT JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE KCU2 
        ON KCU2.CONSTRAINT_CATALOG =  RC.UNIQUE_CONSTRAINT_CATALOG  
        AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA 
        AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME 
        AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION 

    WHERE (1=1)
    AND KCU1.TABLE_NAME <> KCU2.TABLE_NAME 
)

,MyData AS 
( 
    SELECT 
         TABLE_NAME AS OnTable  
        ,Fkeys.AgainstTable AS AgainstTable
    FROM INFORMATION_SCHEMA.TABLES 

    LEFT JOIN Fkeys
        ON TABLE_NAME = Fkeys.OnTable  

    WHERE (1=1) 
        AND TABLE_TYPE = 'BASE TABLE'
        AND TABLE_NAME NOT IN ('sysdiagrams', 'dtproperties') 
)
INSERT INTO #MyData
SELECT *
FROM MyData;


WITH MyRecursion AS 
(
    -- base case
    SELECT  
         OnTable AS TableName 
        ,1 AS Lvl 
    FROM #MyData
    WHERE 1=1
    AND AgainstTable IS NULL 

    -- recursive case
    UNION ALL 

    SELECT 
         OnTable AS TableName
        ,r.Lvl + 1 AS Lvl 
    FROM #MyData d 

    INNER JOIN MyRecursion r 
        ON d.AgainstTable = r.TableName 
)

SELECT 
     MAX(Lvl) AS Lvl 
    ,TableName
    --,strSql = 'delete from [' + tablename + ']'
FROM 
    MyRecursion
GROUP BY
    TableName

ORDER BY Lvl

DROP TABLE #MyData

oder alternativ

Martin Smith
quelle
Gute Antwort. Und danke für die Randnotiz, das ist ein schöner Fang. Ich denke, ich fühle mich vorerst sicherer, die ältere Version für SQL Server beizubehalten, als eine persistente / tem-Tabelle zu erstellen. Ich beabsichtige, sysobjects durch sys.objects zu ersetzen. Hoffentlich keine schlechten Überraschungen. Ein CTE-Hinweis wäre in der Tat gut, da der Optimierer im Gegensatz zu seinem pg-Gegenstück nicht intelligent genug ist, um dies automatisch zu erkennen. Übrigens wusste ich bis jetzt nicht, dass man den Ausführungsplan als Image exportieren kann. Eines der 4 Dinge, die ich aus diesem Beitrag gelernt habe - danke.
Quandary
2

In beiden Fällen fragen Sie Ansichten ab, die für die Kompatibilität übrig bleiben: Kompatibilitätsansichten und Informationsschemaansichten .

Verwenden Sie stattdessen Katalogansichten , um die beste Leistung zu erzielen (msdn: "Wir empfehlen, Katalogansichten zu verwenden, da diese die allgemeinste Schnittstelle zu den Katalogmetadaten darstellen und die effizienteste Möglichkeit bieten, benutzerdefinierte Formen dieser Informationen abzurufen, zu transformieren und darzustellen.") ..

Jānis
quelle
2
Informationsschema wird nicht aus Kompatibilitätsgründen belassen, es ist Teil des SQL-Standards (92+) ...
Quandary