sql >> データベース >  >> RDS >> Database

フィルター処理されたインデックスと強制パラメーター化(redux)

    フィルター処理されたインデックスがどのように強力になるか、そして最近では強制的なパラメーター化によってそれらを役に立たなくする方法についてブログを書いた後、フィルター処理されたインデックス/パラメーター化のトピックを再検討しています。最近、一見単純すぎるソリューションが登場し、共有する必要がありました。

    次の例を見てください。ここでは、注文のテーブルを含む販売データベースがあります。場合によっては、まだ出荷されていない注文のみのリスト(またはカウント)が必要な場合があります。これは、時間の経過とともに(願わくば!)、テーブル全体のますます小さな割合を表します。

    CREATE DATABASE Sales;
    GO
    USE Sales;
    GO
     
    -- simplified, obviously:
    CREATE TABLE dbo.Orders
    (
        OrderID   int IDENTITY(1,1) PRIMARY KEY,
        OrderDate datetime  NOT NULL,
        filler    char(500) NOT NULL DEFAULT '',
        IsShipped bit       NOT NULL DEFAULT 0
    );
    GO
     
    -- let's put some data in there; 7,000 shipped orders, and 50 unshipped:
     
    INSERT dbo.Orders(OrderDate, IsShipped)
      -- random dates over two years
      SELECT TOP (7000) DATEADD(DAY, ABS(object_id % 730), '20171101'), 1 
      FROM sys.all_columns
    UNION ALL 
      -- random dates from this month
      SELECT TOP (50)   DATEADD(DAY, ABS(object_id % 30),  '20191201'), 0 
      FROM sys.all_columns;

    このシナリオでは、次のようなフィルター処理されたインデックスを作成することが理にかなっている場合があります(これにより、未出荷の注文を取得しようとしているクエリをすばやく処理できます):

    CREATE INDEX ix_OrdersNotShipped 
      ON dbo.Orders(IsShipped, OrderDate) 
      WHERE IsShipped = 0;

    このようなクイッククエリを実行して、フィルタリングされたインデックスがどのように使用されるかを確認できます。

    SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

    実行プランはかなり単純ですが、UnmatchedIndexesに関する警告があります:

    警告の名前は少し誤解を招く可能性があります。オプティマイザーは最終的にインデックスを使用できましたが、ステートメントがパラメーター化されているように見えても、パラメーターなしで「より良い」ことを示唆しています。

    本当に必要な場合は、実際のパフォーマンスに違いはなく、警告を削除できます(これは単なる表面的なものです)。 1つの方法は、AND (1 > 0)のような影響のない述語を追加することです。 :

    SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

    もう1つ(おそらくもっと一般的)は、OPTION (RECOMPILE)を追加することです。 :

    SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

    これらのオプションは両方とも同じ計画を生成します(警告なしのシーク):

    ここまでは順調ですね;フィルタリングされたインデックスが使用されています(予想どおり)。もちろん、これらだけがトリックではありません。読者がすでに提出している他の人については、以下のコメントを参照してください。

    次に、合併症

    データベースは多数のアドホッククエリの対象となるため、誰かが強制的なパラメータ化をオンにして、コンパイルを減らし、計画キャッシュを汚染することから低使用および使い捨ての計画を排除しようとします。

    ALTER DATABASE Sales SET PARAMETERIZATION FORCED;

    これで、元のクエリはフィルター処理されたインデックスを使用できなくなります。クラスタ化されたインデックスをスキャンすることを余儀なくされています:

    SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;

    一致しないインデックスに関する警告が返され、残りのI/Oに関する新しい警告が表示されます。ステートメントはパラメーター化されていますが、少し異なって見えることに注意してください:

    強制的なパラメーター化の全体的な目的は、このようなクエリをパラメーター化することであるため、これは仕様によるものです。ただし、変更可能なパラメーターではなく、述語内の単一の値をサポートすることを目的としているため、フィルター処理されたインデックスの目的は無効になります。

    おとなしい

    追加の述語を使用する「トリック」クエリも、フィルタリングされたインデックスを使用できず、起動するための計画が少し複雑になります。

    SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);

    オプション(再コンパイル)

    この場合の一般的な反応は、前に警告を削除した場合と同様に、OPTION (RECOMPILE)を追加することです。 ステートメントに。これは機能し、効率的なシークのためにフィルタリングされたインデックスを選択できるようにします…

    SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);

    …ただし、OPTION (RECOMPILE)を追加します そして、クエリのすべての実行に対してこの追加のコンパイルヒットを取得することは、大量の環境で常に受け入れられるとは限りません(特に、すでにCPUにバインドされている場合)。

    ヒント

    再コンパイルのコストを回避するために、フィルタリングされたインデックスを明示的にヒントすることを提案した人がいます。一般に、これはコードよりも優れたインデックスに依存しているため、かなり脆弱です。私はこれを最後の手段として使う傾向があります。この場合、とにかく有効ではありません。パラメータ化ルールにより、オプティマイザがフィルタリングされたインデックスを自動的に選択できない場合、手動で選択することもできなくなります。一般的なFORCESEEKと同じ問題 ヒント:

    SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0;
     
    SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;

    どちらもこのエラーを引き起こします:

    メッセージ8622、レベル16、状態1
    このクエリで定義されたヒントが原因で、クエリプロセッサはクエリプランを生成できませんでした。ヒントを指定せず、SET FORCEPLANを使用せずに、クエリを再送信します。

    IsShippedの不明な値を知る方法がないため、これは理にかなっています。 パラメータは、フィルタリングされたインデックスと一致します(または任意のインデックスでシーク操作をサポートします)。

    動的SQL?

    動的SQLを使用して、少なくとも、小さいインデックスをヒットすることがわかっている場合にのみ、その再コンパイルヒットを支払うことをお勧めします。

    DECLARE @IsShipped bit = 0;
     
    DECLARE @sql nvarchar(max) = N'SELECT dynsql = OrderID, OrderDate FROM dbo.Orders'
      + CASE WHEN @IsShipped IS NOT NULL THEN N' WHERE IsShipped = @IsShipped'
        ELSE N'' END
      + CASE WHEN @IsShipped = 0 THEN N' OPTION (RECOMPILE)' ELSE N'' END;
     
    EXEC sys.sp_executesql @sql, N'@IsShipped bit', @IsShipped;

    これは、上記と同じ効率的な計画につながります。変数を@IsShipped = 1に変更した場合 、次に、予想されるより高価なクラスター化インデックススキャンを取得します。

    しかし、このようなエッジケースで動的SQLを使用することを好む人は誰もいません。コードの読み取りと保守が難しくなり、このコードがアプリケーションに含まれていたとしても、そこに追加する必要のあるロジックが追加されるため、望ましいとは言えません。 。

    もっと簡単なもの

    プランガイドの実装について簡単に説明しましたが、これは確かに単純ではありませんが、同僚は、ストアドプロシージャ、ビュー、またはインラインテーブル値関数内にパラメーター化されたステートメントを「非表示」にすることでオプティマイザーをだますことができると提案しました。とてもシンプルだったので、うまくいくとは思いませんでした。

    しかし、それから私はそれを試しました:

    CREATE PROCEDURE dbo.GetUnshippedOrders
    AS
    BEGIN
      SET NOCOUNT ON;
      SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
    END
    GO
     
    CREATE VIEW dbo.vUnshippedOrders
    AS
      SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
    GO
     
    CREATE FUNCTION dbo.fnUnshippedOrders()
    RETURNS TABLE
    AS
      RETURN (SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0);
    GO

    これらの3つのクエリはすべて、フィルタリングされたインデックスに対して効率的なシークを実行します。

    EXEC dbo.GetUnshippedOrders;
    GO
    SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders;
    GO
    SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();

    結論

    これがとても効果的だったのには驚きました。もちろん、これにはアプリケーションを変更する必要があります。ストアドプロシージャを呼び出したり、ビューや関数を参照したりするようにアプリコードを変更できない場合(またはOPTION (RECOMPILE)を追加する場合もあります )、他のオプションを探し続ける必要があります。ただし、アプリケーションコードを変更できる場合は、述語を別のモジュールに詰め込むのが良い方法かもしれません。


    1. 多次元配列を集約する関数の初期配列

    2. PHPが複数のチェックボックスとテキストボックスの配列をMySQLデータベースに挿入

    3. SQLスキーマのみをバックアップしますか?

    4. psqlを介してSQLスクリプトを実行すると、PgAdminでは発生しない構文エラーが発生します