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
StartDateiEndDatewartość) 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ćNULLna odległą wartość daty, ponieważNULLwartości są sortowane przed zamiast poNULLwartoś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 OVERChangewartoś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 -1Nastę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ć ostatniActiveCountwiersz dla tego miesiąca (w rzeczywistości dzieje się to wWHEREnajbardziej 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:https://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.