COUNT句をOVER句とともに使用できますか?


25

次のクエリのパフォーマンスを改善しようとしています。

        UPDATE  [#TempTable]
        SET     Received = r.Number
        FROM    [#TempTable] 
        INNER JOIN (SELECT  AgentID,
                            RuleID,
                            COUNT(DISTINCT (GroupId)) Number
                    FROM    [#TempTable]
                    WHERE   Passed = 1
                    GROUP BY AgentID,
                            RuleID
                   ) r ON r.RuleID = [#TempTable].RuleID AND
                          r.AgentID = [#TempTable].AgentID                            

現在、私のテストデータでは約1分かかります。このクエリが存在するすべてのストアドプロシージャに対する変更への入力は限られていますが、おそらくこの1つのクエリを変更することができます。または、インデックスを追加します。次のインデックスを追加してみました。

CREATE CLUSTERED INDEX ix_test ON #TempTable(AgentID, RuleId, GroupId, Passed)

また、実際にクエリにかかる時間は2倍になりました。NON-CLUSTEREDインデックスでも同じ効果が得られます。

効果なしで次のように書き直してみました。

        WITH r AS (SELECT  AgentID,
                            RuleID,
                            COUNT(DISTINCT (GroupId)) Number
                    FROM    [#TempTable]
                    WHERE   Passed = 1
                    GROUP BY AgentID,
                            RuleID
            ) 
        UPDATE  [#TempTable]
        SET     Received = r.Number
        FROM    [#TempTable] 
        INNER JOIN r 
            ON r.RuleID = [#TempTable].RuleID AND
               r.AgentID = [#TempTable].AgentID                            

次に、このようなウィンドウ関数を使用しようとしました。

        UPDATE  [#TempTable]
        SET     Received = COUNT(DISTINCT (CASE WHEN Passed=1 THEN GroupId ELSE NULL END)) 
                    OVER (PARTITION BY AgentId, RuleId)
        FROM    [#TempTable] 

この時点で、エラーが発生し始めました

Msg 102, Level 15, State 1, Line 2
Incorrect syntax near 'distinct'.

そこで、2つの質問があります。最初に、OVER句でCOUNT DISTINCTを実行できませんか、それとも間違って記述しましたか?第二に、私がまだ試したことがない改善を提案できる人はいますか?参考までに、これはSQL Server 2008 R2 Enterpriseインスタンスです。

編集:元の実行計画へのリンクを次に示します。私の大きな問題は、このクエリが30〜50回実行されていることです。

https://onedrive.live.com/redir?resid=4C359AF42063BD98%21772

EDIT2:これは、コメントで要求されたとおりにステートメントが存在する完全なループです。私は、ループの目的に関して定期的にこれを扱っている人に確認しています。

DECLARE @Counting INT              
SELECT  @Counting = 1              

--  BEGIN:  Cascading Rule check --           
WHILE @Counting <= 30              
    BEGIN      

        UPDATE  w1
        SET     Passed = 1
        FROM    [#TempTable] w1,
                [#TempTable] w3
        WHERE   w3.AgentID = w1.AgentID AND
                w3.RuleID = w1.CascadeRuleID AND
                w3.RulePassed = 1 AND
                w1.Passed = 0 AND
                w1.NotFlag = 0      

        UPDATE  w1
        SET     Passed = 1
        FROM    [#TempTable] w1,
                [#TempTable] w3
        WHERE   w3.AgentID = w1.AgentID AND
                w3.RuleID = w1.CascadeRuleID AND
                w3.RulePassed = 0 AND
                w1.Passed = 0 AND
                w1.NotFlag = 1        

        UPDATE  [#TempTable]
        SET     Received = r.Number
        FROM    [#TempTable] 
        INNER JOIN (SELECT  AgentID,
                            RuleID,
                            COUNT(DISTINCT (GroupID)) Number
                    FROM    [#TempTable]
                    WHERE   Passed = 1
                    GROUP BY AgentID,
                            RuleID
                   ) r ON r.RuleID = [#TempTable].RuleID AND
                          r.AgentID = [#TempTable].AgentID                            

        UPDATE  [#TempTable]
        SET     RulePassed = 1
        WHERE   TotalNeeded = Received              

        SELECT  @Counting = @Counting + 1              
    END

回答:


28

現在、この構成はSQL Serverではサポートされていません。それは将来のバージョンで実装される可能性があります(そして、私の意見では)

この欠陥を報告するフィードバック項目にリストされている回避策のいずれかを適用すると、クエリは次のように書き換えられます。

WITH UpdateSet AS
(
    SELECT 
        AgentID, 
        RuleID, 
        Received, 
        Calc = SUM(CASE WHEN rn = 1 THEN 1 ELSE 0 END) OVER (
            PARTITION BY AgentID, RuleID) 
    FROM 
    (
        SELECT  
            AgentID,
            RuleID,
            Received,
            rn = ROW_NUMBER() OVER (
                PARTITION BY AgentID, RuleID, GroupID 
                ORDER BY GroupID)
        FROM    #TempTable
        WHERE   Passed = 1
    ) AS X
)
UPDATE UpdateSet
SET Received = Calc;

結果の実行計画は次のとおりです。

計画

これには、ハロウィーン保護のためのEager Table Spoolを回避するという利点があります(自己結合のため)が、SUM OVER (PARTITION BY)結果を計算してすべての行に適用するために、ソート(ウィンドウ用)およびしばしば非効率的なLazy Table Spool構造を導入しますウィンドウで。実際に実行する方法は、あなただけが実行できる練習です。

全体的なアプローチは、うまく機能させるのが難しいものです。更新(特に自己結合に基づく更新)を大きな構造に再帰的に適用することは、デバッグには適している場合がありますが、パフォーマンスが低下するためのレシピです。繰り返される大規模なスキャン、メモリの流出、ハロウィーンの問題は、ほんの一部の問題です。インデックス作成と(その他の)一時テーブルは役立ちますが、特にプロセス内の他のステートメントによってインデックスが更新される場合は、非常に慎重な分析が必要です(インデックスの維持はクエリプランの選択に影響し、I / Oを追加します)。

最終的には、根本的な問題を解決することは興味深いコンサルティング作業になりますが、このサイトには多すぎます。しかし、この答えが表面的な質問に対処することを願っています。


元のクエリの代替解釈(より多くの行を更新する結果):

WITH UpdateSet AS
(
    SELECT 
        AgentID, 
        RuleID, 
        Received, 
        Calc = SUM(CASE WHEN Passed = 1 AND rn = 1 THEN 1 ELSE 0 END) OVER (
            PARTITION BY AgentID, RuleID) 
    FROM 
    (
        SELECT  
            AgentID,
            RuleID,
            Received,
            Passed,
            rn = ROW_NUMBER() OVER (
                PARTITION BY AgentID, RuleID, Passed, GroupID
                ORDER BY GroupID)
        FROM    #TempTable
    ) AS X
)
UPDATE UpdateSet
SET Received = Calc
WHERE Calc > 0;

計画2

注:ソートを削除すると(インデックスを提供するなど)、必要なハロウィーン保護を提供するためにEager Spoolなどの必要性が再導入される可能性があります。ソートはブロック演算子であるため、完全な相分離を提供します。


6

ネクロマンシング:

DENSE_RANKを使用して、パーティションで異なるカウントをエミュレートするのは比較的簡単です。

;WITH baseTable AS
(
              SELECT 'RM1' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM1' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR2' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR2' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR3' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM2' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR1' AS ADR
    UNION ALL SELECT 'RM3' AS RM, 'ADR2' AS ADR
)
,CTE AS
(
    SELECT RM, ADR, DENSE_RANK() OVER(PARTITION BY RM ORDER BY ADR) AS dr 
    FROM baseTable
)
SELECT
     RM
    ,ADR

    ,COUNT(CTE.ADR) OVER (PARTITION BY CTE.RM ORDER BY ADR) AS cnt1 
    ,COUNT(CTE.ADR) OVER (PARTITION BY CTE.RM) AS cnt2 
    -- Geht nicht / Doesn't work 
    --,COUNT(DISTINCT CTE.ADR) OVER (PARTITION BY CTE.RM ORDER BY CTE.ADR) AS cntDist
    ,MAX(CTE.dr) OVER (PARTITION BY CTE.RM ORDER BY CTE.RM) AS cntDistEmu 
FROM CTE

3
このセマンティクスはcount、列がNULL可能かどうかとは異なります。それはあなたが1減算する必要がある任意のヌルが含まれている場合
マーティン・スミス

@マーティンスミス:いいキャッチ。明らかに、null値がある場合はWHERE ADR IS NOT NULLを追加する必要があります。
苦境
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.