ビューでの集計操作はインデックスを無視する[終了]


8

シナリオ

かつては、ETLプロセスに参加し、多数のサードパーティソースからのさまざまな形式のファイルの受信カタログとして機能するステージングデータベースが小さな会社にありました。EはDTSパッケージを介して処理され、監査または制御のための制御構造はほとんどありませんでしたが、「十分十分」と見なされ、すべての意図および目的はそうでした。

E部分によって提供されるデータは、少数の若くて有能なプログラマーによって開発および管理される単一のアプリケーションによる消費を目的としています。当時のデータウェアハウジング技術に関する経験や知識はありませんが、アプリケーションコードから独自のTおよびLプロセスを設定および作成しました。驚いたことに、これらの未熟なソフトウェアエンジニアは、部外者が「理想的とは言えない車輪」と呼ぶものを発明しましたが、「現在十分」を常に存在するサービスレベルとして、運用フレームワークを提供することができました。

しばらくの間、密に結合された領域ではすべてが順調で、ステージングカタログは12のサードパーティのデータをごちそうし、アプリケーションによって供給されていました。アプリケーションが成長するにつれて、その欲求も大きくなりましたが、熟練した白人の騎士の開発者がシステムを監視しているため、これらの欲求は、多くの場合、迅速に対処されました。

しかし、もちろん黄金時代は永遠に続くことはできませんでした。アプリケーションの成功によって繁栄がもたらされ、ビジネスはどんどん成長しました。それが成長するにつれて、ステージング環境とアプリケーションはそれとともに成長することを余儀なくされました。すべての警戒のために、ほんの一握りのヒーロー開発者は、現在の拡張システムを維持するのに追いつくことができず、消費者は自分のデータを受け取る資格がありました。もはやそれは彼らが必要とするもの、あるいは望んでいるものの問題ではなかったが、大衆は彼らがそれに値するだけでもっと多くを要求すると感じた。

ビジネスは盗品でいっぱいの金庫で武装して、市場に手を差し伸べ、成長し続けるシステムをサポートするのを助けるために開発者と管理者を雇いました。あらゆる精神の傭兵が会社に集まりましたが、この成長により、利用可能な専門家のガイダンスにはほとんど影響がありませんでした。新しい開発者と管理者は、欲求不満のせいですべての戦争が起きるまで、自家製のスイートの複雑さを理解するのに苦労しました。各部門はすべての問題を単独で解決しようと試み始め、お互いに取り組むよりもお互いに取り組むためにより多くのことをしました。単一のプロジェクトまたはイニシアチブは、それぞれがわずかに異なるいくつかの異なる方法で実装されます。そのすべての緊張は白い騎士の一部にとっては大きすぎることが判明し、彼らが倒れると帝国は崩壊しました。間もなく、システムは混乱状態になりました。

スパゲッティコードの悪質な約束に対するこれらの分野の変化にもかかわらず、会社は耐えました。結局のところ、それは「十分に良い」でした。

チャレンジ

いくつかの政権交代と後の採用担当者の採用で、私は会社の雇用に気づきました。大戦から長い年月が経ちましたが、被害は依然として非常に目に見えます。システムのE部分のいくつかの弱点に対処し、DTSパッケージをSSISにアップグレードすることを装っていくつかのコントロールテーブルを追加できました。これらは現在、実際のデータウェアハウジングの専門家が通常の作成時に使用しています文書化されたTおよびLの交換。

最初のハードルは、値を切り捨てたり、ネイティブデータタイプを変更したりすることなく、再ロードとパージのためのいくつかの制御キーを含める方法で、サードパーティのファイルからデータをインポートすることでした。これはすべてうまくいきましたが、アプリケーションはこれらの新しいテーブルにシームレスで透過的な方法でアクセスできる必要がありました。DTSパッケージはテーブルにデータを入力し、アプリケーションによって直接読み取られます。SSISのアップグレードはQAの理由で並行して実行する必要がありますが、これらの新しいパッケージにはさまざまな制御キーが含まれ、パーティションスキームも活用します。実際のメタデータの変更だけでなく、いずれにしても新しいテーブルを完全に保証できるほど重要になる可能性があるため、新しいSSISパッケージには新しいテーブルが使用されました。

信頼性の高いデータインポートが機能し、倉庫チームによって使用されているため、実際の課題は、ステージング環境に直接アクセスするアプリケーションに新しいデータを提供することであり、アプリケーションコードへの影響は最小限(「いいえ」)です。このため、私はビューを使用することを選択しました。テーブルの名前をdbo.DailyTransactionto などに変更し、ビューdbo.DailyTranscation_LEGACYdbo.DailyTransactionオブジェクト名を再利用します。LEGACY指定されたテーブル。これらのテーブルに含まれる長年のデータを再ロードすることはビジネスの観点からの選択肢ではないため、新しいSSISで作成されパーティション化されたテーブルが本番環境に移行するため、古いDTSインポートはオフになり、アプリケーションは新しいテーブルの新しいデータにもアクセスします。この時点で、ビューは更新され、新しいテーブル(dbo.DailyTransactionCompleteたとえば)からデータを選択できるようになります(使用できない場合はレガシーテーブルから選択します)。

実際、次のようなことが行われています。

CREATE VIEW dbo.DailyTransaction
AS  SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.DailyTransactionComplete
    UNION ALL
    SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.DailyTransaction_LEGACY l
    WHERE NOT EXISTS (  SELECT  1
                        FROM    dbo.DailyTransactionComplete t
                        WHERE   t.FileDate = l.FileDate );

論理的には健全ですが、これは多くの集計ケースではまったくうまく機能せず、一般に、レガシーテーブル内のデータに対してフルインデックススキャンを実行する実行プランになります。これはおそらく数千万のレコードでは問題ありませんが、数億のレコードではそれほどではありません。後者が実際に当てはまるので、私は「創造的」であることに頼らざるを得ませんでした。その結果、インデックス付きビューを作成することになりました。

ここで私が設定した小さなテストケースを示します。FileDate制御キーがデータウェアハウス互換DateCode_FKポートに移植されており、当面は検索可能な新しいテーブルに対するクエリがどれほど少ないかを示しています。

USE tempdb;
GO

SET NOCOUNT ON;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransaction_LEGACY'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.DailyTransaction_LEGACY;
    CREATE TABLE dbo.DailyTransaction_LEGACY
    (
        DailyTransaction_PK         BIGINT IDENTITY( 1, 1 ) NOT NULL,
        FileDate                    DATETIME NOT NULL,
        Foo                         INT NOT NULL
    );

    INSERT INTO dbo.DailyTransaction_LEGACY ( FileDate, Foo )
    SELECT  DATEADD( DAY, ( 1 - ROW_NUMBER() 
                OVER( ORDER BY so1.object_id ) - 800 ) % 1000, 
                CONVERT( DATE, GETDATE() ) ),
            so1.object_id % 1000 + so2.object_id % 1000
    FROM    sys.all_objects so1
    CROSS JOIN sys.all_objects so2;

    ALTER TABLE dbo.DailyTransaction_LEGACY
    ADD CONSTRAINT PK__DailyTrainsaction
        PRIMARY KEY CLUSTERED ( DailyTransaction_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );
END;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransactionComplete'
                    AND type = 'U' )
BEGIN
    --DROP TABLE dbo.DailyTransactionComplete;
    CREATE TABLE dbo.DailyTransactionComplete
    (
        DailyTransaction_PK            BIGINT IDENTITY( 1, 1 ) NOT NULL,
        DateCode_FK                    INTEGER NOT NULL,
        Foo                            INTEGER NOT NULL
    );

    INSERT INTO dbo.DailyTransactionComplete ( DateCode_FK, Foo )
    SELECT  TOP 100000
            CONVERT( INTEGER, CONVERT( VARCHAR( 8 ), DATEADD( DAY, 
                ( 1 - ROW_NUMBER() OVER( ORDER BY so1.object_id ) ) % 100, 
                GETDATE() ), 112 ) ),
            so1.object_id % 1000
    FROM    sys.all_objects so1
    CROSS JOIN sys.all_objects so2;

    ALTER TABLE dbo.DailyTransactionComplete
    ADD CONSTRAINT PK__DailyTransaction
        PRIMARY KEY CLUSTERED ( DateCode_FK, DailyTransaction_PK )
    WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 100 );        
END;
GO

私のローカルサンドボックスでは、上記により、約440万行のレガシーテーブルと、0.1万行を含む新しいテーブルが取得されますが、DateCode_FK/のFileDate値が一部重複しています。

MAX( FileDate )追加のインデックスのないレガシーテーブルに対するA は、私が期待するものについて実行されます。

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput        DATETIME;
SELECT  @ConsumeOutput = MAX( FileDate )
FROM    dbo.DailyTransaction_LEGACY;

SET STATISTICS IO, TIME OFF;
GO

テーブル「DailyTransaction_LEGACY」。スキャンカウント1、論理読み取り9228、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

SQL Server実行時間:CPU時間= 889ミリ秒、経過時間= 886ミリ秒。

クラスタ化インデックス、レガシー

テーブルに単純なインデックスを投げると、状況が大幅に改善されます。まだスキャンですが、440万レコードではなく1レコードをスキャンしています。私はそれでクールです。

CREATE NONCLUSTERED INDEX IX__DailyTransaction__FileDate
    ON    dbo.DailyTransaction_LEGACY ( FileDate );

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput        DATETIME;
SELECT  @ConsumeOutput = MAX( FileDate )
FROM    dbo.DailyTransaction_LEGACY;

SET STATISTICS IO, TIME OFF;
GO

SQL Serverの解析およびコンパイル時間:CPU時間= 0ミリ秒、経過時間= 1ミリ秒。テーブル「DailyTransaction_LEGACY」。スキャンカウント1、論理読み取り3、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

SQL Server実行時間:CPU時間= 0ミリ秒、経過時間= 0ミリ秒。

非クラスター化インデックス、レガシ

そして今、ビューを作成することで、開発者がコードを変更する必要がないようにしています。一種の大変動。

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransaction'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.DailyTransaction
AS  SELECT  DailyTransaction_PK, FileDate = CONVERT( 
                DATETIME, CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), Foo
    FROM    dbo.DailyTransactionComplete
    UNION ALL    
    SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.DailyTransaction_LEGACY l
    WHERE   NOT EXISTS (    SELECT  1
                            FROM    dbo.DailyTransactionComplete t
                            WHERE   CONVERT( DATETIME, CONVERT( VARCHAR( 8 ),
                                        t.DateCode_FK ), 112 ) = l.FileDate );
GO

はい、サブクエリはひどいですが、これは問題ではありません。おそらく、永続的な計算列を作成し、実際の問題が解決されたら、その目的のためにインデックスをスローします。それ以上の手間をかけずに

問題

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput1        DATETIME;
SELECT  @ConsumeOutput1 = MAX( FileDate )
FROM    dbo.DailyTransaction;

SET STATISTICS IO, TIME OFF;
GO

SQL Serverの解析およびコンパイル時間:CPU時間= 0ミリ秒、経過時間= 4ミリ秒。テーブル「DailyTransaction_LEGACY」。スキャンカウント1、論理読み取り11972、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル「ワークファイル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル 'DailyTransactionComplete'。スキャンカウント2、論理読み取り620、物理読み取り0、先読み読み取り0、lob論理読み取り0、lob物理読み取り0、lob先読み読み取り0。

SQL Server実行時間:CPU時間= 983ミリ秒、経過時間= 983ミリ秒。

プランを見る

わかりました、Sql Serverは私がやっていることはばかげていると私に伝えようとしています。私は概ね同意しますが、それは私の苦境を変えません。これはFileDatedbo.DailyTransactionビューのonが述語に含まれているクエリでは実際に見事に機能しますが、MAX計画は十分に悪いTOPものですが、計画はすべてを南向きに送信します。真南。

SET STATISTICS IO, TIME ON;

SELECT  TOP 10 FileDate
FROM    dbo.DailyTransaction
GROUP BY FileDate 
ORDER BY FileDate DESC

SET STATISTICS IO, TIME OFF;
GO

テーブル「DailyTransactionComplete」。スキャンカウント2、論理読み取り1800110、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル 'DailyTransaction_LEGACY'。スキャンカウント1、論理読み取り1254、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル「ワークファイル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

SQL Server実行時間:CPU時間= 109559ミリ秒、経過時間= 109664ミリ秒。

上

先ほど「クリエイティブ」になることについて述べましたが、これはおそらく誤解を招くものでした。私が言うことは「もっと愚かだった」ので、集計操作中にこのビューを機能させるための私の試みはdbo.DailyTransactionCompletedbo.DailyTransaction_LEGACYテーブルと テーブルにビューを作成し、スキーマバインドとインデックス付けを行い、NOEXPANDヒント付きで別のビューでそれらのビューを使用することでしたレガシービューで。それが今何をする必要があるかについては多かれ少なかれ働いていますが、私は「解決策」全体がかなり動揺していて、次のように最高潮に達していると思います:

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'v_DailyTransactionComplete'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.v_DailyTransactionComplete AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.v_DailyTransactionComplete
AS  SELECT  DailyTransaction_PK, FileDate = CONVERT( DATETIME, 
                CONVERT( VARCHAR( 8 ), DateCode_FK ), 112 ), 
            Foo
    FROM    dbo.DailyTransactionComplete;
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'v_DailyTransaction_LEGACY'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.v_DailyTransaction_LEGACY AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.v_DailyTransaction_LEGACY
WITH SCHEMABINDING
AS  SELECT  l.DailyTransaction_PK,
            l.FileDate,
            l.Foo,
            CountBig = COUNT_BIG( * )
    FROM    dbo.DailyTransaction_LEGACY l
    INNER JOIN dbo.DailyTransactionComplete n
        ON  l.FileDate <> CONVERT( DATETIME, CONVERT( VARCHAR( 8 ), 
                n.DateCode_FK ), 112 )
    GROUP BY l.DailyTransaction_PK,
            l.FileDate,
            l.Foo;
GO

CREATE UNIQUE CLUSTERED INDEX CI__v_DailyTransaction_LEGACY
    ON dbo.v_DailyTransaction_LEGACY ( FileDate, DailyTransaction_PK )
WITH ( DATA_COMPRESSION = PAGE, FILLFACTOR = 80 );
GO

IF NOT EXISTS ( SELECT  1
                FROM    sys.objects
                WHERE   name = 'DailyTransaction'
                    AND type = 'V' )
BEGIN
    EXEC( 'CREATE VIEW dbo.DailyTransaction AS SELECT x = 1;' );
END;
GO

ALTER VIEW dbo.DailyTransaction
AS  SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.v_DailyTransactionComplete
    UNION ALL    
    SELECT  DailyTransaction_PK, FileDate, Foo
    FROM    dbo.v_DailyTransaction_LEGACY WITH ( NOEXPAND );
GO

オプティマイザーにインデックス付きビューによって提供されるインデックスを使用するように強制すると、MAXおよびTOP問題が解消されますが、ここで私がやろうとしていることを達成するためのより良い方法があるはずです。絶対にどんな提案/叱責も大歓迎です!!

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput1        DATETIME;
SELECT  @ConsumeOutput1 = MAX( FileDate )
FROM    dbo.DailyTransaction;

SET STATISTICS IO, TIME OFF;
GO

テーブル 'v_DailyTransaction_LEGACY'。スキャンカウント1、論理読み取り3、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル 'DailyTransactionComplete'。スキャンカウント1、論理読み取り310、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

SQL Server実行時間:CPU時間= 31ミリ秒、経過時間= 36ミリ秒。

SET STATISTICS IO, TIME ON;

DECLARE @ConsumeOutput1        DATETIME;
SELECT  TOP 10 @ConsumeOutput1 = FileDate
FROM    dbo.DailyTransaction
GROUP BY FileDate 
ORDER BY FileDate DESC

SET STATISTICS IO, TIME OFF;
GO

テーブル 'v_DailyTransaction_LEGACY'。スキャンカウント1、論理読み取り101、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル「ワークテーブル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル「ワークファイル」。スキャンカウント0、論理読み取り0、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。テーブル 'DailyTransactionComplete'。スキャンカウント1、論理読み取り310、物理読み取り0、先読み読み取り0、LOB論理読み取り0、LOB物理読み取り0、LOB先読み読み取り0。

SQL Server実行時間:CPU時間= 63ミリ秒、経過時間= 66ミリ秒。

TL; DR:

私が述べた最初のビューで集計クエリを合理的な時間内に合理的なI / Oリソース使用率で実行するために何をする必要があるかを理解してください。


3
インデックス付けされていないビューにはデータが格納されておらず、サブクエリやユニオンなどでビューにインデックスを付けることはできません。そのビューを2つのビューに分割するなど、別の方法でデータを具体化することを検討する必要があると思います次に、それらに対してクエリを実行するか、ビューを完全にバイパスします。
アーロンバートランド

私はあなたが提案していることを達成しようとしていると思いますが、私が何をすべきかを完全には把握していません。バンドエイドとしてインデックスを作成して具体化したレガシービューは、サブクエリの制限に対するかなり大まかな回避策のようであり、現在の状態では多かれ少なかれ機能しますが、追加の影響を非常に受けやすいと思いますスコープクリープ。インポートが発生した後、まったく新しいベーステーブルを生成するプロセスを設定し、それを参照するようにビューを変更するという考えに取り組んでいます。
Avarkx 2014

回答:


4

不等結合NOT EXISTSDISTINCT介してのように書き換えると、ビューにインデックスを付けることができますが、これが一般的に行われない正当な理由があります。

ビューにインデックスを作成するために生成された実行プランは、不可避な恐ろしいものです。不等式は、ネストされたループの物理結合を強制します。1つの値を除いて、これはクロス結合です。(例のコードのように)結合列がnull可能ではないと想定して、製品を明確なまたは同等のgroup byで折りたたむと正しい結果が得られますが、効率的ではありません。この非効率性は、時間が経過し、関係するテーブルが大きくなると悪化するだけです。

同様の問題は、ビューによって参照されるテーブルに影響を与えるすべてのDMLステートメントの実行プランに影響します(ビューは常にSQL Serverのベーステーブルと同期する必要があるため)。どちらのテーブルでも単一の行を追加または変更するために生成された実行プランを見て、私の意味を確認してください。

大まかに言うと、あなたが戦っている問題は、SQL Serverクエリオプティマイザーがを含むビューに対して常に適切な計画を生成するとは限らないということUNION ALLです。私たちが当たり前と考えている最適化の多く(MAX->などTOP (1))は、ユニオン全体に実装されていません。

解決する問題ごとに、通常の期待される最適化が行われず、パフォーマンスが低下する実行プランが発生する別のケースが見つかります。明白な解決策は、ビューで共用体を使用しないことです。あなたのケースでこれをどのように実装するかは、質問の詳細にもかかわらず、おそらくあなただけが知っている詳細に依存します。

スペースがある場合、1つの解決策はcompletelegacyベーステーブルを個別に維持し、(存在しないロジックを含めて)維持することです。これによりデータの重複が発生し、同期の問題が発生しますが、私の経験では、これらは、すべての(またはほとんどの)状況で幅広いクエリの適切な実行プランを生成するためにユニオンビューを取得するよりも、はるかに簡単に確実に解決できます。

SQL Serverには、変更の追跡、変更データのキャプチャ、トリガーなど、ご存じのとおり、データの同期を支援する多数の機能が用意されています。実装の詳細はこのフォーラムを超えています。重要な点は、オプティマイザにすべてのビューを結合するのではなく、ベーステーブルを提示することです。


あなたと@AaronBertrandの両方にあなたの入力をありがとう。あなたの洞察と提案は非常に高く評価されています!おそらく古いテーブルのデータを新しいテーブルに手動で移行して、古いテーブルから結合を解除する必要がなくなるようにビューを変更できるようになるでしょう。理論的には、レガシーテーブルを完全に削除できるはずです。このアプローチには他にも課題がありますが、すでに述べたように、私がやっていることは明らかにうまくいかないため、長期的にはこれらの課題はより扱いやすくなるでしょう。
Avarkx 14
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.