Home > Back-end >  Get value for every date in table with date ranges
Get value for every date in table with date ranges

Time:11-11

I'm working with table, its structure looks like this:

  • from_date DATE %Y-%m-%d
  • to_date DATE %Y-%m-%d
  • person VARCHAR
  • tax INT

Table shows that in period between date_from and date_to for such person there was such tax

The whole point is that these periods can intersect and activities can sum. Roughly speaking, here is an example of a table:

date_from date_to person tax
2020-01-30 2020-02-02 person1 20
2020-01-30 2020-01-31 person1 10
2020-02-01 2020-02-03 person1 15

And I need a table with structure:

  • date DATE %Y-%m-%d
  • person VARCHAR
  • tax INT

where I get tax aggregated for every date for every person and it will be look like this

date person tax
2020-01-30 person1 30
2020-01-31 person1 30
2020-02-01 person1 35
2020-02-02 person1 35
2020-02-03 person1 15

Hope I was able to explain in an understandable way what I want.

Below the code I tried. But recursive approach does not solve the problem because the table has about 5 millions rows

WITH cte AS (
 SELECT person, date_from, date_to, tax, date_from AS report_date
 FROM my_table
 WHERE date_from >= @date_from AND date_to < @date_to

 UNION ALL

 SELECT person, date_from, date_to, tax, DATEADD(DAY, 1, report_date) AS report_date
 FROM cte
 WHERE report_date < date_to
)

SELECT person, report_date, SUM(tax) tax
FROM cte
GROUP BY person, report_date

CodePudding user response:

You need a list of dates consisting of all start date, end date and end date 1 in your table. Then calculate running sum such that:

  • On start date the tax is added to sum
  • On end date the tax remains unchanged
  • On end date 1 the tax is removed from sum

A group by is needed to compact rows having same date into one row.

WITH cte AS (
    SELECT person, ca.*
    FROM t
    CROSS APPLY (VALUES
        (date_from, tax),
        (date_to, 0),
        (DATEADD(DAY, 1, date_to), -tax)
    ) AS ca(date, value)
)
SELECT person, date, SUM(SUM(value)) OVER (
    PARTITION BY person
    ORDER BY date
) AS tax_value
FROM cte
GROUP BY person, date
ORDER BY person, date

And the result:

person date tax_value
person1 2020-01-30 30
person1 2020-01-31 30
person1 2020-02-01 35
person1 2020-02-02 35
person1 2020-02-03 15
person1 2020-02-04 0

CodePudding user response:

emm, you can just group by from_date and person. like

select from_date, 
person,
sum(tax) as tax
from table
group by from_date, person

CodePudding user response:

May be not best solution. But below query should work

Declare @FromDate date,@ToDate date;

select @FromDate=min(date_from) from YourTable

select @ToDate=max(date_to) from YourTable

Create table #temp ([Date] date,person varchar(50), tax bigint)

WHILE (@FromDate <= @ToDate)
BEGIN

insert into #temp
select 
@FromDate,
person,
(select Sum(tax) from YourTable where @FromDate between date_from AND date_to)
from YourTable where @FromDate between date_from AND date_to

group by person


set @FromDate=DATEADD(DAY,1,@FromDate)

END


select * from #temp

drop table #temp

CodePudding user response:

Here is an approach that doesn't create all days for all persons (i.e. several rows per row in the table). It merely creates all days, then aggregates the table twice, once to get the amounts that get added a day and once those that get subtracted. Then it's outer joining these sums to the days and get the running total.

This is based on Salman A's idea hence to work with a running total.

with days (day, max_day) as
(
  select min(date_from), max(date_to) from my_table
  union all
  select dateadd(day, 1, day), max_day from days
  where day < max_day
)
, starters (day, total) as
(
  select date_from, sum(tax) from my_table group by date_from
)
, enders (day, total) as
(
  select date_to, sum(tax) from my_table group by date_to
)
select
  days.day,
  sum(coalesce(starters.total, 0) - coalesce(enders.total, 0))
    over (order by days.day) as tax
from days
left join starters on starters.day = days.day
left join enders on dateadd(day, 1, enders.day) = days.day
order by days.day;

If you want to limit this to a date range then:

select *
from (above select)
where day >= @date_from and day < @date_to
order by day

Demo with slighty changed sample data to have date gaps in the data: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=4d64c881b3026abb5f5ab23aa09fc259

  • Related