Aviso de bicicletaEste artigo pode se tornar um exemplo esférico de construção de bicicletas. Se você conhece uma solução padrão ou mais elegante para o problema, ficarei feliz em vê-la nos comentários.
Em um dos projetos, precisávamos elaborar um relatório sobre transações financeiras para o período com um grupo de subtotais no final do mês.
A tarefa geralmente é simples, determine os períodos necessários em um intervalo grande, anexe cada operação a um período adequado, agrupe e some a quantia.
Para gerar períodos dentro do intervalo, usei habitualmente a função generate_series, que costumo usar para gerar sequências numéricas. Eu verifiquei a documentação sobre a possibilidade de gerar uma sequência de datas, considerada um exemplo, escrevi uma consulta e fiquei intrigada.
select gs::date from generate_series('2018-01-31', '2018-05-31', interval '1 month') as gs;
gs |
---|
31/01/2018 |
28/02/2018 |
28/03/2018 |
28/04/2018 |
28/05/2018 |
O resultado foi tão inesperado quanto lógico. A função generate_series honestamente gerou iterativamente uma sequência de datas com base no princípio de adicionar sequencialmente uma mudança no valor anterior. Ao mesmo tempo, em cada etapa, a correção e a edição da data recebida foram verificadas. O dia 31 de fevereiro não acontece, então a data foi transformada em 28 de fevereiro e a adição adicional do mês reduziu toda a sequência ao dia 28.
UPD Explicações após perguntas nos comentários. Em geral, a tarefa inicial é mais ampla - agrupar dados em dias arbitrários do mês. Por exemplo, agrupe até o 20º dia de cada mês, até o 15º dia, mas não há problemas com essas datas ao gerar. O mecanismo que estamos procurando deve igualmente criar uma sequência de 10 números de cada mês, 21 números e calcular corretamente os finais dos meses.
Gostaria de saber como a operação de adição se comportará com vários meses ao mesmo tempo? O que acontecerá se adicionarmos o intervalo não iterativamente, mas "em massa"?
select '2018-01-31'::date +interval '1 mons' 28.02.2018 select '2018-01-31'::date +interval '2 mons' 31.03.2018
Nesse caso, a adição é feita honestamente.
Como gerar as datas necessárias usando essa abordagem?
Se o número de meses for conhecido, será muito simples:
select '2018-01-31'::date +make_interval(0, i) as gs from generate_series(0, 4, 1) as i
gs |
---|
31/01/2018 |
28/02/2018 |
31/03/2018 |
30/04/2018 |
31/05/2018 |
E se apenas a data inicial e a data final forem conhecidas?
Esse problema pode ser resolvido simplesmente escrevendo uma função armazenada e um ciclo simples, no entanto, estamos interessados em uma opção de implementação quando não há possibilidade ou desejo de obstruir a estrutura do banco de dados com objetos desnecessários.
Vamos tentar reduzir a tarefa para a anterior.
O código a seguir é, até certo ponto, uma tábua de pão e não finge ser elegante; escrevemos as primeiras opções de consulta na empresa com ênfase na flexibilidade e permutabilidade de blocos
with dates as ( select '2018-01-31'::date as dt1, '2018-05-31'::date as dt2 ), g_age as ( select age( (select dt2 from dates), (select dt1 from dates)) ), months as ( select (extract(year from (select * from g_age))*12 + extract(month from (select * from g_age))+1)::integer ), seq as( select ((select dt1 from dates) + make_interval(0, gs)) as gs from generate_series ( 0, (select * from months), 1 ) as gs where ((select dt1 from dates) + make_interval(0, gs)) <= (select dt2 from dates) ) select * from seq
gs |
---|
31/01/2018 |
28/02/2018 |
31/03/2018 |
30/04/2018 |
31/05/2018 |
A solução acabou sendo bastante complicada, mas funcionando e é bastante simples integrá-la a outras solicitações por meio do mecanismo with.
Implementamos o relatório, mas a ideia de que essa solicitação não é apenas complicada, mas também é limitada em seu uso apenas por etapas ao longo de meses inteiros, não deu descanso.
Opção 2
Depois de um tempo, percebi que a geração seqüencial de datas é essencialmente um procedimento recursivo. Somente não em sua forma pura, pois, no nosso caso, o cálculo da data seguinte à anterior leva ao problema original. Mas a cada passo, podemos aumentar o intervalo adicionado ao início do nosso período:
with recursive dates as ( select '2018-01-31'::timestamp as dt1, '2018-05-31'::timestamp as dt2, interval '1 month' as interval ), pr AS( select 1 as i, (select dt1 from dates) as dt union select i+1 as i, ( (select dt1 from dates) + ( select interval from dates)*i)::timestamp as dt from pr where ( ((select dt1 from dates) + (select interval from dates)*i)::timestamp) <=(select dt2 from dates) ) select dt as gs from pr;
gs |
---|
31/01/2018 |
28/02/2018 |
31/03/2018 |
30/04/2018 |
31/05/2018 |
Esta consulta funciona corretamente com todos os períodos e intervalos de entrada.