Oracle
 sql >> Base de Dados >  >> RDS >> Oracle

Oracle SQL - Selecione usuários entre duas datas por mês


Esta consulta mostra a contagem de usuários ativos efetiva no final do mês.

Como funciona:

  1. Converta cada linha de entrada (com StartDate e EndDate valor) em dois linhas que representam um ponto no tempo quando a contagem de usuários ativos foi incrementada (em StartDate ) e decrementado (em EndDate ). Precisamos converter NULL para um valor de data distante porque NULL os valores são classificados antes em vez de depois de não NULL 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
    

  2. Então nós simplesmente SUM OVER a Change valores (após a classificação) para obter a contagem de usuários ativos a partir dessa data específica:

    Então, primeiro, classifique por 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
    

    Então 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
    

  3. Então nós PARTITION (não agrupar!) as linhas por mês e classificá-las por data para que possamos identificar o último ActiveCount linha para esse mês (isso realmente acontece no WHERE da consulta mais externa, usando ROW_NUMBER() e COUNT() para cada mês PARTITION ):
    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
    

  4. Em seguida, filtre onde IsLastInMonth = 1 (na verdade, onde ROW_COUNT() = COUNT(*) dentro de cada PARTITION ) 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.