Ist der zwischengespeicherte Ausführungsplan für gespeicherte Prozeduren "besser" als für eine nicht dynamische Abfrage?

7

Beim Lesen verschiedener Erklärungen zum Zwischenspeichern von Ausführungsplänen durch Microsoft SQL Server bin ich verwirrt über die Vorteile der Verwendung gespeicherter Prozeduren anstelle nicht dynamischer Abfragen.

Mit einer nicht dynamischen Abfrage meine ich eine vollständig parametrisierte Abfragezeichenfolge, die sich nicht durch mehrere Aufrufe ändert.

So wie ich es verstehe:

  1. Der Ausführungsplan wird sowohl für eine gespeicherte Prozedur als auch für eine normale Abfrage zwischengespeichert.

  2. Für eine gespeicherte Prozedur wird der Ausführungsplan vorberechnet, was beim ersten Aufruf der gespeicherten Prozedur zu einem geringfügigen Vorteil gegenüber normalen Abfragen führt.

Die Quellen sehen für mich eher widersprüchlich aus:

  • Der Artikel zum Zwischenspeichern und Wiederverwenden von Ausführungsplänen in MSDN unterscheidet nicht zwischen parametrisierten Abfragen und gespeicherten Prozeduren. In den Unterabschnitten wird die Bedeutung parametrisierter Abfragen hervorgehoben, damit SQL Server den Ausführungsplan problemlos zwischenspeichern kann.

  • Ausführungspläne für SQL Server-Abfragen - Basics behauptet das Gegenteil (Schwerpunkt Mine):

    Bei der Ausführung von Ad-hoc-Abfragen werden Abfragepläne auf der Grundlage des vollständigen Codes erstellt, sodass unterschiedliche Parameter oder Codeänderungen die Wiederverwendung des vorhandenen Plans verhindern .

  • In DBA.StackExchange gibt der Kommentar zu einer Antwort zu den Vorteilen gespeicherter Prozeduren an, dass parametrisierte Abfragen genau den gleichen Effekt haben wie gespeicherte Prozeduren.

In dem Kontext, in dem der Ausführungsplan nicht aus dem Cache geworfen wird und in dem ich aus Versuchsgründen milliardenfach eine ziemlich komplizierte Abfrage ausführen möchte, die von einem Ausführungsplan profitieren würde und einen Parameter verwendet, der sich ändert Gibt es jedes Mal einen Vorteil in Bezug auf das Caching von Ausführungsplänen¹, wenn gespeicherte Prozeduren anstelle einer normalen parametrisierten Abfrage verwendet werden?


¹ Außerhalb des Geltungsbereichs des Ausführungsplans ergeben sich geringfügige Leistungsvorteile bei der Verwendung einer gespeicherten Prozedur, z. B. in Bezug auf den Netzwerk-Footprint: Die Übergabe des Namens der gespeicherten Prozedur und ihrer Parameter ist geringfügig besser als die Übergabe der gesamten Abfrage. Diese Vorteile liegen außerhalb des Rahmens meiner Frage, bei der es ausschließlich um den Cache für Ausführungspläne geht.

Arseni Mourzenko
quelle
2
Habe es noch nie getestet gesehen, kann also nicht kommentieren, wie gering oder nicht die Ausführungszeit sein würde. Das heißt, ich denke, Ihre theoretische Einschränkung muss überdacht werden ... wie klein sind diese Bytes? 100 Byte Paketgrößenunterschied xa Milliarden Anrufe sind mehr als nur die gesamte Netzwerklatenz und die Pakete ... dieselben Bytes müssen vom Parser gehasht werden, um festzustellen, ob bereits ein Plan vorhanden ist. Ebenso ist es selten , die ich je mit nur einer Abfrage eine gespeicherte Prozedur sehen ... die schweren Heben und Geschäftslogik Sie können einbetten Faktoren zwingende als auch auf , warum , wenn es wichtig genug für eine Abfrage ist, Sie sho
Andrew Loree

Antworten:

4

Die Antwort ist auch als eigenständiger Blog-Artikel verfügbar .

Um es herauszufinden, habe ich einige Tests durchgeführt. Ziel ist es, dieselbe parametrisierte Abfrage entweder direkt von C # oder durch Aufrufen einer gespeicherten Prozedur auszuführen und die Laufzeitleistung zu vergleichen.

Ich habe begonnen, eine gespeicherte Prozedur zu erstellen, die eine Beispielabfrage mithilfe der Adventure Works-Datenbank ausführt:

create procedure Demo
    @minPrice int 
as
begin
    set nocount on;

    select top 1 [p].[Name], [p].[ProductNumber], [ph].[ListPrice]
    from [Production].[Product] p
    inner join [Production].[ProductListPriceHistory] ph
    on [p].[ProductID] = ph.[ProductID]
    and ph.[StartDate] =
    (
        select top 1 [ph2].[StartDate]
        from [Production].[ProductListPriceHistory] ph2
        where [ph2].[ProductID] = [p].[ProductID]
        order by [ph2].[StartDate] desc
    )
    where [p].[ListPrice] > @minPrice
end

Dann verwende ich den folgenden Code, um die Leistungen zu vergleichen:

long RunQuery(SqlConnection connection, int minPrice)
{
    const string Query = @"
    select top 1 [p].[Name], [p].[ProductNumber], [ph].[ListPrice]
    from [Production].[Product] p
    inner join [Production].[ProductListPriceHistory] ph
    on [p].[ProductID] = ph.[ProductID]
    and ph.[StartDate] =
    (
        select top 1 [ph2].[StartDate]
        from [Production].[ProductListPriceHistory] ph2
        where [ph2].[ProductID] = [p].[ProductID]
        order by [ph2].[StartDate] desc
    )
    where [p].[ListPrice] > @minPrice
    option (recompile)";

    using (var command = new SqlCommand(Query, connection))
    {
        command.Parameters.AddWithValue("@minPrice", minPrice);
        var stopwatch = Stopwatch.StartNew();
        command.ExecuteNonQuery();
        stopwatch.Stop();
        return stopwatch.ElapsedMilliseconds;
    }
}

long RunStoredProcedure(SqlConnection connection, int minPrice)
{
    using (var command = new SqlCommand("exec Demo @minPrice with recompile", connection))
    {
        command.Parameters.AddWithValue("@minPrice", minPrice);
        var stopwatch = Stopwatch.StartNew();
        command.ExecuteNonQuery();
        stopwatch.Stop();
        return stopwatch.ElapsedMilliseconds;
    }
}

ICollection<long> Execute(Func<SqlConnection, int, long> action)
{
    using (var connection = new SqlConnection("Server=.;Database=AdventureWorks2014;Trusted_Connection=True;"))
    {
        connection.Open();
        using (var command = new SqlCommand("DBCC FreeProcCache; DBCC DropCleanbuffers;", connection))
        {
            command.ExecuteNonQuery();
        }

        return Enumerable.Range(0, 100).Select(i => action(connection, i)).ToList();
    }
}

void Main()
{
    var queries = Execute(RunQuery);
    var storedProcedures = Execute(RunStoredProcedure);

    Console.WriteLine("Stored procedures: {0} ms. Details: {1}.", storedProcedures.Sum(), string.Join(", ", storedProcedures));
    Console.WriteLine("Queries: {0} ms. Details: {1}.", queries.Sum(), string.Join(", ", queries));
}

Hinweis option (recompile)und with recompile. Dadurch wird SQL Server gezwungen, zuvor zwischengespeicherte Ausführungspläne zu verwerfen.

Jede Abfrage wird jedes Mal hundertmal mit einem anderen Parameter ausgeführt. Die vom Server verbrachte Zeit wird auf der Clientseite gemessen.

Durch Ausführen DBCC FreeProcCache; DBCC DropCleanbuffers;vor dem Sammeln von Metriken stelle ich sicher, dass alle zuvor zwischengespeicherten Ausführungspläne entfernt werden.

Das Ausführen dieses Codes ergibt die folgende Ausgabe:

Gespeicherte Prozeduren: 786 ms. Details: 12, 7, 7, 9, 7, 7, 9, 8, 8, 6, 8, 9, 8, 8, 14, 8, 7, 8, 7, 10, 10, 7, 9, 6, 9, 8, 8, 7, 7, 10, 8, 7, 7, 6, 7, 8, 8, 7, 7, 7, 14, 8, 8, 8, 7, 9, 8, 8, 7, 6, 6, 12, 7, 7, 8, 7, 8, 7, 8, 6, 7, 7, 7, 12, 8, 6, 6, 7, 8, 7, 8, 8, 7, 11, 8, 7, 8, 8, 7, 9, 8, 9, 10, 8, 7, 7, 8, 8, 7, 9, 7, 6, 9, 7, 6, 9, 8, 6, 6, 6, 6.
Abfragen: 799 ms. Details: 21, 8, 8, 7, 6, 6, 11, 7, 6, 6, 9, 8, 8, 7, 9, 8, 7, 7, 7, 7, 7, 7, 10, 8, 8, 7, 8, 7, 6, 11, 19, 10, 8, 7, 8, 7, 7, 7, 6, 9, 7, 9, 7, 7, 8, 7, 12, 9, 7, 7, 7, 8, 7, 7, 8, 7, 7, 7, 9, 8, 7, 7, 7, 6, 7, 7, 16, 7, 7, 7, 8, 8, 9, 8, 7, 9, 8, 7, 8, 7, 7, 6, 7, 7, 7, 7, 12, 7, 9, 9, 7, 7, 7, 7, 9, 8, 7, 8, 11, 8.

Lassen Sie es uns noch einmal ausführen:

Gespeicherte Prozeduren: 763 ms. Details: 11, 8, 10, 8, 8, 14, 10, 6, 7, 7, 6, 7, 7, 9, 6, 6, 6, 8, 6, 6, 7, 6, 8, 7, 16, 8, 7, 8, 9, 7, 7, 8, 7, 7, 11, 10, 7, 6, 7, 8, 7, 7, 7, 7, 7, 7, 10, 9, 9, 9, 7, 6, 7, 6, 7, 7, 6, 6, 6, 6, 6, 10, 9, 10, 7, 6, 6, 6, 6, 6, 8, 7, 6, 6, 7, 7, 8, 9, 7, 8, 7, 10, 7, 7, 7, 6, 7, 6, 7, 11, 13, 8, 7, 10, 9, 8, 8, 7, 8, 7, 7, 7, 7.
Abfragen: 752 ms. Details: 25, 10, 8, 8, 12, 8, 7, 9, 9, 8, 6, 7, 7, 6, 8, 6, 7, 7, 8, 9, 7, 7, 7, 7, 7, 7 6, 10, 8, 7, 7, 7, 7, 7, 7, 7, 8, 9, 7, 6, 6, 6, 7, 13, 7, 7, 7, 7, 7, 7, 7, 7, 7 7, 7, 7, 6, 10, 7, 7, 8, 9, 8, 7, 6, 6, 7, 7, 9, 7, 8, 6, 9, 7, 7, 8, 7, 6, 6, 7, 7, 7, 7, 6, 7, 7, 8, 7, 7, 6, 7, 9, 8, 7, 7, 7, 7, 6, 7, 6, 6, 9, 7, 7.

Es scheint, dass die Leistung zwischen gespeicherten Prozeduren und direkten Abfragen sehr eng ist. Wenn ich den Code ein Dutzend Mal ausführe, stelle ich fest, dass gespeicherte Prozeduren etwas schnell zu sein scheinen, aber die Lücke ist sehr eng. Durch die Weitergabe der gesamten Abfrage entstehen möglicherweise zusätzliche Kosten, die sich erhöhen können, wenn SQL Server auf einem dedizierten Computer mit einem langsamen LAN zwischen ihm und dem Anwendungsserver gehostet wird.

Lassen Sie uns nun das Caching des Ausführungsplans aktivieren und sehen, was passiert. Dazu entferne ich option (recompile)und with recompileaus dem Code. Hier ist die neue Ausgabe:

Gespeicherte Prozeduren: 26 ms. Details: 23, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.
Abfragen: 15 ms. Details: 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.

Es wird deutlich, dass das Caching sowohl für direkte Abfragen als auch für gespeicherte Prozeduren genau den gleichen Effekt hat. In beiden Fällen wird die Zeit auf nahezu null Millisekunden reduziert, und die teuerste Abfrage ist die erste, die nach dem Entfernen zwischengespeicherter Ausführungspläne ausgeführt wird.

Wenn Sie denselben Code erneut ausführen, wird ein ähnliches Muster angezeigt. Manchmal sind Abfragen schneller und manchmal sind gespeicherte Prozeduren schneller. Aber jedes Mal ist die erste Abfrage die teuerste, und alle anderen sind nahe null Millisekunden.

SQL-Verbindung erneut öffnen

Wenn die SQL-Verbindung für jede Abfrage geöffnet wird, z. B. in diesem leicht geänderten Code:

long RunQuery(string connectionString, int minPrice)
{
    const string Query = @"
    select top 1 [p].[Name], [p].[ProductNumber], [ph].[ListPrice]
    from [Production].[Product] p
    inner join [Production].[ProductListPriceHistory] ph
    on [p].[ProductID] = ph.[ProductID]
    and ph.[StartDate] =
    (
        select top 1 [ph2].[StartDate]
        from [Production].[ProductListPriceHistory] ph2
        where [ph2].[ProductID] = [p].[ProductID]
        order by [ph2].[StartDate] desc
    )
    where [p].[ListPrice] > @minPrice
    option (recompile)";

    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var command = new SqlCommand(Query, connection))
        {
            command.Parameters.AddWithValue("@minPrice", minPrice);
            var stopwatch = Stopwatch.StartNew();
            command.ExecuteNonQuery();
            stopwatch.Stop();
            return stopwatch.ElapsedMilliseconds;
        }
    }
}

long RunStoredProcedure(string connectionString, int minPrice)
{
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var command = new SqlCommand("exec Demo @minPrice with recompile", connection))
        {
            command.Parameters.AddWithValue("@minPrice", minPrice);
            var stopwatch = Stopwatch.StartNew();
            command.ExecuteNonQuery();
            stopwatch.Stop();
            return stopwatch.ElapsedMilliseconds;
        }
    }
}

ICollection<long> Execute(Func<string, int, long> action)
{
    var connectionString = "Server=.;Database=AdventureWorks2014;Trusted_Connection=True;";
    using (var connection = new SqlConnection(connectionString))
    {
        connection.Open();
        using (var command = new SqlCommand("DBCC FreeProcCache; DBCC DropCleanbuffers;", connection))
        {
            command.ExecuteNonQuery();
        }
    }

    return Enumerable.Range(0, 100).Select(i => action(connectionString, i)).ToList();
}

void Main()
{
    var queries = Execute(RunQuery);
    var storedProcedures = Execute(RunStoredProcedure);

    Console.WriteLine("Stored procedures: {0} ms. Details: {1}.", storedProcedures.Sum(), string.Join(", ", storedProcedures));
    Console.WriteLine("Queries: {0} ms. Details: {1}.", queries.Sum(), string.Join(", ", queries));
}

Die beobachteten Metriken sind sehr ähnlich:

Gespeicherte Prozeduren: 748 ms. Details: 11, 8, 6, 6, 8, 9, 9, 8, 8, 7, 6, 8, 7, 9, 6, 6, 6, 6, 6, 6, 7, 7, 6, 9, 6, 6, 7, 6, 6, 7, 8, 6, 7, 7, 7, 13, 7, 7, 8, 7, 8, 8, 7, 7, 7, 7, 6, 7, 8, 8, 8, 9, 7, 6, 8, 7, 6, 7, 6, 6, 6, 6, 8, 12, 7, 9, 9, 6, 7, 7, 7, 8, 10, 12, 8, 7, 6, 9, 8, 7, 6, 6, 7, 8, 6, 6, 12, 7, 8, 10, 10, 7, 8, 7, 8, 10, 8, 7, 8, 7.
Abfragen: 761 ms. Details: 31, 9, 7, 6, 6, 8, 7, 7, 7, 7, 7, 6, 8, 7, 6, 6, 7, 10, 8, 10, 9, 7, 7, 7, 7, 7, 10, 13, 7, 10, 7, 6, 6, 6, 8, 7, 7, 7, 7, 7, 7, 7, 9, 7, 7, 7, 6, 6, 6, 9, 7, 7, 7, 7, 7, 6, 8, 10, 7, 7, 7, 7, 7, 7, 7, 8, 6, 10, 10, 7, 8, 8, 7, 7, 7, 7, 7, 7, 6, 6, 7, 6, 8, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 7, 9, 7, 6, 6, 12, 10, 7, 6.

mit option (recompile)und with recompileund:

Gespeicherte Prozeduren: 15 ms. Details: 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.
Abfragen: 32 ms. Details: 26, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0.

ohne.

Unter der Haube

Mal sehen, was unter der Haube passiert. Die folgende Abfrage zeigt zwischengespeicherte Ausführungspläne:

select usecounts, size_in_bytes, cacheobjtype, objtype, text 
from sys.dm_exec_cached_plans 
cross apply sys.dm_exec_sql_text(plan_handle)
where cacheobjtype = 'Compiled Plan'
order by usecounts desc

Wenn Sie diese Abfrage nach hundertmaliger Ausführung der gespeicherten Prozeduren ausführen, sieht das Ergebnis der Abfrage folgendermaßen aus:

usecounts   size_in_bytes cacheobjtype                                       objtype              text
----------- ------------- -------------------------------------------------- -------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
100         90112         Compiled Plan                                      Proc                 create procedure Demo
    @minPrice int 
as
begin
    set nocount on;

    select top 1 [p].[Name], [p].[ProductNumber], [ph].[ListPrice]
    from [Production].[Product] p
    inner join [Production].[ProductListPriceHistory] ph
    on [p].[ProductID] = ph.[Product
100         16384         Compiled Plan                                      Prepared             (@minPrice int)exec Demo @minPrice --with recompile
1           49152         Compiled Plan                                      Adhoc                --DBCC FreeProcCache
--DBCC DropCleanbuffers

select usecounts, size_in_bytes, cacheobjtype, objtype, text 
from sys.dm_exec_cached_plans 
cross apply sys.dm_exec_sql_text(plan_handle)
where cacheobjtype = 'Compiled Plan'
order by usecounts desc

(3 row(s) affected)

Wenn die Abfrage hundertmal direkt ausgeführt wird, lautet das Ergebnis:

usecounts   size_in_bytes cacheobjtype                                       objtype              text
----------- ------------- -------------------------------------------------- -------------------- ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
100         73728         Compiled Plan                                      Prepared             (@minPrice int)
    select top 1 [p].[Name], [p].[ProductNumber], [ph].[ListPrice]
    from [Production].[Product] p
    inner join [Production].[ProductListPriceHistory] ph
    on [p].[ProductID] = ph.[ProductID]
    and ph.[StartDate] =
    (
        select top 1 [ph2].[
1           49152         Compiled Plan                                      Adhoc                --DBCC FreeProcCache
--DBCC DropCleanbuffers

select usecounts, size_in_bytes, cacheobjtype, objtype, text 
from sys.dm_exec_cached_plans 
cross apply sys.dm_exec_sql_text(plan_handle)
where cacheobjtype = 'Compiled Plan'
order by usecounts desc

(2 row(s) affected)

Fazit

  • Der Ausführungsplan wird für gespeicherte Prozeduren und direkte Abfragen zwischengespeichert.

  • Die Leistung zwischen gespeicherten Prozeduren und direkten Abfragen ist sehr ähnlich, wenn der SQL Server und die Anwendung auf demselben Computer gehostet werden. Wenn SQL Server auf einem dedizierten Server gehostet wird, auf den über LAN zugegriffen wird, kann die Verwendung gespeicherter Prozeduren zu einer besseren Leistung führen.

Arseni Mourzenko
quelle
0

Beim Plan-Caching handelt es sich bei einer gespeicherten Prozedur um eine parametrisierte Abfrage. Sie verwenden dieselben Abfragepläne. Es gibt möglicherweise einen geringen Aufwand bei der Verwendung übereinander, nur weil sie sich in der Ausführungsphase unterscheiden, aber ich habe nie einen signifikanten Unterschied bemerkt.

Die Vorteile gespeicherter Prozeduren hängen mit Sicherheit, Wartbarkeit und Bereitstellung zusammen. Abgesehen von dem inhärenten Schutz parametrisierter Abfragen gegenüber dynamischem SQL der alten Schule.

Duffy
quelle