web-dev-qa-db-ja.com

実行プランは、非動的クエリよりもストアドプロシージャの方が「優れています」か?

Microsoft SQL Serverによる実行プランのキャッシュについてのさまざまな説明を読んでいると、非動的クエリの代わりにストアドプロシージャを使用する利点に戸惑っています。

非動的クエリとは、複数の呼び出しで変更されない完全にパラメーター化されたクエリ文字列を意味します。

私が理解しているように:

  1. 実行プランは、ストアドプロシージャと通常のクエリの両方に対してキャッシュされます。

  2. ストアドプロシージャの場合、実行プランは事前に計算されるため、最初にストアドプロシージャが呼び出されたときに、通常のクエリよりもわずかに有利です。

ソースは私にはかなり矛盾しています:

  • MSDNの実行プランのキャッシュと再利用に関する記事 は、パラメーター化されたクエリとストアドプロシージャを区別しません。サブセクションでは、SQL Serverが実行プランをキャッシュしやすくするために、パラメーター化されたクエリの重要性を強調しています。

  • SQL Serverクエリ実行プラン–基本 反対の主張(私の強調):

    アドホッククエリの実行に関しては、完全なコードに基づいてクエリプランが作成されるため、異なるパラメーターまたはコードの変更既存のプランの再利用を防止します

  • DBA.StackExchangeでは、ストアドプロシージャの利点に関する回答の コメント は、パラメータ化されたクエリがストアドプロシージャとまったく同じ効果を持つことを示しています。

したがって、実行計画が キャッシュから破棄 ではない状況で、実験のために、実行計画の恩恵を受けるかなり複雑なクエリを数十億回実行したい場合毎回変化する1つのパラメーターを使用する場合、実行プランのキャッシュに関して、通常のパラメーター化されたクエリの代わりにストアドプロシージャを使用するメリットはありますか?


the実行計画の範囲外では、たとえばネットワークフットプリントの観点から、ストアドプロシージャを使用することによるパフォーマンス上の小さなメリットがあります。ストアドプロシージャとそのパラメータの名前を渡す方が、クエリ全体を渡すよりもわずかに優れています。これらの利点は私の質問の範囲外です。これは純粋に実行プランのキャッシュに関するものです。

7

回答は、スタンドアロン ブログ記事 としても入手できます。

それを見つけるために、いくつかのテストを行いました。目的は、同じパラメーター化されたクエリをC#から直接実行するか、ストアドプロシージャを呼び出して実行し、ランタイムパフォーマンスを比較することです。

Adventure Worksデータベースを使用してサンプルクエリを実行するストアドプロシージャの作成を開始しました。

_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
_

次に、次のコードを使用してパフォーマンスを比較します。

_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));
}
_

option (recompile)および_with recompile_に注意してください。これにより、SQL Serverは以前にキャッシュされた実行プランを強制的に破棄します。

各クエリは、毎回異なるパラメーターを使用して100回実行されます。サーバーが費やした時間は、クライアント側で測定されます。

メトリックを収集する前に_DBCC FreeProcCache; DBCC DropCleanbuffers;_を実行することで、以前にキャッシュされた実行プランがすべて削除されることを確認します。

このコードを実行すると、次の出力が得られます。

ストアドプロシージャ:786 ms。詳細: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。
クエリ:799ミリ秒。詳細: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。

もう一度実行してみましょう。

ストアドプロシージャ:763 ms。詳細: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 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 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。
クエリ:752 ms。詳細:25、10、8、8、12、8、7、9、9、8、6、7、7、6、8、6、7、7、8、9、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、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。

パフォーマンスは、ストアドプロシージャと直接クエリの間で非常に近いようです。コードを十数回実行すると、ストアドプロシージャは少し高速に見えますが、ギャップは非常に狭いことがわかります。クエリ全体を渡すとこの追加のコストが発生する可能性があり、SQL Serverがアプリケーションサーバーとの間に低速のLANを備えた専用マシンでホストされている場合は、コストが増加する可能性があります。

次に、実行プランのキャッシュをオンにして、何が起こるかを見てみましょう。これを行うには、コードからoption (recompile)および_with recompile_を削除します。新しい出力は次のとおりです。

ストアドプロシージャ:26 ms。詳細: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、1、1、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、 0。
クエリ:15ミリ秒。詳細: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、1、0、0、0、0、0、0、0、0、0、 0。

キャッシュは、直接クエリとストアドプロシージャの両方でまったく同じ効果があることが明らかになります。どちらの場合も、時間はほぼ0ミリ秒に短縮され、最も負荷の高いクエリは最初のクエリ、つまりキャッシュされた実行プランの削除後に実行されるクエリです。

同じコードを再度実行すると、同様のパターンが表示されます。クエリが高速になる場合もあれば、ストアドプロシージャが高速になる場合もあります。しかし、毎回、最初のクエリが最も負荷が高く、他のすべてのクエリは0ミリ秒に近いです。

SQL接続を再開する

このわずかに変更されたコードのように、すべてのクエリに対してSQL接続が開かれている場合:

_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));
}
_

観測されたメトリックは非常に似ています:

ストアドプロシージャ:748 ms。詳細: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。
クエリ:761 ms。詳細: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、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、6、6、7、6、8、7、7、7、7、7、7、7、8、7、8、7、9、7、6、6、12、10、7、 6。

option (recompile)および_with recompile_および:

ストアドプロシージャ:15 ms。詳細: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、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。
クエリ:32 ms。詳細: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、1、0、0、0、0、0、0、1、0、1、0、0、0、0、0、0、0、0、 0。

なし。

フードの下

フードの下で何が起こるか見てみましょう。次のクエリは、キャッシュされた実行プランを示しています。

_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
_

ストアドプロシージャを100回実行した後でこのクエリを実行すると、クエリの結果は次のようになります。

_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)
_

クエリを直接100回実行すると、結果は次のようになります。

_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)
_

結論

  • 実行プランは、ストアドプロシージャと直接クエリ用にキャッシュされます。

  • SQL Serverとアプリケーションが同じマシンでホストされている場合、ストアドプロシージャと直接クエリ間のパフォーマンスは非常に似ています。 SQL ServerがLAN経由でアクセスされる専用サーバーでホストされている場合、ストアドプロシージャを使用するとパフォーマンスが向上する場合があります。

4

プランのキャッシュに関するストアドプロシージャがパラメーター化されたクエリである限り、それらは同じクエリプランを使用します。実行段階への到達方法の違いのために、どちらか一方をもう一方に使用すると、わずかなオーバーヘッドが発生する可能性がありますが、大きな違いに気づいたことはありません。

ストアドプロシージャの利点は、セキュリティ、保守性、および展開に関係しています。もちろん、パラメータ化されたクエリと旧式の動的SQLの本質的な保護は別として。

0
Duffy