totals by month even for if there is a missing month - php

just a quick one, hopefully....
i am after getting some totals (sales value) by month from only a single table.
The problem i have is:
If there are no sales for a month, the month is of course not being returned in the results. Is there a way i can do this in a single query so if there were no sales in i.e "January 2015" the result would return "0.00 - January - 2015"
The basic SQL i currently have is:
SELECT SUM(p.PaymentAmount) AS Total, MONTHNAME(p.PaymentDate) AS Month, YEAR(p.PaymentDate) AS Year
FROM tPayment p
WHERE p.PaymentType = 2
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
i cant think of how to do this without selecting the date range in php and then querying each month and year... this just seems messy... so i would like to know if i can do this in a single query.
Any help is much appreciated!

you should create yourself a separate table containing at dates such as
CREATE TABLE `dates` (
`uid` INT NOT NULL AUTO_INCREMENT,
`datestamp` DATE NOT NULL,
PRIMARY KEY (`uid`))
ENGINE = InnoDB;
and fill it
INSERT INTO dates (datestamp)
SELECT ADDDATE('2015-01-01', INTERVAL SomeNumber DAY)#set start date
FROM (SELECT a.i+b.i*10+c.i*100+d.i*1000 AS SomeNumber
FROM integers a, integers b, integers c, integers d) Sub1
WHERE SomeNumber BETWEEN 0 AND (365 * 3)#3 years
then you can join against it
SELECT SUM(p.PaymentAmount) AS Total, MONTHNAME(p.PaymentDate) AS Month, YEAR(p.PaymentDate) AS Year
FROM tPayment p
LEFT OUTER JOIN dates d
ON d.datestamp = CAST(p.PaymentDate AS DATE)
WHERE p.PaymentType = 2
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
ORDER BY d.datestamp DESC;
regardless of if I fatfingered the queries here, the concept should hold up for you

This wouldn't be my first choice method for accomplishing this task, but for the sake of providing multiple alternatives I offer this if you're trying to keep it all in MySQL and avoid creating an additional table.
SELECT
SUM(p.PaymentAmount) AS Total,
MONTHNAME(p.PaymentDate) AS Month,
YEAR(p.PaymentDate) AS Year
FROM ( SELECT 1 AS m UNION ALL
SELECT 2 UNION ALL
SELECT 3 UNION ALL
SELECT 4 UNION ALL
SELECT 5 UNION ALL
SELECT 6 UNION ALL
SELECT 7 UNION ALL
SELECT 8 UNION ALL
SELECT 9 UNION ALL
SELECT 10 UNION ALL
SELECT 11 UNION ALL
SELECT 12
) AS months
OUTER JOIN tPayment p
ON MONTH(p.PaymentDate) = months.m
WHERE p.PaymentType = 2
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
I think it'd be easier to check against it in some quick PHP code after you run the query.
As suggested by Jon B Creating a months table and joining against that would shorten and clean the query up quite a bit. If you're trying to keep it all in your MySQL query I personally would choose his method.

If you have data in your table for all months -- but the where clause is filtering out all the rows from one or more months -- you can try conditional aggregation:
SELECT SUM(CASE WHEN p.PaymentType = 2 THEN p.PaymentAmount ELSE 0 END) AS Total,
MONTHNAME(p.PaymentDate) AS Month, YEAR(p.PaymentDate) AS Year
FROM tPayment p
GROUP BY YEAR(p.PaymentDate), MONTH(p.PaymentDate)
This isn't guaranteed to work (it depends on the data). But if it does, it is the simplest way to solve this problem.

Related

SQL for calculating the sum in the past 24 months and above

I want to calculate the sum and average of my data in the past 24 months how can I achieve that? In my database I have 1000 records with the field (ID, Store_ID, Date, Sales) I want to calculate the average of all my data from THIS month up to the last 24 months.
I believe you are looking for the DATEADD function. Here is the documentation.
Essentially, you will use a where condition to check if a date is older than a certain date.
By using the DATEADD function with the current time, you can create a date X months in the past.
SELECT columns FROM table WHERE date < DATEADD(month, numberOfMonths, GETDATE());
For SQL querys that use aggregate functions (such as SUM), depending on how you use the function you may require the HAVING clause; instead of, the WHERE clause. I don't think your case will require this; but, I may be wrong.
You can read more on HAVING vs WHERE at this article.
Here is an example:
SELECT columns FROM table HAVING date < DATEADD(month, numberOfMonths, GETDATE());
The code samples are untested; however, the general structure is there. We are also using the GETDATE() function to get the current database date.
Please find below SQL where I am getting last 12 month sum of members for a given group in X table.
You can use the similar approach with some modification to below code snippet as per actual table structure to find out desired result set.
;with tbl1
as
(
select MonthID,count(MemberID) as RecCount from MemberTable a with (nolock) group by MonthID
)
,tbl2
as
(
select
ROW_NUMBER() OVER(order by MonthID) as RowID
,MonthID
,sum(RecCount) over(order by MonthID ROWS BETWEEN 11 PRECEDING and CURRENT ROW) as [SumLast12Months]
from tbl1
)
select * from tbl2 where RowID>=12 -- Before RowID 12 none of the month has complete 12 month rollup
Here are a couple of other options:
select sum(Sales) as [Total Sales], avg(Sales) as [Average Sales]
from (select * from #tbl where dt >= dateadd(mm, -24, getdate())) as foo
or
with CTE
as
(
select * from #tbl where dt >= dateadd(mm, -24, getdate())
)
select sum(Sales) as [Total Sales], avg(Sales) as [Average Sales] from CTE

Mysql find dates using where in clause, or dates nearest to those in array

I have the following code and query:
//$month is an array of datetime objects
foreach($month as $key => $indMonth){
$formattedMonth[] = $month[$key]->format('Y-m-d');
}
$formattedMonths = implode("','",$formattedMonth);
$query = "SELECT id,date FROM table WHERE date in ('$formattedMonths') ORDER by date DESC";
The database holds dates for the past 450 days, but it is imperfect and there are some missing days. The point of the script is to retrieve data from the current day of the month and then the corresponding day on the five previous months, but I need a failsafe for when a date happens to be missing.
How can I modify this query so it picks either the date in the "where in" portion of the query or it finds the date nearest to that particular date in the array?
Is this best to do in the query, or am I better off returning a more complete data set, then using PHP to find out if the date I want is available?
MySQL offers some decent date arithmetic. For example, if you have the date '2015-11-10' (10-Nov-2015) you can get the same day three months prior with this expression:
'2015-11-10` - INTERVAL 3 MONTH
That will kick back '2015-08-10', which is what you want.
This date arithmetic works predictably even with longer and shorter months, and with leap years. For example,
'2015-03-31' - INTERVAL 1 MONTH, '2016-03-31' - INTERVAL 1 MONTH
gives back '2015-02-28', '2016-02-29' as you might expect. And
'2015-03-31' - INTERVAL 2 MONTH, '2016-03-31' - INTERVAL 2 MONTH
gives back '2015-03-31', '2016-03-31'. Perfect.
Now, only you can decide whether this predictable behavior is correct for your application: only you know what you want to do with the previous five months of data, when the day in question is near the end of the month.
Let's assume it's correct and move on. Here is a subquery that can be used to generate a sequence of six dates, one day per month ending today.
SELECT CURDATE() - INTERVAL seq.seq MONTH day_of_month
FROM ( SELECT 0 AS seq UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL
SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 6) seq
We can use this little query as a subquery, and LEFT JOIN it to your data. That would work like this:
SELECT id, day_of_month
FROM (
SELECT CURDATE() - INTERVAL seq.seq MONTH day_of_month
FROM ( SELECT 0 AS seq UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL
SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 6) seq
) days
LEFT JOIN table ON table.date = days.day_of_month
This is a cool way to do it because you'll always get at least one row in the resultset for each date in the list, even if there's nothing matching in table.
The closest date gets a little hairier. It's possible to write a query like that. But MySQL lacks a WITH clause so the query is ridiculously repetitive.

PHP/MySQL Select the day before and day after a range from the same table

I have two tables, one called check_ins and another called holidays.
check_ins has a datetime_start and datetime_end columns (in addition to other stuff that isn't needed for this question). The holidays table has a date range of two columns for the start and end of the holiday.
I need to figure out who was in the day prior to and the day directly after the holiday range to determine who gets holiday pay. In other words, I need only results from the table that the same employee was in one day before and one day after, ignoring the rest.
I've been racking my brain all day trying to figure out a way to do this and have found nothing. Am I barking up the wrong tree here? Should I do this via PHP?
Thanks!
Edit: this is what I used and though I had it until I realized that their might be multiple check-ins in a single day:
SELECT DISTINCT count(check_ins.Employee_ID), check_ins.ShiftStart_Datetime, check_ins.ShiftEnd_Datetime, holidays.* FROM check_ins, holidays WHERE holidays.ID = 2 AND DATE(DATE_ADD(Datefrom, INTERVAL -1 DAY)) = DATE(ShiftStart_Datetime) GROUP BY Employee_ID HAVING count(check_ins.Employee_ID) >1 UNION SELECT DISTINCT check_ins.Employee_ID, check_ins.ShiftStart_Datetime, check_ins.ShiftEnd_Datetime, holidays.* FROM check_ins, holidays WHERE DATE(DATE_SUB(Dateto, INTERVAL -1 DAY)) = DATE(ShiftStart_Datetime) GROUP BY Employee_ID HAVING count(check_ins.Employee_ID) >1
You can (inner) join the check_ins table twice. Once for the day before the start of the holiday and once for the day after.
If the datetime_start and datetime_end may have different dates, you need to use BETWEEN. Cast both of them to a date instead of a datetime, since you don't care about the time.
Add a GROUP BY in case the employee has multiple check ins on one day.
SELECT holidays.id AS holiday_id, ci_before.employee_id FROM holidays
INNER JOIN check_ins ci_before ON holidays.holiday_start - INTERVAL 1 DAY
BETWEEN DATE(ci_before.datetime_start) AND DATE(ci_before.datetime_end)
INNER JOIN check_ins ci_after ON holidays.holiday_end + INTERVAL 1 DAY
BETWEEN DATE(ci_after.datetime_start) AND DATE(ci_after.datetime_end)
AND ci_before.employee_id = ci_after.employee_id
GROUP BY ci_before.employee_id
See the SQL fiddle here
The used tables are
CREATE TABLE `holidays` (id int, holiday_start date, holiday_end date);
CREATE TABLE `check_ins` (employee_id int, datetime_start datetime, datetime_end datetime);
holidays.id is an ID for a holiday, not an employee.
you can do it via php in 2 queries, but why would you... this'll be fun!
SELECT * FROM check_ins
LEFT JOIN holidays AS holi_before
ON (
datediff(holi_before.datetime_end,check_ins.datetime_end) <= 1
)
LEFT JOIN holidays AS holi_after
ON (
holi_before.id = holi_after.id
AND datediff(holi_after.datetime_end,check_ins.datetime_end) >= 1
)
That should about do it (might need to tweak the datediff comparison a bit to get it exactly right.)
haven't test it, but i hope it gives you the right push :-)

Getting next available date from database

I have a system that allows users to assign a specific file to a past or present date. The limitations are that they may only upload one file per day per user. When the user goes to upload a file the date field must default to the current date and when that date is not available it will show the first available date in the past in DESC order. Below is the relevent field names.
file_id (INT - INDEX - AUTO INCREMENT)
user_id (INT - may index this)
upload_date (INT - stores date as a unix timestamp)
The only solution I have really found would be to build them all into an array in DESC order by date and loop through until i found an empty slot. However, I feel this could really cause speed issues if the user had the past thousand days filled. I feel like I am overlooking a simple solution.
PLEASE NOTE: For one reason or another they Date is being stored as a Unix timestamp which I understand the downsides on and I am not concerned about correcting at this time.
To get the most recent date that has not been used:
select user_id, max(date) - 1
from (select ud.*,
(select max(date) from upload_date ud2 where ud2.user_id = ud.user_id and ud2.date < ud.date
) as prevdate
from upload_date ud
) ud
where date(from_unixtime(ud.prevdate)) <> date(from_unixtime(ud.date)) - 1 or
ud.prevdate is null
group by user_id
This query first gets the previous date for any given day using a correlated subquery. It then converts the time values to dates and selects any row where the previous date has a gap. The largest of the date minus one is the date you are looking for.
This SQL is untested, so it may have syntax errors.
One way to approach this is with a classic "return missing rows" query. Basically, to get a "missing" row returned from the database, you need a way to generate the "missing" rows.
To build such a query, we can start with:
SELECT MAX(t.upload_date)
FROM mytable t
WHERE t.upload_date <= NOW()
AND t.user = 'someuser'
That gets the initial date, that we are going to work backwards from.
For the "one per day" requirement, you probably want to truncate that upload_date to midnight, at least for this query. For now, we'll assume that the expression in the SELECT list is already truncated, to illustrate the approach, without bogging down in the details of dealing with a unix timestamp.
To generate a descending list of dates, starting with that initial date retrieved by the previous query...
SELECT s.upload_date - INTERVAL n.d DAY AS available_date
FROM ( SELECT MAX(t.upload_date) AS upload_date
FROM mytable t
WHERE t.upload_date <= NOW()
AND t.user = 'someuser'
) s
CROSS
JOIN ( SELECT 0 AS d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
) n
ORDER BY n.d DESC
With that result, we can use an anti-join pattern to find which dates are not already used. This is a LEFT JOIN and a predicate that throws out matching rows:
SELECT s.upload_date - INTERVAL n.d DAY AS available_date
FROM ( SELECT MAX(t.upload_date) AS upload_date
FROM mytable t
WHERE t.upload_date <= NOW()
AND t.user = 'someuser'
) s
CROSS
JOIN ( SELECT 0 AS d UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3
UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
) n
LEFT
JOIN mytable u
ON u.upload_date = s.upload_date - INTERVAL n.d DAY
AND u.user = 'someuser'
WHERE u.upload_date IS NULL
ORDER BY n.d DESC
LIMIT 1
That only looks back 9 days, to get it to look back more days, just extend the inline view aliased as n to return more consecutive integers. (There's some tricks we can play with cross joins to get a whole boatload of integers.)
All that remains is working on the "matching" criteria (which works with the MySQL DATE datatype):
ON u.upload_date = s.upload_date - INTERVAL n.d DAY
into something like this:
ON u.upload_date >= UNIX_TIMESTAMP(FROM_UNIXTIME(s.upload_date)-INTERVAL n.d+1 DAY)
AND u.upload_date < UNIX_TIMESTAMP(FROM_UNIXTIME(s.upload_date)-INTERVAL n.d DAY)
And futzing with the integer timestamp value to get a MySQL DATE out of it...
SELECT DATE(FROM_UNIXTIME(s.upload_date)) - INTERVAL n.d DAY AS available_date

Optimising PHP/mysql algorithm

I have to make some statistics for my application, so I need an algorithm with a performance as best as possible. I have some several question.
I have a data structure like this in the mysql database:
user_id group_id date
1 5 2012-11-20
1 2 2012-11-01
1 4 2012-11-01
1 3 2012-10-15
1 9 2013-01-18
...
So I need to find the group of some user at a specific date. For example, the group of the user 1 at date 2012-11-15 (15 november 2012) should return the most recent group, which is 2 and 4 (many group at the same time) at date 2012-11-01 (the closest and smaller date).
Normally, I could do a Select where date <= chosen date order by date desc, etc... but that's not the point because if I have 1000 users, it will need 1000 requests to have all the result.
So here are some question:
I have already using the php method to loop through the array to avoid the high number of mysql request, but it's still not good because the array size may be 10000+. Using a foreach (or for?) is quite costly.
So my question is if given an array, ordered by date (desc or asc), what's the fastest way to find the closest index of the element which contain a date smaller (or greater) than a given date; beside using a for or foreach loop to loop through each element.
If there is no solution for the first question, then what kind of data structure would you suggest for this kind of problem.
Note: the date is in mysql format, it's not converted in timestamp when you stored it in an array
EDIT: this is a sql fiddle http://sqlfiddle.com/#!2/dc28d/1
For dos_id = 6, t="2012-11-01" it should returns only 2 and 5 at date "2010-12-10 13:16:58"
Not sure why you'd want to do this in php. Here's some SQL using joins instead to get most recent group(s) for all users given a date. Make sure you've got indexes on date and userid.
SELECT *
FROM test t1
LEFT JOIN test t2
ON t1.userid = t2.userid AND t2.thedate <= '2012-11-15' AND t2.thedate > t1.thedate
WHERE t1.thedate <= '2012-11-15' AND t2.userid IS NULL;
SQLfiddle
Or using your SQLFiddle
SELECT t1.*
FROM dossier_dans_groupe t1
LEFT JOIN dossier_dans_groupe t2
ON t1.dos_id = t2.dos_id AND t2.updated_at <= '2012-11-01'
AND t2.updated_at > t1.updated_at
WHERE t1.updated_at <= '2012-11-01' AND t2.dos_id IS NULL;
This would give you a list of all users and their groups (1 row per group) for the latest date that is smaller than the one you specify (2012-11-15 below).
SELECT user_id, group_id, date FROM table WHERE date <= '2012-11-15' AND NOT EXISTS (SELECT 1 FROM table test WHERE test.user_id = table.user_id AND test.date > table.date and test.date <= '2012-11-15')

Categories