Multiple relationships in a stored procedure - php

I am preparing a query in which the total to be paid is added to the number of daily libraries that a student requests throughout the month.
It is worth mentioning that there are several types of libraries and what I need is to be able to associate the document payable to the game library catch of that month depending on the type of library
So, if a student requested 5 libraries in the month and 2 are morning and 3 are sports. 2 documents payable would be created and the morning document payable would be assigned to the 2 captures and the sports document to the 3 captures of the month.
CREATE PROCEDURE pagodeludotecaalumnos (IN alumnoid, IN fecha) BEGIN
insert into cj_documentoporpagar(documentoid, subconceptoid, pagoestatusid, alumnoid, cicloid, gradoid,
mediopagoid, importe, saldo, fechalimitepago, fechacreacion, fechaprontopago, referencia, documento,
hermanos, reingreso, padreexalumno, concepto, iva)
select 17 as DocumentoId,
case when a.TipoId = 1 or a.tipoid = 2 then 122
when a.TipoId = 3 then 235
when a.TipoId = 4 then -1 end as SubConceptoId,
1 as PagoEstatusId, b.AlumnoId,
5 as CicloId, b.GradoId, 1 as MedioPagoId,
case when c.PrimerNombre like '%*%' or c.ApellidoPaterno like '%*%' or c.ApellidoMaterno like '%*%' then 40 * Count(*) else 55 * Count(*) end as Importe,
case when c.PrimerNombre like '%*%' or c.ApellidoPaterno like '%*%' or c.ApellidoMaterno like '%*%' then 40 * Count(*) else 55 * Count(*) end as Saldo,
DATE(DATE_ADD(LAST_DAY(fecha), INTERVAL 1 MONTH)) as FechaLImitePago, now() as FechaCreacion, now() as FechaProntoPago,
'' as Referencia, '202003L' as documento, 0 as Hermanos, 0 as Reingreso, 0 as PadreExAlumno,
Concat('Ludoteca Marzo (', cast(Count(*) as int), ') dia(s)') as Concepto, 0 as IVA
/*, Concat(c.PrimerNombre, ' ', c.ApellidoPaterno, ' ', c.ApellidoMaterno) AlumnoNombre*/
from lu_captura a
inner join ce_alumnoporciclo b on a.alumnoporcicloid = b.alumnoporcicloid
inner join ce_alumno c on b.alumnoid = c.alumnoid
where date(a.Fecha) between DATE_FORMAT(fecha, '%Y-%m-01') and LAST_DAY(fecha) and a.TieneContrato = 0 and c.alumnoid = alumnoid
group by a.TipoId, b.AlumnoId, b.GradoId
SET #LID = LAST_INSERT_ID();
update lu_captura set DocumentoPorPagarId = #LID where Fecha between DATE_FORMAT(fecha, '%Y-%m-01') and LAST_DAY(fecha)
Currently I have this query and the question is: How could I relate the document payable created to the corresponding type of library?

According to MySQL Stored Procedure
I am using this syntax in stored procedure if you work in the same
then you can try this
DROP Procedure IF EXISTS pagodeludotecaalumnos;
Delimitter to used to set whole procedure below it as single operationable
DELIMITER $$
Create procedure command
CREATE PROCEDURE pagodeludotecaalumnos (
IN alumnoid int, IN fecha varchar(50), IN_action char(30)
)
Case begin from here
BEGIN
**-- switch case begin here**
CASE
WHEN IN_action = "insert" THEN
Creating entry for new record
INSERT INTO cj_documentoporpagar(
documentoid, subconceptoid,pagoestatusid, alumnoid, cicloid,
gradoid,mediopagoid, importe,saldo, fechalimitepago, fechacreacion,
fechaprontopago, referencia,documento,hermanos, reingreso,
padreexalumno, concepto, iva
)
VALUES(
IN_documentoid, IN_subconceptoid, IN_pagoestatusid,
IN_alumnoid,IN_cicloid,IN_gradoid, IN_mediopagoid, IN_importe,
IN_saldo,IN_fechalimitepago, IN_fechacreacion,IN_fechaprontopago,
IN_referencia, IN_documento, IN_hermanos, IN_reingreso,
IN_padreexalumno, IN_concepto, IN_iva
);
-- Insert end here
For update
WHEN IN_action = "update" THEN
-- For update elements
-- I am only show you how i make stored procedure in mysql i.e. your update statement
UPDATE lu_captura SET DocumentoPorPagarId = #LID where Fecha between
DATE_FORMAT(fecha, '%Y-%m-01') and LAST_DAY(fecha)
WHERE condition(if_exists);
For select statement
-- For Select elements
-- i.e. your select statement because i don't know what are you want to update, store an insert
WHEN IN_action = "select" THEN
select 17 as DocumentoId,
case when a.TipoId = 1 or a.tipoid = 2 then 122
when a.TipoId = 3 then 235
when a.TipoId = 4 then -1 end as
SubConceptoId,
1 as PagoEstatusId, b.AlumnoId,
5 as CicloId, b.GradoId, 1 as MedioPagoId,
case when c.PrimerNombre like '%*%' or c.ApellidoPaterno
like '%*%' or c.ApellidoMaterno
like '%*%' then 40 * Count(*)
else 55 * Count(*) end as Importe,
case
when c.PrimerNombre like '%*%' or c.ApellidoPaterno
like '%*%' or c.ApellidoMaterno
like '%*%' then 40 * Count(*) else 55 * Count(*)
end as Saldo,
DATE(DATE_ADD(LAST_DAY(fecha), INTERVAL 1 MONTH)) as
FechaLImitePago, now() as FechaCreacion, now() as
FechaProntoPago, '' as Referencia, '202003L' as documento, 0
as Hermanos, 0 as Reingreso, 0 as PadreExAlumno,
Concat('Ludoteca Marzo (', cast(Count(*) as int), ') dia(s)')
as Concepto, 0 as IVA
/*, Concat(c.PrimerNombre, ' ', c.ApellidoPaterno, ' ',
c.ApellidoMaterno) AlumnoNombre*/
from lu_captura a
inner join ce_alumnoporciclo b on a.alumnoporcicloid =
b.alumnoporcicloid
inner join ce_alumno c on b.alumnoid = c.alumnoid
where
date(a.Fecha) between DATE_FORMAT(fecha, '%Y-%m-01') and
LAST_DAY(fecha) and a.TieneContrato = 0 and c.alumnoid =
alumnoid group by a.TipoId, b.AlumnoId, b.GradoId
END CASE;
-- switch case ends here
END;
-- end of the PROCEDURE

Related

Query BETWEEN not working if it has the same date

I have an Oracle query that works fine using 'WHERE something BETWEEN FROM and TO' and if from/to are not the same, but when they are, it doesn't works.
I tried to validate on PHP if from/to are the same then i change the query replacing the BETWEEN for a 'WHERE something = date' but it doesn't works either.
This is the query with the PHP validation i mentioned:
<?php
$db = "(DESCRIPTION=(ADDRESS_LIST = (ADDRESS = (PROTOCOL = TCP)(HOST = 191.238.210.61)(PORT = 1521)))(CONNECT_DATA=(SID=opera)))";
$conn = oci_connect("opera", "opera", $db);
if (!$conn):
#--- Si no hay conexión, muestra error y detiene la depuración
$m = oci_error();
echo $m['message'];
elseif ($desde == $hasta):
$consulta = "
SELECT
CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_NAME,
ADULTS,
CHILDREN,
RATE_CODE,
TRACE_ON,
DEPT_ID,
SUM(ADULTS) OVER()
|| ' ADULTOS Y '
|| SUM(CHILDREN) OVER()
|| ' NIÑOS' AS RESULT
FROM
( SELECT
MAX(CONFIRMATION_NO) AS CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_FIRST_NAME
|| ' '
|| GUEST_NAME GUEST_NAME,
MAX(ADULTS) AS ADULTS,
MAX(CHILDREN) AS CHILDREN,
RATE_CODE,
MIN(TRACE_ON) AS TRACE_ON,
LISTAGG(GUEST_RSV_TRACES.DEPT_ID, ',') WITHIN GROUP (ORDER BY NULL) AS DEPT_ID
FROM
GUEST_RSV_TRACES,
NAME_RESERVATION
WHERE trace_on = '$desde'
AND guest_rsv_traces.resv_name_id = name_reservation.resv_name_id
AND rate_code like 'ALL%'
AND resv_status not in ('CANCELLED')
AND guest_rsv_traces.dept_id not in ('TRF','OD')
GROUP BY
NATIONALITY_DESC,
GUEST_FIRST_NAME,
GUEST_NAME,
RATE_CODE
)
";
$query = oci_parse($conn, $consulta);
oci_execute($query);
elseif ($desde != $hasta):
$consulta = "
SELECT
CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_NAME,
ADULTS,
CHILDREN,
RATE_CODE,
TRACE_ON,
DEPT_ID,
SUM(ADULTS) OVER()
|| ' ADULTOS Y '
|| SUM(CHILDREN) OVER()
|| ' NIÑOS' AS RESULT
FROM
( SELECT
MAX(CONFIRMATION_NO) AS CONFIRMATION_NO,
NATIONALITY_DESC,
GUEST_FIRST_NAME
|| ' '
|| GUEST_NAME GUEST_NAME,
MAX(ADULTS) AS ADULTS,
MAX(CHILDREN) AS CHILDREN,
RATE_CODE,
MIN(TRACE_ON) AS TRACE_ON,
LISTAGG(GUEST_RSV_TRACES.DEPT_ID, ',') WITHIN GROUP (ORDER BY NULL) AS DEPT_ID
FROM
GUEST_RSV_TRACES,
NAME_RESERVATION
WHERE trace_on BETWEEN '$desde' and '$hasta'
AND guest_rsv_traces.resv_name_id = name_reservation.resv_name_id
AND rate_code like 'ALL%'
AND resv_status not in ('CANCELLED')
AND guest_rsv_traces.dept_id not in ('TRF','OD')
GROUP BY
NATIONALITY_DESC,
GUEST_FIRST_NAME,
GUEST_NAME,
RATE_CODE
)
";
$query = oci_parse($conn, $consulta);
oci_execute($query);
endif;
?>
Output example: (Using BETWEEN '05-SEP-19' and '10-SEP-19'):
145580 EE.UU JAMES LANTZKE 3 0 ALLIN5NMT 05/09/19 HK 6 ADULTOS Y 0 NIÑOS
167410 EE.UU KATHARINE BLOOD 3 0 ALLIN5NMT 05/09/19 HK 6 ADULTOS Y 0 NIÑOS
SUMMARY:
If i do an (BETWEEN '05-SEP-19' and '10-SEP-19') it works
If i do an (BETWEEN '05-SEP-19' and '05-SEP-19') doesn't works
If i do an (trace_on = '05-SEP-19') doesn't works
I would like to know how i can query the same day using BETWEEN or a way to get results searching an specific date.
¡Thanks in advance!
Dates always have the components: year, month, day, hour, minute and second. User interfaces do not always show all these components (but they are still always there).
If your TRACE_ON column has the date 2019-09-05T12:34:56 and you are trying to see if it is BETWEEN '05-SEP-19' AND '05-SEP-19' then Oracle will (assuming that matches the NLS_DATE_FORMAT session parameter) convert the string to a date at midnight and 2019-09-05T12:34:56 BETWEEN 2019-09-05T00:00:00 AND 2019-09-05T00:00:00 is false so the row will not be returned.
Change your query to:
SELECT
-- ...
TO_CHAR( TRACE_ON, 'YYYY-MM-DD HH24:MI:SS' ),
-- ...
FROM (
SELECT
-- ...
FROM GUEST_RSV_TRACES
INNER JOIN NAME_RESERVATION
ON ( guest_rsv_traces.resv_name_id = name_reservation.resv_name_id )
WHERE trace_on >= TO_DATE( '$desde', 'DD-MON-RR' )
AND trace_on < TO_DATE( '$hasta', 'DD-MON-RR' ) + INTERVAL '1' DAY
AND -- ...
GROUP BY
-- ...
)
If column which is to be compared to those "dates" is of a DATE datatype, you shouldn't compare it to strings, because '05-SEP-19' (as well as all other values) are strings, not dates. Don't rely on implicit conversion - use either to_date function or date literal.
For example:
between date '2019-09-05' and date '2019-09-05'
or (presuming your NLS settings is set to English)
between to_date('05-sep-19', 'dd-mon-rr') and to_date('05-sep-19', 'dd-mon-rr')
Because, if you do it correctly, then between works as expected. Have a look:
SQL> create table test (id, datum) as
2 select 1, date '2019-09-04' from dual union all
3 select 2, date '2019-09-05' from dual union all --> this ...
4 select 3, date '2019-09-05' from dual union all --> ... and this are between 05.09.2019 and 05.09.2019
5 select 4, date '2019-09-06' from dual;
Table created.
SQL> alter session set nls_date_Format = 'dd.mm.yyyy';
Session altered.
SQL> select * From test
2 where datum between date '2019-09-05' and date '2019-09-05';
ID DATUM
---------- ----------
2 05.09.2019
3 05.09.2019
SQL>

PHP MYSQLI count users with two consecutive dates

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;

How to edit this mySQL so as to combine the rows of the same id into 1 instead of 3?

The following mySQL query gets data from 2 tables, alerts_data and alerts_list. The first table has the data of an alert, and the second has the description of the alert. So in the alerts_data there are multiple rows with the same alerts_data_id that is the same with the alerts_id of alerts_list.
What i want to achieve, is to display something like this
alert number 51, 5 clicked , 2 closed
alert number 57, 13 clicked, 3 closed, 8 waiting
using mySQL or PHP (i do not know if i can get this through plain mySQL)
So for now with my knowledge I can not display the data of alert 51 in one row, but because of the different alerts_data_status i have to show 3 rows for each.
How can I do it as above?
SELECT COUNT( alerts_data_id ) AS total, alerts_data_id, alerts_data_status, alerts_list.alerts_title
FROM alerts_data
JOIN alerts_list ON
alerts_data.alerts_data_id = alerts_list.alerts_id
GROUP BY alerts_data_id, alerts_data_status
//output
total - alerts_data_id - alerts_data_status - alerts_title
5 - 51 - clicked - alert number 51
2 - 52 - closed - alert number 51
13 - 57 - clicked - alert number 57
3 - 57 - waiting - alert number 57
8 - 57 waiting - alert number 57
Note: the alerts number are just examples, it can be any number
// alert_data
id - alerts_data_id - alerts_data_status
// alerts_list
alerts_id - alerts_name - alerts_text
Here's a sqlfiddle: http://sqlfiddle.com/#!9/c70c2/1
This may be an application for GROUP_CONCAT().
You first want a summary of your alerts by alerts_data_id and alerts_data_status. This is a little complex, because your sqlfiddle has a whole bunch of empty alerts_data_status strings. Here, I'm replacing those empty strings with `?'. (http://sqlfiddle.com/#!9/c70c2/23/0)
SELECT COUNT(*) AS alerts_count,
alerts_data_id,
CASE WHEN LENGTH(alerts_data_status) = 0 THEN '?'
ELSE alerts_data_status END AS alerts_data_status
FROM alerts_data
GROUP BY alerts_data_id, alerts_data_status
You then want to roll that up inside another query
SELECT SUM(a.alerts_count) total,
a.alerts_data_id, b. alerts_name,
GROUP_CONCAT( CONCAT(a.alerts_count, ': ', a.alerts_data_status)
ORDER BY a.alerts_data_status
SEPARATOR "; " ) detail
FROM (
SELECT COUNT(*) AS alerts_count,
alerts_data_id,
CASE WHEN LENGTH(alerts_data_status) = 0 THEN '?'
ELSE alerts_data_status END AS alerts_data_status
FROM alerts_data
GROUP BY alerts_data_id, alerts_data_status
) a
JOIN alerts_list b ON a.alerts_data_id = b.alerts_id
GROUP BY a.alerts_data_id, b.alerts_name
This will give you one row for each distinct alerts_data_id. Each alert is identified by its count, its id, and its name. (http://sqlfiddle.com/#!9/c70c2/26/0)
Then the row will contain a semicolon-separated list of the counts of the different alert status.
If i understand you well i think here is what you need;
SELECT
COUNT( alerts_data.id ) AS total,
ad.id,
(SELECT count(*) from alert_list al where al.alerts_id=ad.id and alerts_data_status='clicked') as clicked,
(SELECT count(*) from alert_list al where al.alerts_id=ad.id and alerts_data_status='closed') as closed,
(SELECT count(*) from alert_list al where al.alerts_id=ad.id and alerts_data_status='waiting') as waiting,
FROM alerts_data ad
GROUP BY ad.id
Was checking other answers and your SQLFIDDLE and thought this might be a nicer approach:
SELECT alerts_list.alerts_title,
SUM(CASE WHEN alerts_data_status = 'clicked' THEN 1 ELSE 0 END) AS clicked,
SUM(CASE WHEN alerts_data_status = 'closed' THEN 1 ELSE 0 END) AS closed,
SUM(CASE WHEN alerts_data_status = 'waiting' THEN 1 ELSE 0 END) AS waiting
FROM alerts_data
JOIN alerts_list ON alerts_data.alerts_data_id = alerts_list.alerts_id
GROUP BY alerts_list.alerts_title

mysql select Where (subquery) IN

I have a mysql query of type
select some_value FROM table WHERE (subquery) IN ( values )
which seems to be extremly slow!
I have a mysql table with orders and a second one with the corresponding processing states of them. I want now to show all orders having their last status code = 1 .
table order (id = primary key)
id | value
---+-------
1 + 10
2 + 12
3 + 14
table state (id = primary key)
id | orderid | code
---+---------+-------
1 + 1 + 1
2 + 2 + 1
3 + 2 + 2
4 + 1 + 3
5 + 3 + 1
My query is:
select order.id FROM order WHERE
( select state.code FROM state WHERE state.orderid = order.id ORDER BY state.id DESC LIMIT 0,1 ) IN ( '1' )
It takes roughly 15 seconds to process this for a single order. How to modify the mysql statement in order to speed the query procession time up?
Update
Try this one:
select s1.orderid
from state s1
left outer join state s2 on s1.orderid = s2.orderid
and s2.id > s1.id
where s1.code = 1
and s2.id is null
You may need an index on state.orderid.
SQL Fiddle Example
CREATE TABLE state
(`id` int, `orderid` int, `code` int)
;
INSERT INTO state
(`id`, `orderid`, `code`)
VALUES
(1, 1, 1),
(2, 2, 1),
(3, 2, 2),
(4, 1, 3),
(5, 3, 1)
;
Query:
select s1.orderid
from state s1
left outer join state s2 on s1.orderid = s2.orderid
and s2.id > s1.id
where s1.code = 1
and s2.id is null
Results:
| ORDERID |
|---------|
| 3 |
in this situation I think you could simply forget of order, as all information stays in state.
SELECT x.id, x.orderid
FROM state AS x
WHERE x.code =1
AND x.id = (
SELECT max( id )
FROM a
WHERE orderid = x.orderid )
Maybe would be possible to change your CRUD by putting last state directly in order table too, this would be the better

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