Funkcja partycji COUNT () OVER jest możliwa przy użyciu DISTINCT

88

Próbuję napisać co następuje, aby uzyskać bieżącą liczbę różnych NumUserów, na przykład:

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

Studio zarządzania nie wydaje się z tego powodu zbytnio zadowolone. Błąd znika po usunięciu DISTINCTsłowa kluczowego, ale wtedy nie będzie to wyraźna liczba.

DISTINCTnie wydaje się być możliwe w ramach funkcji partycji. Jak mam znaleźć odrębną liczbę? Czy używam bardziej tradycyjnej metody, takiej jak skorelowane podzapytanie?

Patrząc nieco dalej, być może te OVERfunkcje działają inaczej niż Oracle, ponieważ nie można ich używać SQL-Serverdo obliczania sum bieżących.

Dodałem tutaj przykład na żywo w SQLfiddle, w którym próbuję użyć funkcji partycji do obliczenia bieżącej sumy.

whytheq
źródło
2
COUNTz ORDER BYzamiast PARTITION BYjest źle zdefiniowana w 2008 roku. Dziwię się, że w ogóle pozwala ci to mieć. Zgodnie z dokumentacją nie możesz ORDER BYkorzystać z funkcji agregującej.
Damien_The_Unbeliever
tak - myślę, że jestem mylony z jakąś funkcjonalnością wyroczni; te podsumowania biegowe i liczniki biegowe będą nieco bardziej
skomplikowane

Odpowiedzi:

177

Jest bardzo proste rozwiązanie przy użyciu dense_rank()

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

W ten sposób uzyskasz dokładnie to, o co prosiłeś: liczbę różnych kluczy UserAccountKeys w każdym miesiącu.

David
źródło
23
Jedną rzeczą, na którą należy uważać, dense_rank()jest to, że będzie liczyć wartości NULL, a COUNT(field) OVERnie. Z tego powodu nie mogę go zastosować w moim rozwiązaniu, ale nadal uważam, że jest całkiem sprytne.
bf2020
1
Ale ja szukam bieżącej liczby różnych kluczy kont użytkowników w miesiącach każdego roku: nie wiesz, jak to odpowiada?
dlaczego
4
@ bf2020, jeśli nie może być NULLwartości w UserAccountKey, wtedy trzeba dodać ten termin: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth). Pomysł pochodzi z odpowiedzi LarsRönnbäcka poniżej. Zasadniczo, jeśli UserAccountKeyma NULLwartości, musisz odjąć dodatkowe 1od wyniku, ponieważ DENSE_RANKliczy NULL.
Vladimir Baranov
1
@ahsteele dziękuję człowieku, rozwaliłeś mnie i rozwiązałeś mój problem
Henrique Donati
Tutaj omówienie korzystania z tego dense_rankrozwiązania, gdy funkcja okna ma ramkę. SQL Server nie pozwala na dense_rankużycie z ramką okna: stackoverflow.com/questions/63527035/ ...
K4M
6

Nekromancja:

Emulowanie COUNT DISTINCT przez PARTITION BY z MAX przez DENSE_RANK jest stosunkowo proste:

;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

Uwaga: przy
założeniu, że dane pola są polami, które nie dopuszczają wartości null.
Jeśli w polach znajduje się jeden lub więcej wpisów NULL, należy odjąć 1.

Stefan Steiger
źródło
5

Używam rozwiązania podobnego do rozwiązania Davida powyżej, ale z dodatkowym skrętem, jeśli niektóre wiersze powinny zostać wyłączone z liczenia. Zakłada się, że [UserAccountKey] nigdy nie jest 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 z rozszerzonym przykładem można znaleźć tutaj.

Lars Rönnbäck
źródło
1
Twój pomysł może zostać wykorzystany do stworzenia oryginalnej formuły (bez zawiłości, o [Include]której mówisz w swojej odpowiedzi) z dense_rank()pracą, kiedy UserAccountKeyjest to możliwe NULL. Dodaj tego określenia wzoru: -MAX(CASE WHEN UserAccountKey IS NULL THEN 1 ELSE 0 END) OVER (PARTITION BY Mth).
Vladimir Baranov
5

Myślę, że jedynym sposobem na zrobienie tego w SQL-Server 2008R2 jest użycie skorelowanego podzapytania lub zastosowania zewnętrznego:

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;

Można to zrobić w SQL-Server 2012 przy użyciu sugerowanej składni:

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

Jednak użycie DISTINCTnadal jest niedozwolone, więc jeśli DISTINCT jest wymagany i / lub jeśli aktualizacja nie jest opcją, myślę, że OUTER APPLYjest to najlepsza opcja

GarethD
źródło
fajnie dziękuję. Znalazłem tę odpowiedź SO, która zawiera opcję OUTER APPLY, którą spróbuję. Czy widziałeś zapętlone podejście UPDATE w tej odpowiedzi ... jest dość daleko i najwyraźniej szybkie. Życie będzie łatwiejsze w 2012 roku - czy to zwykła kopia Oracle?
whytheq