Функция разделения COUNT () OVER возможна при использовании DISTINCT

88

Я пытаюсь написать следующее, чтобы получить общее количество различных NumUsers, например:

NumUsers = COUNT(DISTINCT [UserAccountKey]) OVER (PARTITION BY [Mth])

Студия менеджмента, похоже, не слишком довольна этим. Ошибка исчезает, когда я удаляю DISTINCTключевое слово, но тогда это не будет отдельного подсчета.

DISTINCTне представляется возможным в рамках функций секционирования. Как мне найти точное количество? Могу ли я использовать более традиционный метод, такой как коррелированный подзапрос?

Если посмотреть на это немного подробнее, возможно, эти OVERфункции работают иначе, чем Oracle, в том смысле, что их нельзя использовать SQL-Serverдля расчета промежуточных итогов.

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

почему
источник
2
COUNTwith ORDER BYвместо PARTITION BYis нечетко определено в 2008 году. Я удивлен, что он вообще позволяет вам его иметь. Согласно документации , вам не разрешено ORDER BYиспользовать агрегатную функцию.
Damien_The_Unbeliever
ага - думаю, я запутался с какой-то функциональностью оракула; эти
промежуточные

Ответы:

177

Есть очень простое решение, использующее dense_rank()

dense_rank() over (partition by [Mth] order by [UserAccountKey]) 
+ dense_rank() over (partition by [Mth] order by [UserAccountKey] desc) 
- 1

Это даст вам именно то, о чем вы просили: количество различных UserAccountKeys в течение каждого месяца.

Дэвид
источник
23
Следует быть осторожным с тем dense_rank(), что он будет считать NULL, тогда как COUNT(field) OVERнет. Из-за этого я не могу использовать его в своем решении, но все же думаю, что это довольно умно.
bf2020
1
Но я ищу общее количество различных учетных записей пользователей по месяцам каждого года: не знаете, как это на это ответить?
whytheq
4
@ bf2020, если может быть NULLзначение в UserAccountKey, то вам нужно добавить этот термин: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth). Идея взята из ответа Ларса Рённбека ниже. По сути, если UserAccountKeyесть NULLзначения, вам нужно вычесть лишнее 1из результата, потому что DENSE_RANKучитываются NULL.
Владимир Баранов
1
@ahsteele, спасибо, чувак, ты взорвал мне голову и решил мою проблему
Энрике Донати
Здесь обсуждается использование этого dense_rankрешения, когда оконная функция имеет фрейм. SQL Server не позволяет dense_rankиспользовать с оконным фреймом: stackoverflow.com/questions/63527035/…
K4M
6

Некромантинг:

Относительно просто эмулировать COUNT DISTINCT над PARTITION BY с MAX через 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 
    -- Not supported
    --,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

Примечание.
Предполагается, что рассматриваемые поля являются полями, которые НЕ допускают значения NULL.
Если в полях есть одна или несколько NULL-записей, нужно вычесть 1.

Стефан Штайгер
источник
5

Я использую решение, аналогичное тому, что было у Дэвида выше, но с дополнительным поворотом, если некоторые строки следует исключить из подсчета. Это предполагает, что [UserAccountKey] никогда не имеет значения NULL.

-- subtract an extra 1 if null was ranked within the partition,
-- which only happens if there were rows where [Include] <> 'Y'
dense_rank() over (
  partition by [Mth] 
  order by case when [Include] = 'Y' then [UserAccountKey] else null end asc
) 
+ dense_rank() over (
  partition by [Mth] 
  order by case when [Include] = 'Y' then [UserAccountKey] else null end desc
)
- max(case when [Include] = 'Y' then 0 else 1 end) over (partition by [Mth])
- 1

SQL Fiddle с расширенным примером можно найти здесь.

Ларс Рённбек
источник
1
Ваша идея может быть использована для составления исходной формулы (без сложностей, о [Include]которых вы говорите в своем ответе) с dense_rank()работой, когда UserAccountKeyэто возможно NULL. Добавьте этот термин к формуле: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth).
Владимир Баранов
5

Я думаю, что единственный способ сделать это в SQL-Server 2008R2 - использовать коррелированный подзапрос или внешнее применение:

SELECT  datekey,
        COALESCE(RunningTotal, 0) AS RunningTotal,
        COALESCE(RunningCount, 0) AS RunningCount,
        COALESCE(RunningDistinctCount, 0) AS RunningDistinctCount
FROM    document
        OUTER APPLY
        (   SELECT  SUM(Amount) AS RunningTotal,
                    COUNT(1) AS RunningCount,
                    COUNT(DISTINCT d2.dateKey) AS RunningDistinctCount
            FROM    Document d2
            WHERE   d2.DateKey <= document.DateKey
        ) rt;

Это можно сделать в SQL-Server 2012, используя предложенный вами синтаксис:

SELECT  datekey,
        SUM(Amount) OVER(ORDER BY DateKey) AS RunningTotal
FROM    document

Однако использование по- DISTINCTпрежнему запрещено, поэтому, если требуется DISTINCT и / или если обновление не является вариантом, я думаю, что OUTER APPLYэто ваш лучший вариант

GarethD
источник
круто спасибо. Я нашел этот SO-ответ, в котором есть опция OUTER APPLY, которую я попытаюсь. Вы видели в этом ответе циклический подход UPDATE ... он довольно далек и, по-видимому, быстр. В 2012 году жизнь станет проще - это прямая копия Oracle?
whytheq