To zapytanie pokazuje liczbę aktywnych użytkowników obowiązującą na koniec miesiąca.
Jak to działa:
-
Konwertuj każdy wiersz wejściowy (z
StartDate
iEndDate
wartość) na dwa wiersze reprezentujące punkt w czasie, w którym wzrosła liczba aktywnych użytkowników (w dniuStartDate
) i zmniejszony (w dniuEndDate
). Musimy przekonwertowaćNULL
na odległą wartość daty, ponieważNULL
wartości są sortowane przed zamiast poNULL
wartości:Dzięki temu Twoje dane wyglądają tak:
OnThisDate Change 2018-01-01 1 2019-01-01 -1 2018-01-01 1 9999-12-31 -1 2019-01-01 1 2019-06-01 -1 2017-01-01 1 2019-03-01 -1
-
Następnie po prostu
SUM OVER
Change
wartości (po sortowaniu), aby uzyskać liczbę aktywnych użytkowników na ten konkretny dzień:Więc najpierw posortuj według
OnThisDate
:OnThisDate Change 2017-01-01 1 2018-01-01 1 2018-01-01 1 2019-01-01 1 2019-01-01 -1 2019-03-01 -1 2019-06-01 -1 9999-12-31 -1
Następnie
SUM OVER
:OnThisDate ActiveCount 2017-01-01 1 2018-01-01 2 2018-01-01 3 2019-01-01 4 2019-01-01 3 2019-03-01 2 2019-06-01 1 9999-12-31 0
-
Następnie
PARTITION
(nie grupuj!) wierszy według miesięcy i posortuj je według daty, abyśmy mogli zidentyfikować ostatniActiveCount
wiersz dla tego miesiąca (w rzeczywistości dzieje się to wWHERE
najbardziej zewnętrznego zapytania, używającROW_NUMBER()
iCOUNT()
za każdy miesiącPARTITION
):OnThisDate ActiveCount IsLastInMonth 2017-01-01 1 1 2018-01-01 2 0 2018-01-01 3 1 2019-01-01 4 0 2019-01-01 3 1 2019-03-01 2 1 2019-06-01 1 1 9999-12-31 0 1
-
Następnie filtruj według tego, gdzie
IsLastInMonth = 1
(właściwie, gdzieROW_COUNT() = COUNT(*)
wewnątrz każdejPARTITION
) aby dać nam ostateczne dane wyjściowe:At-end-of-month Active-count 2017-01 1 2018-01 3 2019-01 3 2019-03 2 2019-06 1 9999-12 0
Powoduje to „luki” w zestawie wyników, ponieważ At-end-of-month
kolumna pokazuje tylko wiersze, w których Active-count
wartość faktycznie się zmieniła, zamiast uwzględniać wszystkie możliwe miesiące kalendarzowe – ale jest to idealne (jeśli o mnie chodzi), ponieważ wyklucza zbędne dane. Wypełnianie luk można wykonać w kodzie aplikacji, po prostu powtarzając wiersze wyjściowe dla każdego dodatkowego miesiąca, aż do osiągnięcia następnego At-end-of-month
wartość.
Oto zapytanie używające T-SQL na SQL Server (nie mam w tej chwili dostępu do Oracle). A oto SQLFiddle, z którym kiedyś doszedłem do rozwiązania:http://sqlfiddle.com/# !18/ad68b7/24
SELECT
OtdYear,
OtdMonth,
ActiveCount
FROM
(
-- This query adds columns to indicate which row is the last-row-in-month ( where RowInMonth == RowsInMonth )
SELECT
OnThisDate,
OtdYear,
OtdMonth,
ROW_NUMBER() OVER ( PARTITION BY OtdYear, OtdMonth ORDER BY OnThisDate ) AS RowInMonth,
COUNT(*) OVER ( PARTITION BY OtdYear, OtdMonth ) AS RowsInMonth,
ActiveCount
FROM
(
SELECT
OnThisDate,
YEAR( OnThisDate ) AS OtdYear,
MONTH( OnThisDate ) AS OtdMonth,
SUM( [Change] ) OVER ( ORDER BY OnThisDate ASC ) AS ActiveCount
FROM
(
SELECT
StartDate AS [OnThisDate],
1 AS [Change]
FROM
tbl
UNION ALL
SELECT
ISNULL( EndDate, DATEFROMPARTS( 9999, 12, 31 ) ) AS [OnThisDate],
-1 AS [Change]
FROM
tbl
) AS sq1
) AS sq2
) AS sq3
WHERE
RowInMonth = RowsInMonth
ORDER BY
OtdYear,
OtdMonth
To zapytanie może być spłaszczone do mniejszej liczby zagnieżdżonych zapytań, używając bezpośrednio funkcji agregujących i funkcji okna zamiast aliasów (takich jak OtdYear
, ActiveCount
, itp.), ale to sprawiłoby, że zapytanie byłoby znacznie trudniejsze do zrozumienia.