Reputation: 1476
I need to query data with count and sum by multiple date ranges and I am looking for a faster query than what I am doing now.
I have a transaction table with a date and amount. I need to present a table with a count of transactions and total amount by date ranges of today, yesterday, this week, last week, this month, last month. Currently I am doing sub queries, is there a better way?
select
(select count(date) from transactions where date between ({{today}})) as count_today,
(select sum(amount) from transactions where date between ({{today}})) as amount_today,
(select count(date) from transactions where date between ({{yesterday}})) as count_yesterday,
(select sum(amount) from transactions where date between ({{yesterday}})) as amount_yesterday,
(select count(date) from transactions where date between ({{thisweek}})) as count_thisweek,
(select sum(amount) from transactions where date between ({{thisweek}})) as amount_thisweek,
etc...
Is there a better way?
Upvotes: 0
Views: 1150
Reputation: 48179
although you have a marked solution, I have another that will probably simplify your query even further using MySQL variables so you don't have to mis-type / calculate dates and such...
Instead of declaring variables up front, you can do them inline as a select statement, then use them as if they were columns in another table. Since it is created as a single row, there is no Cartesian result. First the query, then I'll describe the computations on it.
select
sum( if( t.date >= @today AND t.date < @tomorrow, 1, 0 )) as TodayCnt,
sum( if( t.date >= @today AND t.date < @tomorrow, amount, 0 )) as TodayAmt,
sum( if( t.date >= @yesterday AND t.date < @today, 1, 0 )) as YesterdayCnt,
sum( if( t.date >= @yesterday AND t.date < @today, amount, 0 )) as YesterdayAmt,
sum( if( t.date >= @FirstOfWeek AND t.date < @EndOfWeek, 1, 0 )) as WeekCnt,
sum( if( t.date >= @FirstOfWeek AND t.date < @EndOfWeek, amount, 0 )) as WeekAmt
from
transations t,
( select @today := curdate(),
@yesterday := date_add( @today, interval -1 day ),
@tomorrow := date_add( @today, interval 1 day ),
@FirstOfWeek := date_add( @today, interval +1 - dayofweek( @today) day ),
@EndOfWeek := date_add( @FirstOfWeek, interval 7 day ),
@minDate := least( @yesterday, @FirstOfWeek ) ) sqlvars
where
t.date >= @minDate
AND t.date < @EndOfWeek
Now, the dates. Since the @variables are prepared in sequence, you can think of it as an inline program to set the variables. Since they are a pre-query, they are done first and available for the duration of the rest of the query as previously stated. So to start, I am working with whatever "curdate()" is which gets the date portion only without respect to time. From that, subtract 1 day (add -1) to get the beginning of yesterday. Add 1 day to get Tomorrow. Then, the first of the week is whatever the current date is +1 - the actual day of week (you will see shortly). Add 7 days from the first of the week to get the end of the week. Finally, get whichever date is the LEAST between a yesterday (which COULD exist at the end of the prior week), OR the beginning of the week.
Now look at today for example... Feb 23rd.
Sun Mon Tue Wed Thu Fri Sat Sun
21 22 23 24 25 26 27 28
Today = 23
Yesterday = 22
Tomorrow = 24
First of week = 23 + 1 = 24 - 3rd day of week = 21st
End of Week = 21st + 7 days = 28th.
Why am I doing a cutoff of the dates stripping times? To simplify the SUM() condition for >= AND <. If I stated some date = today, what if your transactions were time-stamped. Then you would have to extract the date portion only to qualify. By this approach, I can say that "Today" count and amount is any date >= Feb 23 at 12am midnight AND < Feb 24th 12 am midnight. This is all time inclusive Feb 23rd up to 11:59:59pm hence LESS than Feb 24th (tomorrow).
Similar consideration for yesterday is all inclusive UP TO but not including whatever "today" is. Similarly for the week range.
Finally the WHERE clause is looking for the earliest date as the range so it does not have to run through the entire database of transactions to the end.
Lastly, if you ever wanted the counts and totals for a prior week / period, whatever, you could just extrapolate and change
@today := '2015-01-24'
and the computations will be AS IF the query was run ON THAT DATE.
Similar if you cared to alter such as for a month, you could compute the first of the month to the first of a following month for MONTHLY totals.
Hope you enjoy this flexible solution to you.
Upvotes: 2
Reputation: 15961
Yes, you can use aggregate functions on conditional expressions, like so:
SELECT SUM(IF(date between ({{today}})), 1, 0) AS count_today
, SUM(IF(date between ({{today}})), amount, 0) AS amount_today
, ...
Upvotes: 1