MySQL: count unique days in period with overlaps and gaps (optimized) - mysql

How to calculate the number of unique days per month from a table with two date columns, in which the periods can have gaps and overlaps?
I rather not use a calendar table to get the unique days, because it generates a temporary table with thousands of records, and resources are limited.
Example table:
+---------+------------+------------+
| mygroup | alpha | omega |
+---------+------------+------------+
| 1 | 2017-02-04 | 2017-04-14 |
| 1 | 2017-03-25 | 2017-03-28 |
| 1 | 2017-01-23 | 2017-01-25 |
| 2 | 2017-02-05 | 2017-02-20 |
| 1 | 2017-04-28 | 2017-05-12 |
| etc. | etc. | etc. |
+---------+------------+------------+

Is it what you need?
select count(distinct selected_date),te.mygroup, MONTHNAME(selected_date)from
(select adddate('1970-01-01',t4.i*10000 + t3.i*1000 + t2.i*100 + t1.i*10 + t0.i) selected_date from
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t0,
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t1,
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t2,
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t3,
(select 0 i union select 1 union select 2 union select 3 union select 4 union select 5 union select 6 union select 7 union select 8 union select 9) t4) v
cross join test te
where selected_date between te.alpha and te.omega
group by mygroup, MONTHNAME(selected_date)
Оutput for your example:
'17','1','April'
'25','1','February'
'3','1','January'
'31','1','March'
'12','1','May'
'16','2','February'
Count may be greater than number of the days in month because such overlap exists in few rows - it's not а mistake.

There is another way to do this roughly 10 times faster than a calendar table.
The biggest resource spoiler is the calendar table itself, it is used to filter unique days.
But instead of using a whole table record for that, it can be done using 31 bits in an UINT.
Recepe:
Create a calendar table with months only
Cut periods in months, and join them with the calendar table
Translate periods to bits of UINTs
OR the UINTs per month for uniqueness
Count their bits as unique days per month
Output:
+--------------+---------+---------+-------+
| Period | Group 1 | Group 2 | Total |
+--------------+---------+---------+-------+
| 2017 month 5 | 11 | 0 | 11 |
| 2017 month 4 | 15 | 0 | 15 |
| 2017 month 3 | 30 | 0 | 30 |
| 2017 month 2 | 24 | 15 | 39 |
| 2017 month 1 | 2 | 0 | 2 |
+--------------+---------+---------+-------+
MySQL query:
SELECT
`tabulate`.`period` AS `Period`,
SUM(IF(`tabulate`.`mygroup` = 1,
`tabulate`.`days`, 0)) AS `Group 1`,
SUM(IF(`tabulate`.`mygroup` = 2,
`tabulate`.`days`, 0)) AS `Group 2`,
SUM(`tabulate`.`days`) AS `Total`
FROM
( SELECT
`unique`.`period`,
BIT_COUNT(BIT_OR(CONV(CONCAT(
REPEAT("1", DAYOFMONTH(`unique`.`omega`) - DAYOFMONTH(`unique`.`alpha`)),
REPEAT("0", DAYOFMONTH(`unique`.`alpha`) - 1)
), 2, 10))) AS `days`,
`unique`.`mygroup`
FROM
( SELECT
DATE_FORMAT(`permonth`.`period_alpha`, "%Y month %c") AS `period`,
GREATEST(`permonth`.`period_alpha`, `permonth`.`example_alpha`) AS `alpha`,
LEAST(`permonth`.`period_omega`, `permonth`.`example_omega`) AS `omega`,
`permonth`.`mygroup`
FROM
( SELECT
`period`.`alpha` AS `period_alpha`,
DATE_SUB(`period`.`omega`, INTERVAL 1 DAY) AS `period_omega`,
`example`.`mygroup`,
IFNULL(`example`.`alpha`, `period`.`alpha`) AS `example_alpha`,
IFNULL(`example`.`omega`, CURDATE()) AS `example_omega`
FROM
( SELECT
DATE_ADD(
MAKEDATE(YEAR(CURDATE()), 1),
INTERVAL `season`.`n` + (`month`.`n` << 2) MONTH
) AS `alpha`,
DATE_ADD(
MAKEDATE(YEAR(CURDATE()), 1),
INTERVAL 1 + `season`.`n` + (`month`.`n` << 2) MONTH
) AS `omega`
FROM
( SELECT 0 AS `n`
UNION ALL SELECT 1
UNION ALL SELECT 2
) AS `month`
CROSS JOIN (SELECT 0 AS `n`
UNION ALL SELECT 1
UNION ALL SELECT 2
UNION ALL SELECT 3
) AS `season`
) AS `period`
INNER JOIN
( SELECT 1 AS `mygroup`, "2017-02-04" AS `alpha`, "2017-04-14" AS `omega`
UNION ALL SELECT 1, "2017-03-25", "2017-03-28"
UNION ALL SELECT 1, "2017-01-23", "2017-01-25"
UNION ALL SELECT 2, "2017-02-05", "2017-02-20"
UNION ALL SELECT 1, "2017-04-28", "2017-05-12"
) AS `example` ON (
(`example`.`alpha` < `period`.`omega` OR `example`.`alpha` IS NULL)
AND IFNULL(`example`.`omega`, CURDATE()) >= `period`.`alpha`
)
) AS `permonth`
) AS `unique`
GROUP BY
`unique`.`period`,
`unique`.`mygroup`
) AS `tabulate`
GROUP BY `tabulate`.`period`
ORDER BY `tabulate`.`period` DESC

Related

Group query by date which is between range

I have a table like:
id | date_start | date_end
1 | 2019.05.01 | 2019.05.06
2 | 2019.05.05 | 2019.05.05
3 | 2019.05.05 | 2019.05.08
Date_start and date_end means that entry is active during this period. Is it possible to get days with his count of active entries for intermediate values too? Like:
2019.05.01 | 1
2019.05.02 | 1
2019.05.03 | 1
2019.05.04 | 1
2019.05.05 | 3
2019.05.06 | 2
2019.05.07 | 1
2019.05.08 | 1
I think that this will work for you:
SELECT temp_date,
COUNT(*)
FROM (
SELECT ((SELECT MAX(date_start) FROM {dates_table}) - INTERVAL c.number DAY) AS temp_date
FROM (SELECT singles + tens + hundreds number
FROM ( SELECT 0 singles
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
) singles
JOIN (SELECT 0 tens
UNION ALL SELECT 10 UNION ALL SELECT 20 UNION ALL SELECT 30
UNION ALL SELECT 40 UNION ALL SELECT 50 UNION ALL SELECT 60
UNION ALL SELECT 70 UNION ALL SELECT 80 UNION ALL SELECT 90
) tens
JOIN (SELECT 0 hundreds
UNION ALL SELECT 100 UNION ALL SELECT 200 UNION ALL SELECT 300
UNION ALL SELECT 400 UNION ALL SELECT 500 UNION ALL SELECT 600
UNION ALL SELECT 700 UNION ALL SELECT 800 UNION ALL SELECT 900
) hundreds
ORDER BY number DESC) c
WHERE c.number BETWEEN 0 AND (SELECT DATEDIFF(MAX(date_end),MIN(date_start)) FROM {dates_table})
) calendar
JOIN {dates_table} ON temp_date BETWEEN end_date AND start_date
GROUP BY temp_date
Just replace {dates_table} with the name of your table.
The base of this that generates the series of dates was taken from IvanD's answer on this SO question: How to populate a table with a range of dates?

How to select Current Week working Dates from MySQL stored in comma saperated values

I'm having a table Storing planned_production below Format.I need to get the working_dates for current week.
+------------+-------------+---------+----------+---------+------+--------------+---------------------------------------------+
| planned_id | per_quarter | per_day | per_week | quarter | year | working_days | working_dates | |
+------------+-------------+---------+----------+---------+------+--------------+---------------------------------------------+
| 1 | 860 | 14.10 | 70.50 | 2 | 2018 | 61 | 02-04-2018,03-04-2018,04-04-2018,05-04-2018 |
| 06-04-2018,09-04-2018,12-04-2018,15-04-2018
+------------+-------------+---------+----------+---------+------+--------------+---------------------------------------------+
I'm trying to get the working dates for current week.
the query i'm using is below.
i'm not able to figure it out where i'm going wrong.
SELECT Value from ( SELECT SUBSTRING_INDEX(SUBSTRING_INDEX(working_dates, ',', n.n), ',', -1) Value
FROM planned_production t CROSS JOIN
(
SELECT a.N + b.N * 10 + 1 n
FROM
(SELECT 0 AS N 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) a
,(SELECT 0 AS N 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) b
ORDER BY n
) n
WHERE quarter = quarter(curdate()) and n.n <= 1 + (LENGTH(t.working_dates) - LENGTH(REPLACE(t.working_dates, ',', '')))
) as TEMP_TABLE
WHERE Value between CURRENT_DATE() - INTERVAL
WEEKDAY(CURRENT_DATE()) + 7 DAY
AND
(CURRENT_DATE() - INTERVAL WEEKDAY(CURRENT_DATE()) DAY) - INTERVAL 1
SECOND ORDER BY Value asc;
I need something like this as output
+------------+
| value |
+------------+
| 17-05-2018 |
| 18-05-2018 |
+------------+
Any suggestions welcomed and Thanks in advance.
Table Structure:
create table test (planned_id int, per_quarter float, per_day float, per_week float,
quarter int, year int, working_days int, working_dates varchar(1000));
Table Values:
insert into test values ( 1,860,14.10,70.50,2,2018,61,'02-04-2018,03-04-2018,04-04-2018,05-04-2018,06-04-2018,09-04-2018,12-04-2018,15-04-2018,17-05-2018,18-05-2018');
insert into test values ( 2,800,12.10,60.50,2,2018,50,'02-04-2018,03-04-2018,04-04-2018,05-04-2018,06-04-2018,09-04-2018,12-04-2018,15-04-2018,14-05-2018,15-05-2018') ;
SQL Query:
set #startdate := date_add(SUBDATE(current_date, WEEKDAY(current_date)), interval -1 day);
select test.planned_id, checkdate
from
(
select #startdate := date_add(#startdate, interval 1 day) as checkdate
from ( SELECT 0 singles
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)
a cross join (
SELECT 0 singles
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) b
cross join (
SELECT 0 singles
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) c
) as dim_date
join test
where
checkdate between SUBDATE(current_date, WEEKDAY(current_date))
and date_add(SUBDATE(current_date, WEEKDAY(current_date)) , interval 7 day)
and find_in_set(DATE_FORMAT(checkdate, '%d-%m-%Y'),working_dates) != 0
order by test.planned_id
Output:
planned_id checkdate
1 2018-05-17
1 2018-05-18
2 2018-05-14
2 2018-05-15
Logic:
1) First create table dim_date. You can create it using query as shown in above query but I would suggest you to create one table in your database.
2) Use filter to select only one week
checkdate between SUBDATE(current_date, WEEKDAY(current_date))
and date_add(SUBDATE(current_date, WEEKDAY(current_date)) , interval 7 day)
3) Use cross join between dim_table and test
4) Use find_in_set function to check date is present or not.
and find_in_set(DATE_FORMAT(checkdate, '%d-%m-%Y'),working_dates) != 0

SQL/Mysql query available dates and times in database

I need some help querying my calendar/dates table
Scenario: I have a "calendar" table with dates and times (see below), users will set their available dates, usually day by day with available time slots for each day. So my table looks like this:
+------+------------+---------------------+---------------------+
| ID | user_id | start_date | end_date |
+------+------------+---------------------+---------------------+
| 1 | 1 | 2016-09-01 08:00:00 | 2016-09-01 16:00:00 |
| 2 | 1 | 2016-09-03 00:00:00 | 2016-09-03 23:59:59 |
| 3 | 1 | 2016-09-04 00:00:00 | 2016-09-04 16:00:00 |
| 4 | 1 | 2016-09-05 08:00:00 | 2016-09-05 16:00:00 |
| 5 | 2 | 2016-09-05 08:00:00 | 2016-09-05 16:00:00 |
| 6 | 2 | 2016-09-07 08:00:00 | 2016-09-07 16:00:00 |
| 7 | 2 | 2016-09-08 08:00:00 | 2016-09-08 16:00:00 |
| 8 | 2 | 2016-09-08 18:00:00 | 2016-09-08 22:00:00 |
+------+------------+---------------------+---------------------+
We have 2 users here so I want the following:
If I search for start_date = 2016-09-05 08:00:00 and end_date = 2016-09-05 16:00:00 it should return user 1 and 2. Since both of them has an entry with these dates. Same goes as well if start_date = 2016-09-05 09:00:00 and end_date = 2016-09-05 15:00:00, this should as well return both users since the time im searching for is between the time slots as shown in the example.
Second scenario is a little bit more tricky, If user search for start_date = 2016-09-03 08:00:00 and end_date = 2016-09-04 16:00:00 i want the query to check the following:
see if the user is available each day at these times.
so in this case, is the user available on 2016-09-03 between 08:00:00 and 16:00:00 and as well on 2016-09-04 between 08:00:00 and 16:00:00.
In the example over this should return user 1.
Im open for suggestion on re-designed my schema if needed.
Hope some can help me with this.
DEMO include some aditional code comment, and show how the query evolve . Also I add another row for user_id = 2 to show how only match one of the two days in the range.
SELECT U.`user_id`
FROM (
select a.selectDate,
CONCAT(a.selectDate, ' ', time(#s_date)) as start_time,
CONCAT(a.selectDate, ' ', time(#s_date)) as end_time
from (
select '1900-01-01' + INTERVAL (a.a + (10 * b.a) + (100 * c.a) + (1000 * d.a) + (10000 * e.a)) DAY as selectDate
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) as a
cross join (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) as b
cross join (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) as c
cross join (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) as d
cross join (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) as e
) a
CROSS JOIN (SELECT #s_date := '2016-09-03 08:00:00', #e_date := '2016-09-04 16:00:00') par
-- CROSS JOIN (SELECT #s_date := '2016-09-05 08:00:00', #e_date := '2016-09-05 16:00:00') par
-- CROSS JOIN (SELECT #s_date := '2016-09-05 09:00:00', #e_date := '2016-09-05 15:00:00') par
WHERE selectDate BETWEEN date(#s_date)
AND date(#e_date)
) D
CROSS JOIN (SELECT DISTINCT `user_id` FROM Table1) U
LEFT JOIN Table1 T
ON U.`user_id` = T.`user_id`
AND D.start_time <= T.`end_date`
AND D.end_time >= T.`start_date`
GROUP BY U.`user_id`
HAVING COUNT(U.`user_id`) = COUNT(T.`user_id`);
OUTPUT
Step 1: create a list of dates, in this case 273 years
Step 2: select all dates between the range define in the parameters, also include the time window to each date.
Step 3: join all together to see what dates have user in that time window
Step 4: select only user with a time window for all dates

Opening balance and closing balance for a non-working date

Need a help on how to get opening balances and closing balances of a non-working date in my transaction table in my database.
transaction table
+------------------+----------+-----------+
| id | trans_date | debit | credit |
+----+-------------+----------+-----------+
| 1 | 2016-05-09 | 200.00 | 0.00 |
| 2 | 2016-05-11 | 0.00 | 50.00 |
+---------------+-------------+-----------+
Want a result like below. You will realize there were no transaction on "2016-05-10" but the result shows both opening balance and closing balance. Thanks
+-------------+--------------+-----------+-----------+------------+
| trans_date | open_bal | debit | credit |closing_bal |
+-------------+--------------+-----------+-----------+------------+
| 2016-05-09 | 0.00 | 200.00 | 0.00 | 200.00 |
| 2016-05-10 | 200.00 | 0.00 | 0.00 | 200.00 |
| 2016-05-11 | 200.00 | 0.00 | 50.00 | 150.00 |
+-------------+-------------+--------------+------------+---------+
I tried this but "2016-05-10" does not show in the result.
SELECT trans_date,open_balance
FROM(SELECT s.gen_id, s.trans_id, s.trans_date,
s.narations, s.account_code,
s.op_balance as open_balance,
s.debit, s.credit, s.closing_balance
from ( select t.gen_id, t.trans_id,
t.narations, t.account_code,
t.trans_date, t.credit, t.debit,
#tot_debit := if(#prev_client = t.account_code, #tot_debit + t.debit,t.debit) as tot_cred,
#tot_credit := if(#prev_client = t.account_code,#tot_credit + t.credit,t.credit) as tot_deb,
#cur_bal := if(#prev_client = t.account_code, #tot_debit - #tot_credit,t.debit-t.credit) as closing_balance,
(#cur_bal + t.credit) - t.debit as op_balance, #prev_client := t.account_code
from (select * from journal WHERE account_code = 41003
GROUP BY trans_date order by trans_date,account_code,trans_id)t, (select #prev_client:=0,#cur_bal:=0,#tot_debit:=0,#tot_credit:= 0,#open_balance:=0)r )s) g where trans_date <= '2016-05-11'
You can use this to select list of dates in MySQL.
SELECT DATE_ADD(CURDATE() , INTERVAL -(a.n + 10 * b.n + 100 * c.n) DAY) AS `date`
FROM (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) AS a
CROSS JOIN (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) AS b
CROSS JOIN (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) AS c
If you want to include in result dates not presented at table transactions, it can be done with LEFT JOIN
SELECT * FROM
(SELECT DATE_ADD(CURDATE() , INTERVAL -(a.n + 10 * b.n + 100 * c.n) DAY) AS `date`
FROM (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) AS a
CROSS JOIN (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) AS b
CROSS JOIN (SELECT 1 AS n UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9 UNION SELECT 0) AS c
) AS dates
LEFT JOIN transactions ON trans_date = dates.date
Note, that this will select dates from current for 999 past days. If you want more days ago, another CROSS JOIN can be added and 9 999 days will be returned.

Find dates between start date and end date in mysql

I got 2 columns, one is start date and the other is end date. I need to find every single day between the two dates.
Need it in another table with two coloums, one name pno that refers to the pno's id in the first table, and the other with the dates between start date and end date.
As an example this could be my input
pno startdate end date
p1 2012-12-03 2012-12-06
p2 2013-01-05 2013-01-08
p3 2013-01-15 2012-01-20
and this has to be my output.
pno dates
----------
p01 2012-12-03
p01 2012-12-04
p01 2012-12-05
p01 2012-12-06
p02 2013-01-05
p02 2013-01-06
p02 2013-01-07
p02 2013-01-08
p03 2013-01-15
...
You can do it like this
SELECT pno, startdate + INTERVAL q.n - 1 DAY dates
FROM table1 t CROSS JOIN
(
SELECT a.N + b.N * 10 + 1 n
FROM
(SELECT 0 AS N 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) a
,(SELECT 0 AS N 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) b
ORDER BY n
) q
WHERE q.n - 1 <= DATEDIFF(enddate, startdate)
ORDER BY pno, dates
The subquery generates a sequence of numbers from 1 to 100. You can adjust it for your needs (if date differences span more or less than 100 days) or completely substitute it with a persisted tally(numbers) table if you do a lot of such queries.
Output:
+------+------------+
| pno | dates |
+------+------------+
| p1 | 2012-12-03 |
| p1 | 2012-12-04 |
| p1 | 2012-12-05 |
| p1 | 2012-12-06 |
| p2 | 2013-01-05 |
| p2 | 2013-01-06 |
| p2 | 2013-01-07 |
| p2 | 2013-01-08 |
| p3 | 2013-01-15 |
| p3 | 2013-01-16 |
| p3 | 2013-01-17 |
| p3 | 2013-01-18 |
| p3 | 2013-01-19 |
| p3 | 2013-01-20 |
+------+------------+
Here is SQLFiddle demo
UPDATE: To create and populate persisted tally table use
CREATE TABLE tally (n INT NOT NULL PRIMARY KEY);
INSERT INTO tally (n)
SELECT a.n + b.n * 10 + c.n * 100 + d.n * 1000 + 1 n
FROM
(SELECT 0 AS n 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) a
,(SELECT 0 AS n 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) b
,(SELECT 0 AS n 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) c
,(SELECT 0 AS n 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) d
ORDER BY n;
You'll have in the tally table sequence of numbers from 1 to 10000. That will allow you to work with date ranges that span more than 27 years.
Now the query boils down to
SELECT pno, startdate + INTERVAL q.n - 1 DAY dates
FROM table1 t CROSS JOIN tally q
WHERE q.n - 1 <= DATEDIFF(enddate, startdate)
ORDER BY pno, dates
Here is SQLFiddle demo