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 recompile
aus 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 recompile
und:
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.