PHP MYSQLI count users with two consecutive dates - php

How can I count users with two consecutive absences only? I have to create a chart to show frequency absences by employee.
My table name = incidencias
id | name | dateA | description
1 | al |2017-08-01| absence
2 | al |2017-08-02| absence
3 | alex |2017-08-01| absence
4 | alex |2017-08-02| absence
5 | alex |2017-08-03| absence
6 | al2 |2017-08-01| absence
7 | al2 |2017-08-02| absence
I want the result to be 2, only al and al2 have two consecutive dates where description = absence.
I´m using php to run the query, i did try this code i found but and I tested it in sqlfiddle and works great,. but not in my host.I think this is for PostgreSQL.
$query2 = mysqli_query($conn, "SELECT name,
sum(diff) as days,
(dateA) as work_start,
(dateA) as work_end
FROM (SELECT name,
dateA,
diff
FROM (select name,
dateA,
nvl(dateA- lag(dateA) over (partition by name order by dateA),1) as diff
from incidencias
where description = 'absence'
) t1
where diff = 1
) t2
group by name
having sum(diff) = 2");
$row_cnt = mysqli_num_rows($query2);
printf("Result set has %d rows.\n", $row_cnt);
I would really appreciate it.

So, this is normally done through JOINing on to the same table.
SELECT oinc.*
FROM incidencias oinc
LEFT JOIN
incidencias iinc
ON (oinc.name = iinc.name AND oinc.description = iinc.description)
WHERE description = 'absence'
AND oinc.dateA = DATE_ADD( iinc.dateA, 'INTERVAL 1 DAY');
So, line by line:
SELECT oinc.* -- grab me everything from the oinc table
FROM incidencias oinc -- We're going to call incidencias "oinc" in this query
-- "oinc" is now an alias for "incidencias"
LEFT JOIN -- I want a result whether or not the result is duplicated.
-- (Technically, by including the condition that it not be duplicated
-- this is the same thing as an "INNER JOIN".)
incidencias iinc -- We're looking at the same table, better call it something else
ON (oinc.name = iinc.name AND oinc.description = iinc.description)
-- We're matching the name and the description between the two
-- aliases of the table (oinc, iicn)
WHERE description = 'absence' -- Should be obvious
AND oinc.dateA = DATE_ADD( iinc.dateA, 'INTERVAL 1 DAY'); -- the iinc alias
-- has a date which is one day less than the oinc alias
Some side notes:
I used left join so that you can omit the AND ... later.
You should experiment with moving that AND query from the WHERE into the ON clause. Then you can use an INNER join. You'll get the same results, but knowing both will help you more later.

Here's one way (there may be a simpler solution, but this should be fast anyway)...
SELECT COUNT(*)
FROM
( SELECT name
, MAX(i) i
FROM
( SELECT x.*
, CASE WHEN #prev_name = name THEN
CASE WHEN #prev_date = datea - INTERVAL 1 DAY THEN #i:=#i+1 ELSE #i:=1 END
ELSE #i:=1 END i
, #prev_name := name
, #prev_date := datea
FROM my_table x
, ( SELECT #prev_name:=null,#prev_date:=null, #i:=1) vars
WHERE x.description = 'absence'
ORDER
BY name
, datea
) b
GROUP
BY name
HAVING i = 2
) p;

Related

MYSQL: closest to supplied date grouped by user_id

I need to get multiple rows with a date_added closest to but not past a user supplied date, grouped by user_id.
I've looked at a bunch of max in group type answers but I'm not quite there:
Get nearest records to specific date grouped by type
SQL Query to show nearest date?
Find closest datetime to specified datetime in mysql query
Get closest date from MySQL table
https://dev.mysql.com/doc/refman/5.0/en/example-maximum-column-group-row.html
This is close: https://stackoverflow.com/a/17038667/5319244. Finds the max date though, I need the max specified by user input, not max outright.
Here's a subset of the data with the correct organisation_id, framework_id and level_id already filtered.
+----+---------+-----------------+--------------+----------+---------------------+
| id | user_id | organisation_id | framework_id | level_id | date_added |
+----+---------+-----------------+--------------+----------+---------------------+
| 2 | 1 | 2 | 1 | 1 | 2015-07-31 14:02:49 |
| 9 | 2 | 2 | 1 | 1 | 2015-09-01 11:05:09 |
| 11 | 1 | 2 | 1 | 1 | 2015-09-07 14:13:39 |
+----+---------+-----------------+--------------+----------+---------------------+
If the supplied date is 2015-09-07. I'd expect to see id's: 9 and 11.
If the supplied date is 2015-09-01. I'd expect to see id's: 2 and 9.
If the supplied date is 2015-07-31. I'd expect to see id: 2.
This query is as close as I got:
SELECT t1.id
, t1.user_id
, t1.date_added
FROM
completed_frameworks AS t1
WHERE date_added = (
SELECT
MAX(date_added)
FROM
completed_frameworks
WHERE
user_id = t1.user_id
AND
date_added <= '2015-09-07 23:59:59'
)
AND
(
t1.organisation_id = 2
AND
t1.framework_id = 1
AND
t1.level_id = 1
)
It returns what I expect for the date: 2015-09-07
When the date is 2015-09-01 however it only returns id 9. Not also 2 as I'd expect.
When the date is 2015-07-31 it returns 0 rows..
Let me know if I there's anything else I can provide.
Cheers!
EDIT:
Thanks for the replies thus far. I need to clarify two points:
1) I don't have a limit. I'm expecting those rows due to the user id's. There could be n users returned. I just want a row for each user where the date_added is closest to the user supplied date.
2) The date supplied will not have a time value. It will be from a simple datepicker UI. In my example query I've added the time of 23:59:59 to encompass all of that day.
Try this subquery instead. The organisation_id/framework_id/level_id filter is moved into the subquery and it now returns the right values for the examples you've given.
SELECT t1.id, t1.user_id, t1.date_added
FROM
completed_frameworks AS t1
WHERE date_added = (
SELECT
MAX(date_added)
FROM
completed_frameworks
WHERE
user_id = t1.user_id
AND
date_added <= '2015-09-01 23:59:59'
AND
organisation_id = 2
AND
framework_id = 1
AND
level_id = 1
)
Here is a sample query you could use for an input date of '2015-09-07 14:13:39'. The inner query returns the input date along with the next highest date. This temporary table is then used to filter completed_frameworks to give you only records from the two dates most recent to the input date.
SELECT t1.id, t1.user_id, t1.date_added
FROM completed_frameworks t1
INNER JOIN
(
SELECT cf.date_added
FROM completed_frameworks cf
GROUP BY cf.date_added
HAVING cf.date_added <= '2015-09-07 14:13:39'
ORDER BY cf.date_added DESC
LIMIT 2
) t2
ON t1.date_added = t2.date_added
WHERE t1.organisation_id = 2 AND t1.framework_id = 1 AND t1.level_id = 1
I think this does what you want:
SELECT f.id, f.user_id f.date_added
FROM completed_frameworks f
WHERE date(f.date_added) <= '2025-09-07' -- or whatever your date is
ORDER BY f.date_added DESC
LIMIT 2;
EDIT:
If you want one date per user id that is closest to the specified date, I would suggest:
select f.*
from completed_frameworks f join
(select user_id, max(date_added) as maxda
from completed_frameworks
where date(date_added) <= '2015-09-07'
group by user_id
) u
on f.user_id = u.user_id and f.date_added = maxda

MySQL Select from 3 tables and get attendance In and Out Time for all Members in specific date

I have three table Like this:
members_tbl
id | Fullname | Email | MobileNo
attendance_in_tbl
id | member_id | DateTimeIN
attendance_out_tbl
id | member_id | DateTime_OUT
I want to select all members for date: 2014-03-10 by this query:
SELECT
attendance_in.EDatetime,
members_info.mfullname,
attendance_out.ODatetime
FROM
attendance_in
LEFT JOIN members_info ON members_info.id = attendance_in.MemID
LEFT JOIN attendance_out ON attendance_out.MemID = attendance_in.MemID
WHERE date(attendance_in.EDatetime) OR date(attendance_out.ODatetime) = "2014-03-10"
But it give me different results in Attendace_out Results
You have a mistake in your query.
You wrote:
WHERE date(attendance_in.EDatetime) /* wrong! */
OR date(attendance_out.ODatetime) = "2014-03-10"
This is wrong, as the first expression date(attendance_in.EDatetime) always evaluates to true.
You may want
WHERE date(attendance_in.EDatetime) = "2014-03-10"
OR date(attendance_out.ODatetime) = "2014-03-10"
But, this is guaranteed to perform poorly when your attendance_in and attendance_out tables get large, because it will have to scan them; it can't use an index.
You may find that it performs better to write this:
WHERE (attendance_in.EDatetime >='2014-03-10' AND
attendance_in.EDatetime < '2014-03-10' + INTERVAL 1 DAY)
OR (attendance_out.EDatetime >='2014-03-10' AND
attendance_out.EDatetime < '2014-03-10' + INTERVAL 1 DAY)
That will check whether either the checkin our checkout time occurs on the day in question.

PHP: Find all products contained in an order

i want to notify users if all products they selected are available. I created a mysql table containing the following information:
+--------+-------------+------+-----+
| id | order_id | pro | avai|
+--------+-------------+------+-----+
| 1 | 28 | NO1 | y |
| 2 | 28 | NO2 | y |
| 3 | 28 | NO4 | n |
| 4 | 29 | NO8 | y |
+--------+-------------+------+-----+
I want to issue an email if all products (pro) from an order (order_id) are available (avai). The code i have so far:
$getData2 = mysql_query("SELECT * FROM notify WHERE avai = 'y'");
while($row = mysql_fetch_object($getData2))
{
//only gets the two with y, does not know that there is a third item that belongs to the order
}
That process should be automated, thats why i am not able to use a select based on the order_id.
Something like this, using an anti-join pattern should return the order_id where there are rows that are avail='y' and there no rows for that same order_id which are avail='n':
SELECT a.order_id
FROM notify a
LEFT
JOIN ( SELECT n.order_id
FROM notify n
WHERE n.avail = 'n'
GROUP BY n.order_id
) o
ON o.order_id = a.order_id
WHERE a.avail = 'y'
AND o.order_id IS NULL
GROUP BY a.order_id
There are other ways to get an equivalent result, such as using a NOT EXISTS predicate.
SELECT a.order_id
FROM notify a
WHERE a.avail = 'y'
AND NOT EXISTS (SELECT 1
FROM notify n
WHERE n.order_id = a.order_id
AND n.avail = 'n'
)
GROUP BY a.order_id
If there values other than 'y' and 'n', you may need to adjust the predicates to account for those conditions as well.
The logical inverse to avail = 'y' would be
NOT (avail <=> 'y')
To get only orders where ALL rows for a given order are avail='y', then we'd really need to check for any rows that are "not (avail='y')", rather than just "avail='n'". And can use the null-safe comparison operator ( <=> ) to conveniently handle comparisons to NULL values without having to code the OR avail IS NULL conditions.
SELECT a.order_id
FROM notify a
LEFT
JOIN ( SELECT n.order_id
FROM notify n
WHERE NOT (n.avail <=> 'y')
GROUP BY n.order_id
) o
ON o.order_id = a.order_id
WHERE a.avail = 'y'
AND o.order_id IS NULL
GROUP BY a.order_id
As your table is called notify, I assume it is just for this eMail notification?
Then you could use:
SELECT GROUP_CONCAT(avai SEPARATOR ',') FROM notify GROUP BY order_id
Then you loop through all the returned rows and explode() on ',' so you'll get an array for each row meaning for each order_id. So now, you just check the array for 'n'. If there is no 'n' in the array, you'll send the eMail and delete all rows containing that order_id. This way you don't send multiple eMails for the same order_id.
I hope that helps.
Here you can read up on GROUP_CONCAT if you want
I recommend you to change the n/y to Enum (statuses)
YES|NO|NOTIFY|ORDERED|SHIPPED|RECEIVED|RETURNED
SELECT t1.order_id,
(SELECT COUNT(*) FROM table AS t2 WHERE t2.order_id = t1.order_id) AS numOfProducts,
(SELECT COUNT(*) FROM table AS t3 WHERE t3.order_id = t1.order_id AND avai = 'y') AS
numOfAvaiProducts,
FROM table as t1
HAVING numOfAvaiProducts = numOfProducts
GROUP BY t1.order_id
The Query not tested but the it should work, you get here all the orders that
numOfAvaiProducts = numOfProducts and after notifying user you can update the status to notify and you dont get this result next time
Another advantage to do it like that is to save PHP code and runtime

PHP mySQL query to return a reordered list and count occurences of field value even if absent

Basically, I have a mysql db table which contains a datetime column and a category column. I want to create a SQL query to retrieve all the values present in the category column and count how many occurences of each category values grouped by month/year of the datetime column. If it is possible, I'd also like totals to be returned. A total for the number of all occurences in a month and a total of category counted.
Note: the category values cannot be hardcoded because they are set by the user and stored in another table.
DB table has following structure:
datetime | category
2009-01-05 | fish
2009-01-06 | fish
2009-01-06 | potato
2009-01-16 | fish
2009-02-08 | pineapple
2009-02-15 | potato
I wish returned result from query would be:
Month | fish | potato | pineapple | total
2009-01 | 3 | 1 | 0 | 4
2009-02 | 0 | 1 | 1 | 2
Total | 3 | 2 | 1 | 6
I think (hope) it can be done in a single SQL query but I can't figure out how.
Can anyone help me?
Thanks!
Let me first say that I think this feels more like an issue to handle in your presentation logic (php code). However, SQL can produce such a result. You are trying to accomplish two different things.
First, you're looking for a PIVOT table. MySQL does not support the PIVOT command, but you can simulate it with MAX and CASE. This works well when you know the number of potential categories, but won't work in your case.
Next, you want to have row totals and then a final total row. Again, this is more appropriate to handle in the presentation layer.
However, using Dynamic SQL, you can achieve both a PIVOT table and row totals. Here is some sample code:
First build your PIVOT variable #sql:
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'COUNT(IF(category = ''', category, ''',1,NULL)) AS ', category)
) INTO #sql
FROM (
SELECT *,
#rn:=IF(#prevMonthYear=CONCAT(YEAR(datetime),'-',MONTH(datetime)),#rn+1,1) rn,
#prevMonthYear:=CONCAT(YEAR(datetime),'-',MONTH(datetime)) dt
FROM yourtable JOIN (SELECT #rn:=0,#prevParent:=0) t
) t
;
Now build your Row Summary variable #totsql:
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'SUM(', category, ') AS sum_', category)
) INTO #totsql
FROM (
SELECT *,
#rn:=IF(#prevMonthYear=CONCAT(YEAR(datetime),'-',MONTH(datetime)),#rn+1,1) rn,
#prevMonthYear:=CONCAT(YEAR(datetime),'-',MONTH(datetime)) dt
FROM yourtable JOIN (SELECT #rn:=0,#prevParent:=0) t
) t
;
Put it all together:
SET #sql = CONCAT('SELECT dt,
', #sql, ', COUNT(1) total
FROM (
SELECT *,
#rn:=IF(#prevMonthYear=CONCAT(YEAR(datetime),''-'',MONTH(datetime)),#rn+1,1) rn,
#prevMonthYear:=CONCAT(YEAR(datetime),''-'',MONTH(datetime)) dt
FROM yourtable JOIN (SELECT #rn:=0,#prevParent:=0) t
) t
GROUP BY dt
UNION
SELECT ''Totals'',', #totsql, ', SUM(total)
FROM (
SELECT dt,
', #sql, ', COUNT(1) total
FROM (
SELECT *,
#rn:=IF(#prevMonthYear=CONCAT(YEAR(datetime),''-'',MONTH(datetime)),#rn+1,1) rn,
#prevMonthYear:=CONCAT(YEAR(datetime),''-'',MONTH(datetime)) dt
FROM yourtable JOIN (SELECT #rn:=0,#prevParent:=0) t
) t
GROUP BY dt
) t2
;');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SQL Fiddle Demo
Results:
MONTH FISH POTATO PINEAPPLE TOTAL
2009-1 3 1 0 4
2009-2 0 1 1 2
Totals 3 2 1 6
You can have multiple nested queries or you can use mysql loops in this case. Easiest thing would be to get all the data you need and process it using php.

Fetch range from days

I have this table structure:
EDIT more complex example: add hidden range
category| day | a |
--------|------------|-------|
1 | 2012-01-01 | 4 |
1 | 2012-01-02 | 4 |
1 | 2012-01-03 | 4 |
1 | 2012-01-04 | 4 |
1 | 2012-01-05 | 5 |
1 | 2012-01-06 | 5 |
1 | 2012-01-07 | 5 |
1 | 2012-01-08 | 4 |
1 | 2012-01-09 | 4 |
1 | 2012-01-10 | 4 |
1 | 2012-01-11 | 5 |
1 | 2012-01-12 | 5 |
1 | 2012-01-16 | 5 |
1 | 2012-01-17 | 5 |
1 | 2012-01-18 | 5 |
1 | 2012-01-19 | 5 |
...
with 'category-day' as unique keys. I would extract a range of dates, for each category, according with column "a" and given limit range, like so:
1,2012-01-01|2012-01-04,4
1,2012-01-05|2012-01-07,5
1,2012-01-08|2012-01-10,4
1,2012-01-11|2012-01-12,5
1,2012-01-13|2012-01-15,0
1,2012-01-16|2012-01-19,5
or similar.
I search the best way for do it. Using only mysql preferably but also with a little bit of php.
NOTE1: not all day are inserted: between two days non-contiguos could not be other days. In this case I would in output the missed range with column "a" = 0.
NOTE2: I did it with a simple query and some rows of php but I don't like it because my simple algorithm need a cycle for each day in range multiplied for each category found. If range is too big and there are too much categories, that's not so good.
FINAL EDIT: OK! After reading all comments and answers, I think not exists a valid, efficient and, at same time, readable solution. So Mosty Mostacho answer is a no 100% valid solution, but it has 100% valid suggestions. Thank you all.
New edit:
As I told you in a comment, I strongly recommend you to use the quick query and then process the missing dates in PHP as that would be faster and more readable:
select
concat(#category := category, ',', min(day)) col1,
concat(max(day), ',', #a := a) col2
from t, (select #category := '', #a := '', #counter := 0) init
where #counter := #counter + (category != #category or a != #a)
group by #counter, category, a
However, if you still want to use the query version, then try this:
select
#counter := #counter + (category != #category or a != #a) counter,
concat(#category := category, ',', min(day)) col1,
concat(max(day), ',', #a := a) col2
from (
select distinct s.day, s.category, coalesce(t1.a, 0) a
from (
select (select min(day) from t) + interval val - 1 day day, c.category
from seq s, (select distinct category from t) c
having day <= (select max(day) from t)
) s
left join t t1 on s.day = t1.day and s.category = t1.category
where s.day between (
select min(day) from t t2
where s.category = t2.category) and (
select max(day) from t t2
where s.category = t2.category)
order by s.category, s.day
) t, (select #category := '', #a := '', #counter := 0) init
group by counter, category, a
order by category, min(day)
Note that MySQL won't allow you to create data on the fly, unless you hardcode UNIONS, for example. This is an expensive process that's why I strongly suggest you to create a table with only an integer field with values from 1 to X, where X is, at least the maximum amount of dates that separate the min(day) and max(day) from your table. If you're not sure about that date, just add 100,000 numbers and you'll be able to generate range periods for over 200 years. In the previous query, this table is seq and the column it has is val.
This results in:
+--------------+--------------+
| COL1 | COL2 |
+--------------+--------------+
| 1,2012-01-01 | 2012-01-04,4 |
| 1,2012-01-05 | 2012-01-07,5 |
| 1,2012-01-08 | 2012-01-10,4 |
| 1,2012-01-11 | 2012-01-12,5 |
| 1,2012-01-13 | 2012-01-15,0 |
| 1,2012-01-16 | 2012-01-19,5 |
+--------------+--------------+
Ok, I'm lying. The result is actually returning a counter column. Just disregard it, as removing it (using a derived table) would be even less performant!
and here's a one liner brutality for you :) (Note: Change the "datt" table name.)
select dd.category,
dd.day as start_day,
(select dp.day from
(
select 1 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day - INTERVAL 1 DAY and a=d1.a
)
union
select 2 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day + INTERVAL 1 DAY and a=d1.a
)
) dp where dp.day >= dd.day - INTERVAL (n-2) DAY order by day asc limit 0,1)
as end_day,
dd.a from (
select 1 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day - INTERVAL 1 DAY and a=d1.a
)
union
select 2 as n,d1.category,d1.day,d1.a from datt d1 where not exists (
select * from datt where day = d1.day + INTERVAL 1 DAY and a=d1.a
)
) dd
where n=1
and it's output is :
|| 1 || 2012-01-01 || 2012-01-01 || 4 ||
|| 1 || 2012-01-03 || 2012-01-04 || 4 ||
|| 1 || 2012-01-05 || 2012-01-07 || 5 ||
|| 1 || 2012-01-08 || 2012-01-10 || 4 ||
|| 1 || 2012-01-11 || 2012-01-12 || 5 ||
Note: Thats the result for non-existing 2012-01-02 in a 01-12 day table.
No need for PHP or temporary tables or anything.
DISCLAIMER: I did this just for fun. This stunt may be too crazy to be used in a production environment. Therefore I'm not posting this as a "real" solution. Also I'm not willing to explain how it works :) And I didn't rethink / refactor it. There might be more elegant ways and names / aliases could be more informative. So please no flame or anything.
Here's my solution. Looks more complicated than it is. I think it may be easier to understand than other answers, no offense :)
Setting up test data:
drop table if exists test;
create table test(category int, day date, a int);
insert into test values
(1 , '2012-01-01' , 4 ),
(1 , '2012-01-02' , 4 ),
(1 , '2012-01-03' , 4 ),
(1 , '2012-01-04' , 4 ),
(1 , '2012-01-05' , 5 ),
(1 , '2012-01-06' , 5 ),
(1 , '2012-01-07' , 5 ),
(1 , '2012-01-08' , 4 ),
(1 , '2012-01-09' , 4 ),
(1 , '2012-01-10' , 4 ),
(1 , '2012-01-11' , 5 ),
(1 , '2012-01-12' , 5 ),
(1 , '2012-01-16' , 5 ),
(1 , '2012-01-17' , 5 ),
(1 , '2012-01-18' , 5 ),
(1 , '2012-01-19' , 5 );
And here it comes:
SELECT category, MIN(`day`) AS firstDayInRange, max(`day`) AS lastDayInRange, a
, COUNT(*) as howMuchDaysInThisRange /*<-- as a little extra*/
FROM
(
SELECT
IF(#prev != qr.a, #is_a_changing:=#is_a_changing+1, #is_a_changing) AS is_a_changing, #prev:=qr.a, qr.* /*See if column a has changed. If yes, increment, so we can GROUP BY it later*/
FROM
(
SELECT
test.category, q.`day`, COALESCE(test.a, 0) AS a /*When there is no a, replace NULL with 0*/
FROM
test
RIGHT JOIN
(
SELECT
DATE_SUB(CURDATE(), INTERVAL number_days DAY) AS `day` /*<-- Create dates from now back 999 days. This query is surprisingly fast. And adding more numbers to create more dates, i.e. 10000 dates is also no problem. Therefor a temporary dates table might not be necessary?*/
FROM
(
SELECT (a + 10*b + 100*c) AS number_days FROM
(SELECT 0 AS a 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) aa
, (SELECT 0 AS b 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) bb
, (SELECT 0 AS c 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) cc
)sq /*<-- This generates numbers 0 to 999*/
)q USING(`day`)
, (SELECT #is_a_changing:=0, #prev:=0) r
/*This WHERE clause is just to beautify. It may not be necessary*/
WHERE q.`day` >= (SELECT MIN(test.`day`) FROM test) AND q.`day` <= (SELECT MAX(test.`day`) FROM test)
)qr
)asdf
GROUP BY is_a_changing
ORDER BY 2
Result looks like this:
category firstDayInRange lastDayInRange a howMuchDaysInThisRange
--------------------------------------------------------------------------
1 2012-01-01 2012-01-04 4 4
1 2012-01-05 2012-01-07 5 3
1 2012-01-08 2012-01-10 4 3
1 2012-01-11 2012-01-12 5 2
2012-01-13 2012-01-15 0 3
1 2012-01-16 2012-01-19 5 4
To make this work as you want it to, you should have two tables:
for periods
for days
Where each period can have many days related to it through FOREIGN KEY. With current table structure, the best you can do is to detect the continuous periods on PHP side.
Firstly, this is an extension of #Mosty's solution.
To enable Mosty's solution to include category/date combinations than do not exist in the table I took the following approach -
Start by getting a distinct list of categories and then join this to the entire date range -
SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`
The above query builds the full date range using the table dummy with a single field id. The id field contains 0,1,2,3,.... - it needs to have enough values to cover every day in the required date range. This can then be joined back to the original table to create a complete list of all categories for all dates and the appropriate value for a -
SELECT cj.category, cj.`day`, IFNULL(t.a, 0) AS a
FROM (
SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`
) AS cj
LEFT JOIN t
ON cj.category = t.category
AND cj.`day` = t.`day`
This can then be applied to Mosty's query in place of table t -
SELECT
CONCAT(#category := category, ',', MIN(`day`)) col1,
CONCAT(MAX(`day`), ',', #a := a) col2
FROM (
SELECT cj.category, cj.day, IFNULL(t.a, 0) AS a
FROM (
SELECT category, `start` + INTERVAL id DAY AS `day`
FROM dummy,(SELECT DISTINCT category FROM t) cats, (SELECT MIN(day) `start`, MAX(day) `end` FROM t) tmp
WHERE id <= DATEDIFF(`end`, `start`)
ORDER BY category, `day`
) AS cj
LEFT JOIN t
ON cj.category = t.category
AND cj.`day` = t.day) AS t, (select #category := '', #a := '', #counter := 0) init
WHERE #counter := #counter + (category != #category OR a != #a)
GROUP BY #counter, category, a
Completely on mysql side will have performance adv:
Once the procedure has been created, it runs within 0.35 - 0.37 sec
create procedure fetch_range()
begin
declare min date;
declare max date;
create table testdate(
date1 date
);
select min(day) into min
from category;
select max(day) into max
from category;
while min <= max do
insert into testdate values(min);
set min = adddate(min,1);
end while;
select concat(category,',',min(day)),concat(max(day),',',a)
from(
SELECT if(isNull(category),#category,category) category,if(isNull(day),date1,day) day,#a,if(isNull(a) || isNull(#a),if(isNull(a) && isNull(#a),#grp,#grp:=#grp+1),if(#a!=a,#grp:=#grp+1,#grp)) as sor_col,if(isNull(a),0,a) as a,#a:=a,#category:= category
FROM `category`
RIGHT JOIN testdate ON date1 = category.day) as table1
group by sor_col;
drop table testdate;
end
o/p:
1,2012-01-01|2012-01-04,4
1,2012-01-05|2012-01-07,5
1,2012-01-08|2012-01-10,4
1,2012-01-11|2012-01-12,5
1,2012-01-13|2012-01-15,0
1,2012-01-16|2012-01-19,5
Here is mysql solution which will give the desired result excluding the missed range only.
PHP:
The missing range can be added through php.
$sql = "set #a=0,#grp=0,#datediff=0,#category=0,#day='';";
mysql_query($sql);
$sql= "select category,min(day)min,max(day) max,a
from(
select category,day,a,concat(if(#a!=a,#grp:=#grp+1,#grp),if(datediff(#day,day) < -1,#datediff:=#datediff+1,#datediff)) as grp_datediff,datediff(#day,day)diff, #day:= day,#a:=a
FROM category
order by day)as t
group by grp_datediff";
$result = mysql_query($sql);
$diff = 0;
$indx =0;
while($row = mysql_fetch_object($result)){
if(isset($data[$indx - 1]['max'])){
$date1 = new DateTime($data[$indx - 1]['max']);
$date2 = new DateTime($row->min);
$diff = $date1->diff($date2);
}
if ($diff->days > 1) {
$date = new DateTime($data[$indx-1]['max']);
$interval = new DateInterval("P1D");
$min = $date->add($interval);
$date = new DateTime($data[$indx-1]['max']);
$interval = new DateInterval("P".$diff->days."D");
$max = $date->add($interval);
$data[$indx]['category'] = $data[$indx-1]['category'];
$data[$indx]['min'] = $min->format('Y-m-d');
$data[$indx]['max'] = $max->format('Y-m-d');
$data[$indx++]['a'] = 0;
$data[$indx]['category'] = $row->category;
$data[$indx]['min'] = $row->min;
$data[$indx]['max'] = $row->max;
$data[$indx]['a'] = $row->a;
}else{
$data[$indx]['category'] = $row->category;
$data[$indx]['min'] = $row->min;
$data[$indx]['max'] = $row->max;
$data[$indx]['a'] = $row->a;
}
$indx++;
}
Is this what you mean?
SELECT
category,
MIN(t1.day),
MAX(t2.day),
a
FROM
`table` AS t1
INNER JOIN `table` AS t2 USING (category, a)
If I understand your question correctly, I would use something to the effect of:
SELECT MAX(day), MIN(day) FROM `YourTable` WHERE `category`= $cat AND `A`= $increment;
... and ...
$dateRange = $cat.","."$min"."|"."$max".",".$increment;

Categories