Esta consulta mostra a contagem de usuários ativos efetiva no final do mês.
Como funciona:
-
Converta cada linha de entrada (comStartDate
eEndDate
valor) em dois linhas que representam um ponto no tempo quando a contagem de usuários ativos foi incrementada (emStartDate
) e decrementado (emEndDate
). Precisamos converterNULL
para um valor de data distante porqueNULL
os valores são classificados antes em vez de depois de nãoNULL
valores:
Isso faz com que seus dados fiquem assim:
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
-
Então nós simplesmenteSUM OVER
aChange
valores (após a classificação) para obter a contagem de usuários ativos a partir dessa data específica:
Então, primeiro, classifique porOnThisDate
:
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
EntãoSUM 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
-
Então nósPARTITION
(não agrupar!) as linhas por mês e classificá-las por data para que possamos identificar o últimoActiveCount
linha para esse mês (isso realmente acontece noWHERE
da consulta mais externa, usandoROW_NUMBER()
eCOUNT()
para cada mêsPARTITION
):
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
-
Em seguida, filtre ondeIsLastInMonth = 1
(na verdade, ondeROW_COUNT() = COUNT(*)
dentro de cadaPARTITION
) para nos fornecer os dados de saída finais:
At-end-of-month Active-count 2017-01 1 2018-01 3 2019-01 3 2019-03 2 2019-06 1 9999-12 0
Isso resulta em "lacunas" no conjunto de resultados porque o
At-end-of-month
coluna mostra apenas linhas onde o Active-count
valor realmente mudou em vez de incluir todos os meses do calendário possíveis - mas isso é ideal (no que me diz respeito) porque exclui dados redundantes. O preenchimento das lacunas pode ser feito dentro do código do aplicativo simplesmente repetindo as linhas de saída para cada mês adicional até atingir o próximo At-end-of-month
valor. Aqui está a consulta usando T-SQL no SQL Server (não tenho acesso ao Oracle agora). E aqui está o SQLFiddle que usei para chegar a uma solução: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
Esta consulta pode ser achatado em menos consultas aninhadas usando funções de agregação e janela diretamente em vez de usar aliases (como
OtdYear
, ActiveCount
, etc), mas isso tornaria a consulta muito mais difícil de entender.