Running total over date range - fill in the missing dates - mysql

I have the following table.
DATE | AMT
10/10 | 300
12/10 | 300
01/11 | 200
03/11 | 100
How do I get the monthly total? A result like -
DATE | TOT
1010 | 300
1110 | 300
1210 | 600
0111 | 800
0211 | 800
0311 | 900
A sql statement like
SELECT SUM(AMT) FROM TABLE1 WHERE DATE BETWEEN '1010' AND '0111'
would result in the 800 for 0111 but...
NOTE There is not a date restriction. which is my dilemma. How do I populate this column without doing a loop for all dates and have the missing months displayed as well?

To cater for missing months, create a template table to join against.
Think of it as caching. Rather than looping through and filling gaps, just have a calendar cached in your database.
You can even combine multiple calendars (start of month, start of week, bank holidays, working day, etc) all into one table, with a bunch of search flags and indexes.
You end up with something like...
SELECT
calendar.date,
SUM(data.amt)
FROM
calendar
LEFT JOIN
data
ON data.date >= calendar.date
AND data.date < calendar.date + INTERVAL 1 MONTH
WHERE
calendar.date >= '20110101'
AND calendar.date < '20120101'
GROUP BY
calendar.date
EDIT
I just noticed that the OP wants a running total.
This -is- possible in SQL but it is extremely inefficient. The reason being that the result from one month isn't used to calculate the following month. Instead the whole running-total has to be calculated again.
For this reason It is normally strongly recommended that you calculate the monthly total as above, then use your application to itterate through and make the running total values.
If you really must do it in SQL, it would be something like...
SELECT
calendar.date,
SUM(data.amt)
FROM
calendar
LEFT JOIN
data
ON data.date >= #yourFirstDate
AND data.date < calendar.date + INTERVAL 1 MONTH
WHERE
calendar.date >= #yourFirstDate
AND calendar.date < #yourLastDate
GROUP BY
calendar.date

the main problem is the and have the missing months displayed as well?
I don't see how to do it with out an aux table containing the combination of month\year to be displayed:
create table table1(
date datetime,
amt int
)
insert into table1 values ('10/10/2010',100)
insert into table1 values ('12/12/2010',200)
insert into table1 values ('01/01/2011',50)
insert into table1 values ('03/03/2011',500)
truncate table #dates
create table #dates(
_month int,
_year int
)
insert into #dates values(10,2010)
insert into #dates values(11,2010) --missing month
insert into #dates values(12,2010)
insert into #dates values(01,2011)
insert into #dates values(02,2011)--missing month
insert into #dates values(03,2011)
select D._month, D._year, sum(amt)
from #dates D left join TABLE1 T on D._month=month(T.date) and D._year=year(T.date)
group by D._month, D._year

You can also generate a range on the fly, pass its value as the interval to DATE_ADD, and basically project a sequence of month values.
As #Dems said, you need to have a correlated subquery calculate the running total, which will be very inefficient, because it will run a nested loop internally.
To see how to generate the sequence, check my post here:
How to generate a range of numbers in Mysql
The end query should look something like this: (Incidentally, you should have a date column, not this varchar mess).
/*NOTE: This assumes a derived table (inline view) containing the sequence of date values and their corresponding TOT value*/
SELECT
DATEVALUES.DateValue,
(
SELECT SUM(TABLE1.AMT) FROM TABLE1 WHERE TABLE1.DateValue <= DATEVALUES.DateValue)
) AS RunningSubTotal
FROM
DATEVALUES
Or something like that.

select sum(AMT) from TABLE1 group by Date

Related

How to select either one row or the other

I have a table with columns VAT, start and end date.
I have two rows. The standard entry has 0000-00-00 as the start and end date and the other row has the start_date 2020-06-01 and the end_date 2020-12-31
I want VAT of the second row to be selected if today's date is between the start and end date, otherwise the standard VAT with 0000-00-00 should be selected
This is my table:
I tried
SELECT *
FROM taxes
WHERE (CASE WHEN start_date < "2020-06-06"
AND end_date > "2020-06-06" THEN 1
ELSE 0
END) = 1
But i don't know how to formulate the else case or whether it can work at all like this
You can use order by and limit for this:
select t.*
from taxes t
where start_date = '0000-00-00' or
'2020-06-06' between start_date and end_date
order by start_date desc
limit 1;
The idea is that the first condition gets the "default" value. The second condition gets the matching condition. These two rows are then sorted, so the matching condition will be first -- if there is one.
There might be ways o doing it with your suggested "0000-00-00' dates for start and end points, but in my view you run a much cleaner ship if you address the time spans individually, i. e. spell out the date ranges for before and after the "exception period", like:
INSERT INTO vat (startdt,enddt,fullrate,reducedrate)
VALUES ('2000-01-01','2020-06-30',.19,.07), -- before
('2020-07-01','2020-12-31',.16,.05), -- exception period
('2021-01-01','2500-12-31',.19,.07); -- after
select * from vat where now() between startdt and enddt;
This way you document in a very clear way which rates were applicable when. And the query itself becomes trivial, see above and check out my demo here: https://rextester.com/YLYUU53617
SELECT *
FROM taxes
WHERE tax_id=IF(start_date < "2020-06-06" AND end_date > "2020-06-06", 1, 0)
you can find the records for current date, then combine this set with the source table filtered by '0000-00-00' excluding country codes from this set
with
current_taxes as (
select *
from taxes
where current_date between start_date and end_date
)
select *
from current_taxes
union all
select *
from taxes
left join current_taxes
using (country_code)
where taxes.start_date='0000-00-00'
and current_taxes.country_code is null
;

MySQL: Selecting In Between DateTime records

I want to get the count of records between two date-time entries.
I have a column in my table named created_date with the datetime data type.
I want to select rows which were created between 2017-01-10 and 2017-01-30
I have written the following query but it doesn't seem to be inclusive
SELECT* FROM table WHERE created_date BETWEEN '2017-01-10' AND '2017-01-30'
The issue you are having has to do with that the date literal 2017-01-31 represents that date at midnight. To get around this, phrase your query as follows:
SELECT * FROM table WHERE created_date >= '2017-01-10' AND created_date < '2017-01-31';
This says to take any date on or after the very start of 2017-01-10 and before the start of 2017-01-31. This implies including the entire day 2017-01-30.
For get count of records between two date you can try below query
SELECT COUNT(*)
FROM tableName
WHERE date(created_date) >= '2017-01-10' AND date(created_date) <= '2017-01-30'
Try This One
SELECT COUNT(1)
FROM tableName
WHERE Cast(created_date as date) Between Cast('2017-01-10' as date) AND Cast('2017-01-30' as date)

group by date, even if there is no entry for the date

I want to visualize my entries by counting how many have been created at the same day.
SELECT dayname(created_at), count(*) FROM logs
group by day(created_at)
ORDER BY created_at desc
LIMIT 7
So I get something like:
Thursday 4
Wednesday 12
Monday 4
Sunday 1
Saturday 20
Friday 23
Thursday 10
But I also want to have the Tuesday in there with 0 so I have it for one week.
Is there a way to do this with full mysql or do I need to update the result before I can give it to the chart?
EDIT:
This is the final query:
SELECT
DAYNAME(date_add(NOW(), interval days.id day)) AS day,
count(logs.id) AS amount
FROM days LEFT OUTER JOIN
(SELECT *
FROM logs
WHERE TIMESTAMPDIFF(DAY,DATE(created_at),now()) < 7) logs
on datediff(created_at, NOW()) = days.id
GROUP BY days.id
ORDER BY days.id desc;
The table days includes numbers from 0 to -6
You only need a table of offsets which could be a real table or something built on the fly like select 0 ofs union all select -1 ....
create table days (ofs int);
insert into days (ofs) values
(0), (-1), (-2), (-3),
(-4), (-5), (-6), (-7);
select
date_add('20160121', interval days.ofs day) as created_at,
count(data.id) as cnt
from days left outer join logs data
on datediff(data.created_at, '20160121') = days.ofs
group by days.ofs
order by days.ofs;
http://sqlfiddle.com/#!9/3e6bc7/1
For performance it would probably be better to limit the search in the data (logs) table:
select
date_add('20160121', interval days.ofs day) as created_at,
count(data.id) as cnt
from days left outer join
(select * from logs where created_at between <start> and <end>) data
on datediff(data.created_at, '20160121') = days.offset
group by days.offset
order by days.offset;
One downside is that you do have to parameterize this with a fixed anchor date in a couple of expressions. It might be better to have a table of real dates sitting in a table somewhere so you don't have to do the calculations.
Use RIGHT JOIN to a dates table, so you can request data for each and all days, no matter if some days have data or not, simply, mull days will show as CERO or NULL.
You can create a dates table, some sort of calendar table.
id_day | day_date |
--------------------
1 | 2016-01-01 |
2 | 2016-01-02 |
.
.
365 | 2016-12-31 |
With this table, you can relate date, then extract day, month, week, whatever you want with MYSQL DATE AND TIME FUNCTIONS
SELECT t2.dayname(day_date), count(t1.created_at) FROM logs t1 right join dates_table t2 on t1.created_at=t2.day_date group by t2.day_date ORDER BY t1.created_at desc LIMIT 7

Cumulative SQL to work out no of payers

Currently trying to create a query that shows how many accounts have paid month on month but on a cumulative basis (penetration). So as an example I have a table with Month paid and account number, which shows what month that account paid.
Month | AccountNo
Jan-14 | 123456
Feb-14 | 321654
So using the above the result set would show
Month | Payers
Jan-14 | 1
Feb-14 | 2
being because one account paid in Jan, then one in Feb meaning that there have been by the end of Feb 2 payments overall, but only one in Jan. Tried a few inner joins back onto the table itself with a t1.Month >= t2.Month as i would for a normal cumulative query but the result is always out.
Any questions please ask, unsure if the above will be clear to anyone but me.
If you have date in the table then you can try the following query.
SELECT [Month]
,(SELECT COUNT(AccountNo)
FROM theTable i
-- This is to make sure to add until the last day of the current month.
WHERE i.[Date] <= DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,o.[Date])+1,0)) AS CumulativeCount
FROM theTable o
Ok, several things. You need to have an actual date field, as you can't order by the month column you have.
You need to consider there may be gaps in the months - i.e. some months where there is no payment (not sure if that is true or not)
I'd recommend a recursive common table expression to do the actual aggregation
Heres how it works out:
-- setup
DECLARE #t TABLE ([Month] NCHAR(6), AccountNo INT)
INSERT #t ( [Month], AccountNo )
VALUES ( 'Jan-14',123456),('Feb-14',456789),('Apr-14',567890)
-- assume no payments in march
; WITH
t2 AS -- get a date column we can sort on
(
SELECT [Month],
CONVERT(DATETIME, '01 ' + REPLACE([Month], '-',' '), 6) AS MonthStart,
AccountNo
FROM #t
),
t3 AS -- group by to get the number of payments in each month
(
SELECT [Month], MonthStart, COUNT(1) AS PaymentCount FROM t2
GROUP BY t2.[Month], t2.MonthStart
),
t4 AS -- get a row number column to order by (accounting for gaps)
(
SELECT [Month], MonthStart, PaymentCount,
ROW_NUMBER() OVER (ORDER BY MonthStart) AS rn FROM t3
),
t5 AS -- recursive common table expression to aggregate subsequent rows
(
SELECT [Month], MonthStart, PaymentCount AS CumulativePaymentCount, rn
FROM t4 WHERE rn = 1
UNION ALL
SELECT t4.[Month], t4.MonthStart,
t4.PaymentCount + t5.CumulativePaymentCount AS CumulativePaymentCount, t4.rn
FROM t5 JOIN t4 ON t5.rn + 1 = t4.rn
)
SELECT [Month], CumulativePaymentCount FROM t5 -- select desired results
and the results...
Month CumulativePaymentCount
Jan-14 1
Feb-14 2
Apr-14 3
If your month column is date type then its easy to work on else you need some additional conversion for it. Here the query goes...
create table example (
MONTHS datetime,
AccountNo INT
)
GO
insert into example values ('01/Jan/2009',300345)
insert into example values ('01/Feb/2009',300346)
insert into example values ('01/Feb/2009',300347)
insert into example values ('01/Mar/2009',300348)
insert into example values ('01/Feb/2009',300349)
insert into example values ('01/Mar/2009',300350)
SELECT distinct datepart (m,months),
(SELECT count(accountno)
FROM example b
WHERE datepart (m,b.MONTHS) <= datepart (m,a.MONTHS)) AS Total FROM example a

How to get values for every day in a month

Data:
values date
14 1.1.2010
20 1.1.2010
10 2.1.2010
7 4.1.2010
...
sample query about january 2010 should get 31 rows. One for every day. And values vould be added. Right now I could do this with 31 queries but I would like this to work with one. Is it possible?
results:
1. 34
2. 10
3. 0
4. 7
...
This is actually surprisingly difficult to do in SQL. One way to do it is to have a long select statement with UNION ALLs to generate the numbers from 1 to 31. This demonstrates the principle but I stopped at 4 for clarity:
SELECT MonthDate.Date, COALESCE(SUM(`values`), 0) AS Total
FROM (
SELECT 1 AS Date UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
--
SELECT 28 UNION ALL
SELECT 29 UNION ALL
SELECT 30 UNION ALL
SELECT 31) AS MonthDate
LEFT JOIN Table1 AS T1
ON MonthDate.Date = DAY(T1.Date)
AND MONTH(T1.Date) = 1 AND YEAR(T1.Date) = 2010
WHERE MonthDate.Date <= DAY(LAST_DAY('2010-01-01'))
GROUP BY MonthDate.Date
It might be better to use a table to store these values and join with it instead.
Result:
1, 34
2, 10
3, 0
4, 7
Given that for some dates you have no data, you'll need to fill in the gaps. One approach to this is to have a calendar table prefilled with all dates you need, and join against that.
If you want the results to show day numbers as you have showing in your question, you could prepopulate these in your calendar too as labels.
You would join your data table date field to the date field of the calendar table, group by that field, and sum values. You might want to specify limits for the range of dates covered.
So you might have:
CREATE TABLE Calendar (
label varchar,
cal_date date,
primary key ( cal_date )
)
Query:
SELECT
c.label,
SUM( d.values )
FROM
Calendar c
JOIN
Data_table d
ON d.date_field = c.cal_date
WHERE
c.cal_date BETWEEN '2010-01-01' AND '2010-01-31'
GROUP BY
d.date_field
ORDER BY
d.date_field
Update:
I see you have datetimes rather than dates. You could just use the MySQL DATE() function in the join, but that would probably not be optimal. Another approach would be to have start and end times in the Calendar table defining a 'time bucket' for each day.
This works for me... Its a modification of a query I found on another site. The "INTERVAL 1 MONTH" clause ensures I get the current month data, including zeros for days that have no hits. Change this to "INTERVAL 2 MONTH" to get last months data, etc.
I have a table called "payload" with a column "timestamp" - Im then joining the timestamp column on to the dynamically generated dates, casting it so that the dates match in the ON clause.
SELECT `calendarday`,COUNT(P.`timestamp`) AS `cnt` FROM
(SELECT #tmpdate := DATE_ADD(#tmpdate, INTERVAL 1 DAY) `calendarday`
FROM (SELECT #tmpdate :=
LAST_DAY(DATE_SUB(CURDATE(),INTERVAL 1 MONTH)))
AS `dynamic`, `payload`) AS `calendar`
LEFT JOIN `payload` P ON DATE(P.`timestamp`) = `calendarday`
GROUP BY `calendarday`
To dynamically get the dates within a date range using SQL you can do this (example in mysql):
Create a table to hold the numbers 0 through 9.
CREATE TABLE ints ( i tinyint(4) );
insert into ints (i)
values (0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
Run a query like so:
select ((curdate() - interval 2 year) + interval (t.i * 100 + u.i * 10 + v.i) day) AS Date
from
ints t
join ints u
join ints v
having Date between '2015-01-01' and '2015-05-01'
order by t.i, u.i, v.i
This will generate all dates between Jan 1, 2015 and May 1, 2015.
Output
2015-01-01
2015-01-02
2015-01-03
2015-01-04
2015-01-05
2015-01-06
...
2015-05-01
The query joins the table ints 3 times and gets an incrementing number (0 through 999). It then adds this number as a day interval starting from a certain date, in this case a date 2 years ago. Any date range from 2 years ago and 1,000 days ahead can be obtained with the example above.
To generate a query that generates dates for more than 1,000 days simply join the ints table once more to allow for up to 10,000 days of range, and so forth.
If I'm understanding the rather vague question correctly, you want to know the number of records for each date within a month. If that's true, here's how you can do it:
SELECT COUNT(value_column) FROM table WHERE date_column LIKE '2010-01-%' GROUP BY date_column