Можете ли вы использовать COUNT DISTINCT с предложением 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                            

В настоящее время с моими тестовыми данными это занимает около минуты. У меня есть ограниченное количество входных данных для изменений всей хранимой процедуры, в которой находится этот запрос, но я, вероятно, могу заставить их изменить этот запрос. Или добавить индекс. Я попытался добавить следующий индекс:

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

И это фактически удвоило количество времени, которое занимает запрос. Я получаю тот же эффект с некластеризованным индексом.

Я попытался переписать это следующим образом безрезультатно.

        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'.

Итак, у меня есть два вопроса. Во-первых, вы не можете сделать COUNT DISTINCT с предложением OVER или я просто написал это неправильно? И во-вторых, кто-нибудь может предложить улучшение, которое я еще не пробовал? К вашему сведению, это экземпляр SQL Server 2008 R2 Enterprise.

РЕДАКТИРОВАТЬ: Вот ссылка на оригинальный план выполнения. Я также должен отметить, что моя большая проблема в том, что этот запрос выполняется 30-50 раз.

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

РЕДАКТИРОВАТЬ 2: Вот полный цикл, в котором оператор находится в соответствии с просьбой в комментариях. Я проверяю с человеком, который работает с этим на регулярной основе относительно цели цикла.

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 для Halloween Protection (из-за самостоятельного объединения), но в нем добавлена ​​сортировка (для окна) и часто неэффективная конструкция Lazy Table Spool для вычисления и применения SUM OVER (PARTITION BY)результата ко всем строкам. в окне Как это работает на практике - упражнение, которое только вы можете выполнить.

Общий подход трудно сделать хорошо. Применение обновлений (особенно тех, которые основаны на самосоединении) рекурсивно для большой структуры может быть полезно для отладки, но это является рецептом для низкой производительности. Повторные большие сканы, разливы памяти и проблемы Хэллоуина - это только некоторые из проблем. Индексирование и (больше) временные таблицы могут помочь, но необходим очень тщательный анализ, особенно если индекс обновляется другими операторами в процессе (поддержание индексов влияет на выбор плана запроса и добавляет ввод / вывод).

В конечном счете, решение основной проблемы может привести к интересной консультационной работе, но это слишком много для этого сайта. Я надеюсь, что этот ответ касается поверхностного вопроса.


Альтернативная интерпретация исходного запроса (приводит к обновлению большего количества строк):

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 или что-то еще для обеспечения необходимой защиты Хэллоуина. Сортировка является блокирующим оператором, поэтому она обеспечивает полное разделение фаз.

Пол Уайт говорит, что GoFundMonica
источник
6

Necromancing:

Относительно просто эмулировать подсчет, различный по разделу, с помощью 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если бы столбец обнулялся. Если он содержит какие-либо нули, вы должны вычесть 1.
Мартин Смит
@ Мартин Смит: Хороший улов. очевидно, вам нужно добавить WHERE ADR NOT NULL, если есть нулевые значения.
затруднительное